Initial production deployment setup
- Production docker-compose with nginx support - Nginx configuration for portal.monacousa.org - Deployment script with backup/restore - Gitea CI/CD workflow - Fix CountryFlag reactivity for dropdown flags Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
786
supabase/migrations/001_initial_schema.sql
Normal file
786
supabase/migrations/001_initial_schema.sql
Normal file
@@ -0,0 +1,786 @@
|
||||
-- Monaco USA Portal 2026 - Initial Database Schema
|
||||
-- Run this migration to set up all tables, views, triggers, and RLS policies
|
||||
|
||||
-- ============================================
|
||||
-- MEMBERSHIP STATUSES (Admin-configurable)
|
||||
-- ============================================
|
||||
CREATE TABLE public.membership_statuses (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
color TEXT NOT NULL DEFAULT '#6b7280',
|
||||
description TEXT,
|
||||
is_default BOOLEAN DEFAULT FALSE,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Default statuses
|
||||
INSERT INTO public.membership_statuses (name, display_name, color, description, is_default, sort_order) VALUES
|
||||
('pending', 'Pending', '#eab308', 'New member, awaiting dues payment', true, 1),
|
||||
('active', 'Active', '#22c55e', 'Dues paid, full access', false, 2),
|
||||
('inactive', 'Inactive', '#6b7280', 'Lapsed membership or suspended', false, 3),
|
||||
('expired', 'Expired', '#ef4444', 'Membership terminated', false, 4);
|
||||
|
||||
-- ============================================
|
||||
-- MEMBERSHIP TYPES (Admin-configurable pricing)
|
||||
-- ============================================
|
||||
CREATE TABLE public.membership_types (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
annual_dues DECIMAL(10,2) NOT NULL,
|
||||
description TEXT,
|
||||
is_default BOOLEAN DEFAULT FALSE,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Default membership types
|
||||
INSERT INTO public.membership_types (name, display_name, annual_dues, description, is_default, sort_order) VALUES
|
||||
('regular', 'Regular Member', 50.00, 'Standard individual membership', true, 1),
|
||||
('student', 'Student', 25.00, 'For students with valid ID', false, 2),
|
||||
('senior', 'Senior (65+)', 35.00, 'For members 65 years and older', false, 3),
|
||||
('family', 'Family', 75.00, 'Household membership', false, 4),
|
||||
('honorary', 'Honorary Member', 0.00, 'Granted by the board', false, 5);
|
||||
|
||||
-- ============================================
|
||||
-- MEMBERS TABLE
|
||||
-- ============================================
|
||||
CREATE TABLE public.members (
|
||||
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
member_id TEXT UNIQUE NOT NULL,
|
||||
|
||||
-- Personal Info
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
phone TEXT NOT NULL,
|
||||
date_of_birth DATE NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
nationality TEXT[] NOT NULL DEFAULT '{}',
|
||||
|
||||
-- Membership
|
||||
role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('member', 'board', 'admin')),
|
||||
membership_status_id UUID REFERENCES public.membership_statuses(id),
|
||||
membership_type_id UUID REFERENCES public.membership_types(id),
|
||||
member_since DATE DEFAULT CURRENT_DATE,
|
||||
|
||||
-- Profile
|
||||
avatar_url TEXT,
|
||||
|
||||
-- Admin
|
||||
notes TEXT,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Auto-generate member_id trigger
|
||||
CREATE OR REPLACE FUNCTION generate_member_id()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
next_num INTEGER;
|
||||
BEGIN
|
||||
SELECT COALESCE(MAX(CAST(SUBSTRING(member_id FROM 6) AS INTEGER)), 0) + 1
|
||||
INTO next_num
|
||||
FROM public.members;
|
||||
|
||||
NEW.member_id := 'MUSA-' || LPAD(next_num::TEXT, 4, '0');
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER set_member_id
|
||||
BEFORE INSERT ON public.members
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.member_id IS NULL)
|
||||
EXECUTE FUNCTION generate_member_id();
|
||||
|
||||
-- Update timestamp trigger
|
||||
CREATE OR REPLACE FUNCTION update_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER members_updated_at
|
||||
BEFORE UPDATE ON public.members
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- ============================================
|
||||
-- DUES PAYMENTS
|
||||
-- ============================================
|
||||
CREATE TABLE public.dues_payments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
member_id UUID NOT NULL REFERENCES public.members(id) ON DELETE CASCADE,
|
||||
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
currency TEXT DEFAULT 'EUR',
|
||||
payment_date DATE NOT NULL,
|
||||
due_date DATE NOT NULL,
|
||||
payment_method TEXT DEFAULT 'bank_transfer',
|
||||
reference TEXT,
|
||||
notes TEXT,
|
||||
|
||||
recorded_by UUID NOT NULL REFERENCES public.members(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Auto-calculate due_date (1 year from payment)
|
||||
CREATE OR REPLACE FUNCTION calculate_due_date()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.due_date IS NULL THEN
|
||||
NEW.due_date := NEW.payment_date + INTERVAL '1 year';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER set_due_date
|
||||
BEFORE INSERT ON public.dues_payments
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION calculate_due_date();
|
||||
|
||||
-- Auto-update member status to active after payment
|
||||
CREATE OR REPLACE FUNCTION update_member_status_on_payment()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
active_status_id UUID;
|
||||
BEGIN
|
||||
SELECT id INTO active_status_id
|
||||
FROM public.membership_statuses
|
||||
WHERE name = 'active';
|
||||
|
||||
UPDATE public.members
|
||||
SET membership_status_id = active_status_id,
|
||||
updated_at = NOW()
|
||||
WHERE id = NEW.member_id;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER activate_member_on_payment
|
||||
AFTER INSERT ON public.dues_payments
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_member_status_on_payment();
|
||||
|
||||
-- ============================================
|
||||
-- EVENT TYPES (Admin-configurable)
|
||||
-- ============================================
|
||||
CREATE TABLE public.event_types (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
color TEXT NOT NULL DEFAULT '#3b82f6',
|
||||
icon TEXT,
|
||||
description TEXT,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Default event types
|
||||
INSERT INTO public.event_types (name, display_name, color, icon, sort_order) VALUES
|
||||
('social', 'Social Event', '#10b981', 'party-popper', 1),
|
||||
('meeting', 'Meeting', '#6366f1', 'users', 2),
|
||||
('fundraiser', 'Fundraiser', '#f59e0b', 'heart-handshake', 3),
|
||||
('workshop', 'Workshop', '#8b5cf6', 'graduation-cap', 4),
|
||||
('gala', 'Gala/Formal', '#ec4899', 'sparkles', 5),
|
||||
('other', 'Other', '#6b7280', 'calendar', 6);
|
||||
|
||||
-- ============================================
|
||||
-- EVENTS
|
||||
-- ============================================
|
||||
CREATE TABLE public.events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Basic Info
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
event_type_id UUID REFERENCES public.event_types(id),
|
||||
|
||||
-- Date/Time
|
||||
start_datetime TIMESTAMPTZ NOT NULL,
|
||||
end_datetime TIMESTAMPTZ NOT NULL,
|
||||
all_day BOOLEAN DEFAULT FALSE,
|
||||
timezone TEXT DEFAULT 'Europe/Monaco',
|
||||
|
||||
-- Location
|
||||
location TEXT,
|
||||
location_url TEXT,
|
||||
|
||||
-- Capacity
|
||||
max_attendees INTEGER,
|
||||
max_guests_per_member INTEGER DEFAULT 1,
|
||||
|
||||
-- Pricing
|
||||
is_paid BOOLEAN DEFAULT FALSE,
|
||||
member_price DECIMAL(10,2) DEFAULT 0,
|
||||
non_member_price DECIMAL(10,2) DEFAULT 0,
|
||||
pricing_notes TEXT,
|
||||
|
||||
-- Visibility
|
||||
visibility TEXT NOT NULL DEFAULT 'members'
|
||||
CHECK (visibility IN ('public', 'members', 'board', 'admin')),
|
||||
status TEXT NOT NULL DEFAULT 'published'
|
||||
CHECK (status IN ('draft', 'published', 'cancelled', 'completed')),
|
||||
|
||||
-- Media
|
||||
cover_image_url TEXT,
|
||||
|
||||
-- Meta
|
||||
created_by UUID NOT NULL REFERENCES public.members(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TRIGGER events_updated_at
|
||||
BEFORE UPDATE ON public.events
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- ============================================
|
||||
-- EVENT RSVPs (Members)
|
||||
-- ============================================
|
||||
CREATE TABLE public.event_rsvps (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id UUID NOT NULL REFERENCES public.events(id) ON DELETE CASCADE,
|
||||
member_id UUID NOT NULL REFERENCES public.members(id) ON DELETE CASCADE,
|
||||
|
||||
status TEXT NOT NULL DEFAULT 'confirmed'
|
||||
CHECK (status IN ('confirmed', 'declined', 'maybe', 'waitlist', 'cancelled')),
|
||||
guest_count INTEGER DEFAULT 0,
|
||||
guest_names TEXT[],
|
||||
notes TEXT,
|
||||
|
||||
-- Payment
|
||||
payment_status TEXT DEFAULT 'not_required'
|
||||
CHECK (payment_status IN ('not_required', 'pending', 'paid')),
|
||||
payment_reference TEXT,
|
||||
payment_amount DECIMAL(10,2),
|
||||
|
||||
-- Attendance
|
||||
attended BOOLEAN DEFAULT FALSE,
|
||||
checked_in_at TIMESTAMPTZ,
|
||||
checked_in_by UUID REFERENCES public.members(id),
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
UNIQUE(event_id, member_id)
|
||||
);
|
||||
|
||||
CREATE TRIGGER event_rsvps_updated_at
|
||||
BEFORE UPDATE ON public.event_rsvps
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- ============================================
|
||||
-- EVENT RSVPs (Public/Non-members)
|
||||
-- ============================================
|
||||
CREATE TABLE public.event_rsvps_public (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id UUID NOT NULL REFERENCES public.events(id) ON DELETE CASCADE,
|
||||
|
||||
full_name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
phone TEXT,
|
||||
|
||||
status TEXT NOT NULL DEFAULT 'confirmed'
|
||||
CHECK (status IN ('confirmed', 'declined', 'maybe', 'waitlist', 'cancelled')),
|
||||
guest_count INTEGER DEFAULT 0,
|
||||
guest_names TEXT[],
|
||||
|
||||
-- Payment
|
||||
payment_status TEXT DEFAULT 'not_required'
|
||||
CHECK (payment_status IN ('not_required', 'pending', 'paid')),
|
||||
payment_reference TEXT,
|
||||
payment_amount DECIMAL(10,2),
|
||||
|
||||
-- Attendance
|
||||
attended BOOLEAN DEFAULT FALSE,
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
UNIQUE(event_id, email)
|
||||
);
|
||||
|
||||
CREATE TRIGGER event_rsvps_public_updated_at
|
||||
BEFORE UPDATE ON public.event_rsvps_public
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- ============================================
|
||||
-- DOCUMENT CATEGORIES
|
||||
-- ============================================
|
||||
CREATE TABLE public.document_categories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
icon TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Default categories
|
||||
INSERT INTO public.document_categories (name, display_name, icon, sort_order) VALUES
|
||||
('meeting_minutes', 'Meeting Minutes', 'file-text', 1),
|
||||
('governance', 'Governance & Bylaws', 'scale', 2),
|
||||
('legal', 'Legal Documents', 'briefcase', 3),
|
||||
('financial', 'Financial Reports', 'dollar-sign', 4),
|
||||
('member_resources', 'Member Resources', 'book-open', 5),
|
||||
('forms', 'Forms & Templates', 'clipboard', 6),
|
||||
('other', 'Other Documents', 'file', 7);
|
||||
|
||||
-- ============================================
|
||||
-- DOCUMENTS
|
||||
-- ============================================
|
||||
CREATE TABLE public.documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category_id UUID REFERENCES public.document_categories(id),
|
||||
|
||||
-- File Info
|
||||
file_path TEXT NOT NULL,
|
||||
file_name TEXT NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
|
||||
-- Visibility
|
||||
visibility TEXT NOT NULL DEFAULT 'members'
|
||||
CHECK (visibility IN ('public', 'members', 'board', 'admin')),
|
||||
allowed_member_ids UUID[],
|
||||
|
||||
-- Version tracking
|
||||
version INTEGER DEFAULT 1,
|
||||
replaces_document_id UUID REFERENCES public.documents(id),
|
||||
|
||||
-- Meeting-specific fields
|
||||
meeting_date DATE,
|
||||
meeting_attendees UUID[],
|
||||
|
||||
-- Meta
|
||||
uploaded_by UUID NOT NULL REFERENCES public.members(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TRIGGER documents_updated_at
|
||||
BEFORE UPDATE ON public.documents
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- ============================================
|
||||
-- APP SETTINGS (Unified key-value store)
|
||||
-- ============================================
|
||||
CREATE TABLE public.app_settings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
category TEXT NOT NULL,
|
||||
setting_key TEXT NOT NULL,
|
||||
setting_value JSONB NOT NULL,
|
||||
setting_type TEXT NOT NULL DEFAULT 'text'
|
||||
CHECK (setting_type IN ('text', 'number', 'boolean', 'json', 'array')),
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
is_public BOOLEAN DEFAULT FALSE,
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_by UUID REFERENCES public.members(id),
|
||||
|
||||
UNIQUE(category, setting_key)
|
||||
);
|
||||
|
||||
-- Default settings
|
||||
INSERT INTO public.app_settings (category, setting_key, setting_value, setting_type, display_name, description, is_public) VALUES
|
||||
-- Organization
|
||||
('organization', 'association_name', '"Monaco USA"', 'text', 'Association Name', 'Official name of the association', true),
|
||||
('organization', 'tagline', '"Americans in Monaco"', 'text', 'Tagline', 'Association tagline shown on login', true),
|
||||
('organization', 'contact_email', '"contact@monacousa.org"', 'text', 'Contact Email', 'Public contact email', true),
|
||||
('organization', 'primary_color', '"#dc2626"', 'text', 'Primary Color', 'Brand primary color (hex)', true),
|
||||
|
||||
-- Dues
|
||||
('dues', 'payment_iban', '"MC58 1756 9000 0104 0050 1001 860"', 'text', 'Payment IBAN', 'Bank IBAN for dues', false),
|
||||
('dues', 'payment_account_holder', '"ASSOCIATION MONACO USA"', 'text', 'Account Holder', 'Bank account holder name', false),
|
||||
('dues', 'payment_bank_name', '"Credit Foncier de Monaco"', 'text', 'Bank Name', 'Name of the bank', false),
|
||||
('dues', 'reminder_days_before', '[30, 7, 1]', 'array', 'Reminder Days', 'Days before due to send reminders', false),
|
||||
('dues', 'grace_period_days', '30', 'number', 'Grace Period', 'Days after due before auto-inactive', false),
|
||||
('dues', 'auto_inactive_enabled', 'true', 'boolean', 'Auto Inactive', 'Auto set inactive after grace period', false),
|
||||
|
||||
-- System
|
||||
('system', 'maintenance_mode', 'false', 'boolean', 'Maintenance Mode', 'Put portal in maintenance mode', false),
|
||||
('system', 'max_upload_size_mb', '50', 'number', 'Max Upload Size', 'Maximum file upload size in MB', false);
|
||||
|
||||
-- ============================================
|
||||
-- EMAIL TEMPLATES
|
||||
-- ============================================
|
||||
CREATE TABLE public.email_templates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
template_key TEXT UNIQUE NOT NULL,
|
||||
template_name TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
body_html TEXT NOT NULL,
|
||||
body_text TEXT,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_system BOOLEAN DEFAULT FALSE,
|
||||
variables_schema JSONB,
|
||||
preview_data JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_by UUID REFERENCES public.members(id)
|
||||
);
|
||||
|
||||
CREATE TRIGGER email_templates_updated_at
|
||||
BEFORE UPDATE ON public.email_templates
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- ============================================
|
||||
-- EMAIL LOGS
|
||||
-- ============================================
|
||||
CREATE TABLE public.email_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
recipient_id UUID REFERENCES public.members(id),
|
||||
recipient_email TEXT NOT NULL,
|
||||
recipient_name TEXT,
|
||||
template_key TEXT,
|
||||
subject TEXT NOT NULL,
|
||||
email_type TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'queued'
|
||||
CHECK (status IN ('queued', 'sent', 'delivered', 'opened', 'clicked', 'bounced', 'failed')),
|
||||
provider TEXT,
|
||||
provider_message_id TEXT,
|
||||
opened_at TIMESTAMPTZ,
|
||||
clicked_at TIMESTAMPTZ,
|
||||
error_message TEXT,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
template_variables JSONB,
|
||||
sent_by UUID REFERENCES public.members(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
sent_at TIMESTAMPTZ,
|
||||
delivered_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- VIEWS
|
||||
-- ============================================
|
||||
|
||||
-- Members with dues status
|
||||
CREATE VIEW public.members_with_dues AS
|
||||
SELECT
|
||||
m.*,
|
||||
ms.name as status_name,
|
||||
ms.display_name as status_display_name,
|
||||
ms.color as status_color,
|
||||
mt.display_name as membership_type_name,
|
||||
mt.annual_dues,
|
||||
dp.last_payment_date,
|
||||
dp.current_due_date,
|
||||
CASE
|
||||
WHEN dp.current_due_date IS NULL THEN 'never_paid'
|
||||
WHEN dp.current_due_date < CURRENT_DATE THEN 'overdue'
|
||||
WHEN dp.current_due_date < CURRENT_DATE + INTERVAL '30 days' THEN 'due_soon'
|
||||
ELSE 'current'
|
||||
END as dues_status,
|
||||
CASE
|
||||
WHEN dp.current_due_date < CURRENT_DATE
|
||||
THEN (CURRENT_DATE - dp.current_due_date)::INTEGER
|
||||
ELSE NULL
|
||||
END as days_overdue,
|
||||
CASE
|
||||
WHEN dp.current_due_date >= CURRENT_DATE
|
||||
THEN (dp.current_due_date - CURRENT_DATE)::INTEGER
|
||||
ELSE NULL
|
||||
END as days_until_due
|
||||
FROM public.members m
|
||||
LEFT JOIN public.membership_statuses ms ON m.membership_status_id = ms.id
|
||||
LEFT JOIN public.membership_types mt ON m.membership_type_id = mt.id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
payment_date as last_payment_date,
|
||||
due_date as current_due_date
|
||||
FROM public.dues_payments
|
||||
WHERE member_id = m.id
|
||||
ORDER BY due_date DESC
|
||||
LIMIT 1
|
||||
) dp ON true;
|
||||
|
||||
-- Events with attendee counts
|
||||
CREATE VIEW public.events_with_counts AS
|
||||
SELECT
|
||||
e.*,
|
||||
et.display_name as event_type_name,
|
||||
et.color as event_type_color,
|
||||
et.icon as event_type_icon,
|
||||
COALESCE(member_rsvps.confirmed_count, 0) +
|
||||
COALESCE(member_rsvps.guest_count, 0) +
|
||||
COALESCE(public_rsvps.confirmed_count, 0) +
|
||||
COALESCE(public_rsvps.guest_count, 0) as total_attendees,
|
||||
COALESCE(member_rsvps.confirmed_count, 0) as member_count,
|
||||
COALESCE(public_rsvps.confirmed_count, 0) as non_member_count,
|
||||
COALESCE(member_rsvps.waitlist_count, 0) +
|
||||
COALESCE(public_rsvps.waitlist_count, 0) as waitlist_count,
|
||||
CASE
|
||||
WHEN e.max_attendees IS NULL THEN FALSE
|
||||
WHEN (COALESCE(member_rsvps.confirmed_count, 0) +
|
||||
COALESCE(member_rsvps.guest_count, 0) +
|
||||
COALESCE(public_rsvps.confirmed_count, 0) +
|
||||
COALESCE(public_rsvps.guest_count, 0)) >= e.max_attendees THEN TRUE
|
||||
ELSE FALSE
|
||||
END as is_full
|
||||
FROM public.events e
|
||||
LEFT JOIN public.event_types et ON e.event_type_id = et.id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE status = 'confirmed') as confirmed_count,
|
||||
COALESCE(SUM(guest_count) FILTER (WHERE status = 'confirmed'), 0) as guest_count,
|
||||
COUNT(*) FILTER (WHERE status = 'waitlist') as waitlist_count
|
||||
FROM public.event_rsvps
|
||||
WHERE event_id = e.id
|
||||
) member_rsvps ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE status = 'confirmed') as confirmed_count,
|
||||
COALESCE(SUM(guest_count) FILTER (WHERE status = 'confirmed'), 0) as guest_count,
|
||||
COUNT(*) FILTER (WHERE status = 'waitlist') as waitlist_count
|
||||
FROM public.event_rsvps_public
|
||||
WHERE event_id = e.id
|
||||
) public_rsvps ON true;
|
||||
|
||||
-- ============================================
|
||||
-- ROW LEVEL SECURITY
|
||||
-- ============================================
|
||||
|
||||
-- Enable RLS on all tables
|
||||
ALTER TABLE public.members ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.dues_payments ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.events ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.event_rsvps ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.event_rsvps_public ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.app_settings ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.email_templates ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.email_logs ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- MEMBERS POLICIES
|
||||
CREATE POLICY "Members viewable by authenticated users"
|
||||
ON public.members FOR SELECT
|
||||
TO authenticated
|
||||
USING (true);
|
||||
|
||||
CREATE POLICY "Users can update own profile"
|
||||
ON public.members FOR UPDATE
|
||||
TO authenticated
|
||||
USING (auth.uid() = id);
|
||||
|
||||
CREATE POLICY "Admins can insert members"
|
||||
ON public.members FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin')
|
||||
OR auth.uid() = id
|
||||
);
|
||||
|
||||
CREATE POLICY "Admins can delete members"
|
||||
ON public.members FOR DELETE
|
||||
TO authenticated
|
||||
USING (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin')
|
||||
);
|
||||
|
||||
-- DUES PAYMENTS POLICIES
|
||||
CREATE POLICY "Own payments viewable"
|
||||
ON public.dues_payments FOR SELECT
|
||||
TO authenticated
|
||||
USING (
|
||||
member_id = auth.uid()
|
||||
OR EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin'))
|
||||
);
|
||||
|
||||
CREATE POLICY "Board can record payments"
|
||||
ON public.dues_payments FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin'))
|
||||
);
|
||||
|
||||
-- EVENTS POLICIES
|
||||
CREATE POLICY "Events viewable based on visibility"
|
||||
ON public.events FOR SELECT
|
||||
TO authenticated
|
||||
USING (
|
||||
visibility = 'members'
|
||||
OR visibility = 'public'
|
||||
OR (visibility = 'board' AND EXISTS (
|
||||
SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin')
|
||||
))
|
||||
OR (visibility = 'admin' AND EXISTS (
|
||||
SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin'
|
||||
))
|
||||
);
|
||||
|
||||
CREATE POLICY "Public events viewable by anyone"
|
||||
ON public.events FOR SELECT
|
||||
TO anon
|
||||
USING (visibility = 'public' AND status = 'published');
|
||||
|
||||
CREATE POLICY "Board can manage events"
|
||||
ON public.events FOR ALL
|
||||
TO authenticated
|
||||
USING (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin'))
|
||||
);
|
||||
|
||||
-- EVENT RSVPs POLICIES
|
||||
CREATE POLICY "RSVPs viewable by member and board"
|
||||
ON public.event_rsvps FOR SELECT
|
||||
TO authenticated
|
||||
USING (
|
||||
member_id = auth.uid()
|
||||
OR EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin'))
|
||||
);
|
||||
|
||||
CREATE POLICY "Members can manage own RSVPs"
|
||||
ON public.event_rsvps FOR ALL
|
||||
TO authenticated
|
||||
USING (member_id = auth.uid())
|
||||
WITH CHECK (member_id = auth.uid());
|
||||
|
||||
CREATE POLICY "Board can manage all RSVPs"
|
||||
ON public.event_rsvps FOR ALL
|
||||
TO authenticated
|
||||
USING (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin'))
|
||||
);
|
||||
|
||||
-- PUBLIC RSVPs POLICIES
|
||||
CREATE POLICY "Public RSVPs viewable by board"
|
||||
ON public.event_rsvps_public FOR SELECT
|
||||
TO authenticated
|
||||
USING (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin'))
|
||||
);
|
||||
|
||||
CREATE POLICY "Anyone can create public RSVP"
|
||||
ON public.event_rsvps_public FOR INSERT
|
||||
TO anon, authenticated
|
||||
WITH CHECK (true);
|
||||
|
||||
CREATE POLICY "Board can manage public RSVPs"
|
||||
ON public.event_rsvps_public FOR ALL
|
||||
TO authenticated
|
||||
USING (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin'))
|
||||
);
|
||||
|
||||
-- DOCUMENTS POLICIES
|
||||
CREATE POLICY "Documents viewable based on visibility"
|
||||
ON public.documents FOR SELECT
|
||||
TO authenticated
|
||||
USING (
|
||||
visibility = 'members'
|
||||
OR visibility = 'public'
|
||||
OR (visibility = 'board' AND EXISTS (
|
||||
SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin')
|
||||
))
|
||||
OR (visibility = 'admin' AND EXISTS (
|
||||
SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin'
|
||||
))
|
||||
OR (allowed_member_ids IS NOT NULL AND auth.uid() = ANY(allowed_member_ids))
|
||||
);
|
||||
|
||||
CREATE POLICY "Board can upload documents"
|
||||
ON public.documents FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin'))
|
||||
);
|
||||
|
||||
CREATE POLICY "Admin can manage all documents"
|
||||
ON public.documents FOR ALL
|
||||
TO authenticated
|
||||
USING (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin')
|
||||
);
|
||||
|
||||
-- APP SETTINGS POLICIES
|
||||
CREATE POLICY "Public settings viewable by anyone"
|
||||
ON public.app_settings FOR SELECT
|
||||
USING (is_public = true);
|
||||
|
||||
CREATE POLICY "All settings viewable by admin"
|
||||
ON public.app_settings FOR SELECT
|
||||
TO authenticated
|
||||
USING (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin')
|
||||
);
|
||||
|
||||
CREATE POLICY "Admin can manage settings"
|
||||
ON public.app_settings FOR ALL
|
||||
TO authenticated
|
||||
USING (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin')
|
||||
);
|
||||
|
||||
-- EMAIL TEMPLATES POLICIES
|
||||
CREATE POLICY "Admin can manage email templates"
|
||||
ON public.email_templates FOR ALL
|
||||
TO authenticated
|
||||
USING (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin')
|
||||
);
|
||||
|
||||
-- EMAIL LOGS POLICIES
|
||||
CREATE POLICY "Own email logs viewable"
|
||||
ON public.email_logs FOR SELECT
|
||||
TO authenticated
|
||||
USING (
|
||||
recipient_id = auth.uid()
|
||||
OR EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin')
|
||||
);
|
||||
|
||||
CREATE POLICY "Admin can manage email logs"
|
||||
ON public.email_logs FOR ALL
|
||||
TO authenticated
|
||||
USING (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin')
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- INDEXES
|
||||
-- ============================================
|
||||
CREATE INDEX idx_members_email ON public.members(email);
|
||||
CREATE INDEX idx_members_member_id ON public.members(member_id);
|
||||
CREATE INDEX idx_members_role ON public.members(role);
|
||||
CREATE INDEX idx_members_status ON public.members(membership_status_id);
|
||||
|
||||
CREATE INDEX idx_dues_payments_member ON public.dues_payments(member_id);
|
||||
CREATE INDEX idx_dues_payments_date ON public.dues_payments(payment_date DESC);
|
||||
|
||||
CREATE INDEX idx_events_start ON public.events(start_datetime);
|
||||
CREATE INDEX idx_events_visibility ON public.events(visibility);
|
||||
CREATE INDEX idx_events_status ON public.events(status);
|
||||
|
||||
CREATE INDEX idx_event_rsvps_event ON public.event_rsvps(event_id);
|
||||
CREATE INDEX idx_event_rsvps_member ON public.event_rsvps(member_id);
|
||||
|
||||
CREATE INDEX idx_documents_category ON public.documents(category_id);
|
||||
CREATE INDEX idx_documents_visibility ON public.documents(visibility);
|
||||
|
||||
CREATE INDEX idx_app_settings_category ON public.app_settings(category, setting_key);
|
||||
|
||||
CREATE INDEX idx_email_logs_recipient ON public.email_logs(recipient_id);
|
||||
CREATE INDEX idx_email_logs_status ON public.email_logs(status);
|
||||
CREATE INDEX idx_email_logs_created ON public.email_logs(created_at DESC);
|
||||
35
supabase/migrations/002_admin_integrations_settings.sql
Normal file
35
supabase/migrations/002_admin_integrations_settings.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
-- ============================================
|
||||
-- ADMIN INTEGRATION SETTINGS
|
||||
-- SMTP, S3/MinIO, and expanded system settings
|
||||
-- ============================================
|
||||
|
||||
-- Add SMTP settings
|
||||
INSERT INTO public.app_settings (category, setting_key, setting_value, setting_type, display_name, description, is_public) VALUES
|
||||
-- Email/SMTP Configuration
|
||||
('email', 'smtp_host', '""', 'text', 'SMTP Host', 'SMTP server hostname (e.g., smtp.gmail.com)', false),
|
||||
('email', 'smtp_port', '587', 'number', 'SMTP Port', 'SMTP server port (25, 465, 587)', false),
|
||||
('email', 'smtp_secure', 'true', 'boolean', 'Use TLS/SSL', 'Enable secure connection (recommended)', false),
|
||||
('email', 'smtp_username', '""', 'text', 'SMTP Username', 'SMTP authentication username', false),
|
||||
('email', 'smtp_password', '""', 'text', 'SMTP Password', 'SMTP authentication password', false),
|
||||
('email', 'smtp_from_address', '"noreply@monacousa.org"', 'text', 'From Address', 'Default sender email address', false),
|
||||
('email', 'smtp_from_name', '"Monaco USA"', 'text', 'From Name', 'Default sender display name', false),
|
||||
('email', 'smtp_reply_to', '"contact@monacousa.org"', 'text', 'Reply-To Address', 'Reply-to email address', false),
|
||||
('email', 'smtp_enabled', 'false', 'boolean', 'Enable Email', 'Enable sending emails via SMTP', false),
|
||||
|
||||
-- S3/MinIO Storage Configuration
|
||||
('storage', 's3_endpoint', '""', 'text', 'S3 Endpoint', 'S3-compatible endpoint URL (e.g., http://minio:9000)', false),
|
||||
('storage', 's3_bucket', '"monacousa-documents"', 'text', 'Bucket Name', 'S3 bucket name for file storage', false),
|
||||
('storage', 's3_access_key', '""', 'text', 'Access Key', 'S3 access key ID', false),
|
||||
('storage', 's3_secret_key', '""', 'text', 'Secret Key', 'S3 secret access key', false),
|
||||
('storage', 's3_region', '"us-east-1"', 'text', 'Region', 'S3 region (use us-east-1 for MinIO)', false),
|
||||
('storage', 's3_use_ssl', 'false', 'boolean', 'Use SSL', 'Enable SSL for S3 connections', false),
|
||||
('storage', 's3_force_path_style', 'true', 'boolean', 'Force Path Style', 'Use path-style URLs (required for MinIO)', false),
|
||||
('storage', 's3_enabled', 'false', 'boolean', 'Enable S3 Storage', 'Use external S3 instead of Supabase Storage', false),
|
||||
|
||||
-- Additional System Settings
|
||||
('system', 'session_timeout_hours', '168', 'number', 'Session Timeout', 'Hours until session expires (default: 7 days)', false),
|
||||
('system', 'allowed_file_types', '["pdf","doc","docx","xls","xlsx","ppt","pptx","txt","jpg","jpeg","png","webp"]', 'array', 'Allowed File Types', 'Allowed file extensions for uploads', false),
|
||||
('system', 'maintenance_message', '"The portal is currently undergoing maintenance. Please check back soon."', 'text', 'Maintenance Message', 'Message shown during maintenance', false),
|
||||
('system', 'enable_public_events', 'true', 'boolean', 'Enable Public Events', 'Allow non-members to view public events', false),
|
||||
('system', 'enable_public_rsvp', 'true', 'boolean', 'Enable Public RSVP', 'Allow non-members to RSVP to public events', false)
|
||||
ON CONFLICT (category, setting_key) DO NOTHING;
|
||||
504
supabase/migrations/003_storage_buckets_and_audit.sql
Normal file
504
supabase/migrations/003_storage_buckets_and_audit.sql
Normal file
@@ -0,0 +1,504 @@
|
||||
-- Monaco USA Portal 2026
|
||||
-- Migration 003: Storage Buckets and Audit Logging
|
||||
-- ================================================
|
||||
|
||||
-- ============================================
|
||||
-- STORAGE BUCKETS
|
||||
-- ============================================
|
||||
|
||||
-- Documents bucket (public for direct URL access - visibility controlled at app level)
|
||||
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||
VALUES (
|
||||
'documents',
|
||||
'documents',
|
||||
true,
|
||||
52428800, -- 50MB
|
||||
ARRAY['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'text/plain', 'text/csv', 'application/json', 'image/jpeg', 'image/png', 'image/webp', 'image/gif']
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
public = true,
|
||||
file_size_limit = EXCLUDED.file_size_limit,
|
||||
allowed_mime_types = EXCLUDED.allowed_mime_types;
|
||||
|
||||
-- Avatars bucket (public for display)
|
||||
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||
VALUES (
|
||||
'avatars',
|
||||
'avatars',
|
||||
true,
|
||||
5242880, -- 5MB
|
||||
ARRAY['image/jpeg', 'image/png', 'image/webp', 'image/gif']
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
public = true,
|
||||
file_size_limit = EXCLUDED.file_size_limit,
|
||||
allowed_mime_types = EXCLUDED.allowed_mime_types;
|
||||
|
||||
-- Event images bucket (public for display)
|
||||
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||
VALUES (
|
||||
'event-images',
|
||||
'event-images',
|
||||
true,
|
||||
10485760, -- 10MB
|
||||
ARRAY['image/jpeg', 'image/png', 'image/webp']
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
public = true,
|
||||
file_size_limit = EXCLUDED.file_size_limit,
|
||||
allowed_mime_types = EXCLUDED.allowed_mime_types;
|
||||
|
||||
-- ============================================
|
||||
-- STORAGE POLICIES
|
||||
-- ============================================
|
||||
|
||||
-- Documents bucket policies
|
||||
DROP POLICY IF EXISTS "documents_read_policy" ON storage.objects;
|
||||
CREATE POLICY "documents_read_policy" ON storage.objects FOR SELECT
|
||||
USING (bucket_id = 'documents' AND auth.role() = 'authenticated');
|
||||
|
||||
DROP POLICY IF EXISTS "documents_insert_policy" ON storage.objects;
|
||||
CREATE POLICY "documents_insert_policy" ON storage.objects FOR INSERT
|
||||
WITH CHECK (
|
||||
bucket_id = 'documents'
|
||||
AND auth.role() = 'authenticated'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.members
|
||||
WHERE id = auth.uid()
|
||||
AND role IN ('board', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "documents_delete_policy" ON storage.objects;
|
||||
CREATE POLICY "documents_delete_policy" ON storage.objects FOR DELETE
|
||||
USING (
|
||||
bucket_id = 'documents'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.members
|
||||
WHERE id = auth.uid()
|
||||
AND role = 'admin'
|
||||
)
|
||||
);
|
||||
|
||||
-- Avatars bucket policies (public read, user-specific write)
|
||||
DROP POLICY IF EXISTS "avatars_read_policy" ON storage.objects;
|
||||
CREATE POLICY "avatars_read_policy" ON storage.objects FOR SELECT
|
||||
USING (bucket_id = 'avatars');
|
||||
|
||||
DROP POLICY IF EXISTS "avatars_insert_policy" ON storage.objects;
|
||||
CREATE POLICY "avatars_insert_policy" ON storage.objects FOR INSERT
|
||||
WITH CHECK (
|
||||
bucket_id = 'avatars'
|
||||
AND auth.role() = 'authenticated'
|
||||
AND (storage.foldername(name))[1] = auth.uid()::text
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "avatars_update_policy" ON storage.objects;
|
||||
CREATE POLICY "avatars_update_policy" ON storage.objects FOR UPDATE
|
||||
USING (
|
||||
bucket_id = 'avatars'
|
||||
AND auth.role() = 'authenticated'
|
||||
AND (storage.foldername(name))[1] = auth.uid()::text
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "avatars_delete_policy" ON storage.objects;
|
||||
CREATE POLICY "avatars_delete_policy" ON storage.objects FOR DELETE
|
||||
USING (
|
||||
bucket_id = 'avatars'
|
||||
AND auth.role() = 'authenticated'
|
||||
AND (storage.foldername(name))[1] = auth.uid()::text
|
||||
);
|
||||
|
||||
-- Event images bucket policies
|
||||
DROP POLICY IF EXISTS "event_images_read_policy" ON storage.objects;
|
||||
CREATE POLICY "event_images_read_policy" ON storage.objects FOR SELECT
|
||||
USING (bucket_id = 'event-images');
|
||||
|
||||
DROP POLICY IF EXISTS "event_images_insert_policy" ON storage.objects;
|
||||
CREATE POLICY "event_images_insert_policy" ON storage.objects FOR INSERT
|
||||
WITH CHECK (
|
||||
bucket_id = 'event-images'
|
||||
AND auth.role() = 'authenticated'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.members
|
||||
WHERE id = auth.uid()
|
||||
AND role IN ('board', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "event_images_delete_policy" ON storage.objects;
|
||||
CREATE POLICY "event_images_delete_policy" ON storage.objects FOR DELETE
|
||||
USING (
|
||||
bucket_id = 'event-images'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.members
|
||||
WHERE id = auth.uid()
|
||||
AND role IN ('board', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- AUDIT LOGS TABLE
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
user_email TEXT,
|
||||
action TEXT NOT NULL,
|
||||
resource_type TEXT,
|
||||
resource_id TEXT,
|
||||
details JSONB DEFAULT '{}',
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Index for querying audit logs
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_resource_type ON audit_logs(resource_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at DESC);
|
||||
|
||||
-- RLS for audit logs (only admins can read, service role can write)
|
||||
ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS "audit_logs_read_admin" ON audit_logs;
|
||||
CREATE POLICY "audit_logs_read_admin" ON audit_logs FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.members
|
||||
WHERE id = auth.uid()
|
||||
AND role = 'admin'
|
||||
)
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- DEFAULT EMAIL TEMPLATES
|
||||
-- ============================================
|
||||
|
||||
-- Insert default email templates if they don't exist
|
||||
-- Using Monaco-branded design matching the login screen
|
||||
INSERT INTO email_templates (template_key, template_name, category, subject, body_html, body_text, is_system)
|
||||
VALUES
|
||||
(
|
||||
'welcome',
|
||||
'Welcome Email',
|
||||
'member',
|
||||
'Welcome to Monaco USA, {{first_name}}!',
|
||||
'<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f172a;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background: linear-gradient(135deg, rgba(15, 23, 42, 0.9) 0%, rgba(30, 41, 59, 0.85) 50%, rgba(127, 29, 29, 0.8) 100%); background-color: #0f172a;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 20px;">
|
||||
<!-- Logo Section -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<div style="background: rgba(255, 255, 255, 0.95); border-radius: 16px; padding: 12px; display: inline-block; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);">
|
||||
<img src="{{logo_url}}" alt="Monaco USA" width="80" height="80" style="display: block; border-radius: 8px;">
|
||||
</div>
|
||||
<h1 style="margin: 16px 0 4px 0; font-size: 24px; font-weight: bold; color: #ffffff; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);">Monaco <span style="color: #fca5a5;">USA</span></h1>
|
||||
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.8);">Americans in Monaco</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" style="max-width: 480px; width: 100%; background: #ffffff; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
|
||||
<tr>
|
||||
<td style="padding: 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; color: #CE1126; font-size: 24px;">Welcome to Monaco USA!</h2>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">We are thrilled to welcome you to the Monaco USA community! Your membership has been created and you can now access all member features.</p>
|
||||
<p style="margin: 0 0 12px 0; color: #334155; font-weight: 600;">To get started:</p>
|
||||
<ol style="margin: 0 0 20px 20px; padding: 0; color: #334155; line-height: 1.8;">
|
||||
<li>Set up your password using the separate email we sent</li>
|
||||
<li>Complete your profile with your details</li>
|
||||
<li>Explore upcoming events and connect with fellow members</li>
|
||||
</ol>
|
||||
<p style="margin: 0 0 20px 0; color: #334155; line-height: 1.6;">If you have any questions, please don''t hesitate to reach out to our board members.</p>
|
||||
<p style="margin: 0; color: #334155;">Best regards,<br><strong style="color: #CE1126;">The Monaco USA Team</strong></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 24px;">
|
||||
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.5);">© 2026 Monaco USA. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>',
|
||||
'Welcome to Monaco USA, {{first_name}}! Your membership has been created. Please set up your password and complete your profile.',
|
||||
true
|
||||
),
|
||||
(
|
||||
'waitlist_promotion',
|
||||
'Waitlist Promotion',
|
||||
'event',
|
||||
'Great news! You''re confirmed for {{event_title}}',
|
||||
'<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f172a;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background: linear-gradient(135deg, rgba(15, 23, 42, 0.9) 0%, rgba(30, 41, 59, 0.85) 50%, rgba(127, 29, 29, 0.8) 100%); background-color: #0f172a;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 20px;">
|
||||
<!-- Logo Section -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<div style="background: rgba(255, 255, 255, 0.95); border-radius: 16px; padding: 12px; display: inline-block; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);">
|
||||
<img src="{{logo_url}}" alt="Monaco USA" width="80" height="80" style="display: block; border-radius: 8px;">
|
||||
</div>
|
||||
<h1 style="margin: 16px 0 4px 0; font-size: 24px; font-weight: bold; color: #ffffff; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);">Monaco <span style="color: #fca5a5;">USA</span></h1>
|
||||
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.8);">Americans in Monaco</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" style="max-width: 480px; width: 100%; background: #ffffff; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
|
||||
<tr>
|
||||
<td style="padding: 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; color: #CE1126; font-size: 24px;">You''re In!</h2>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 20px 0; color: #334155; line-height: 1.6;">Great news! A spot has opened up for <strong>{{event_title}}</strong> and you have been moved from the waitlist to confirmed!</p>
|
||||
<div style="background: #f8fafc; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #64748b; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Event Details</p>
|
||||
<p style="margin: 0 0 8px 0; color: #334155;"><strong>Date:</strong> {{event_date}}</p>
|
||||
<p style="margin: 0; color: #334155;"><strong>Location:</strong> {{event_location}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0; color: #334155; line-height: 1.6;">We look forward to seeing you there!</p>
|
||||
<p style="margin: 0; color: #334155;">Best regards,<br><strong style="color: #CE1126;">The Monaco USA Team</strong></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 24px;">
|
||||
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.5);">© 2026 Monaco USA. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>',
|
||||
'Great news! A spot has opened up for {{event_title}} and you''ve been confirmed. See you on {{event_date}} at {{event_location}}!',
|
||||
true
|
||||
),
|
||||
(
|
||||
'rsvp_confirmation',
|
||||
'RSVP Confirmation',
|
||||
'event',
|
||||
'RSVP Confirmed: {{event_title}}',
|
||||
'<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f172a;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background: linear-gradient(135deg, rgba(15, 23, 42, 0.9) 0%, rgba(30, 41, 59, 0.85) 50%, rgba(127, 29, 29, 0.8) 100%); background-color: #0f172a;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 20px;">
|
||||
<!-- Logo Section -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<div style="background: rgba(255, 255, 255, 0.95); border-radius: 16px; padding: 12px; display: inline-block; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);">
|
||||
<img src="{{logo_url}}" alt="Monaco USA" width="80" height="80" style="display: block; border-radius: 8px;">
|
||||
</div>
|
||||
<h1 style="margin: 16px 0 4px 0; font-size: 24px; font-weight: bold; color: #ffffff; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);">Monaco <span style="color: #fca5a5;">USA</span></h1>
|
||||
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.8);">Americans in Monaco</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" style="max-width: 480px; width: 100%; background: #ffffff; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
|
||||
<tr>
|
||||
<td style="padding: 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; color: #CE1126; font-size: 24px;">RSVP Confirmed!</h2>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 20px 0; color: #334155; line-height: 1.6;">Your RSVP for <strong>{{event_title}}</strong> has been confirmed.</p>
|
||||
<div style="background: #f8fafc; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #64748b; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Event Details</p>
|
||||
<p style="margin: 0 0 8px 0; color: #334155;"><strong>Date:</strong> {{event_date}}</p>
|
||||
<p style="margin: 0 0 8px 0; color: #334155;"><strong>Time:</strong> {{event_time}}</p>
|
||||
<p style="margin: 0 0 8px 0; color: #334155;"><strong>Location:</strong> {{event_location}}</p>
|
||||
<p style="margin: 0; color: #334155;"><strong>Guests:</strong> {{guest_count}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0; color: #334155; line-height: 1.6;">We look forward to seeing you!</p>
|
||||
<p style="margin: 0; color: #334155;">Best regards,<br><strong style="color: #CE1126;">The Monaco USA Team</strong></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 24px;">
|
||||
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.5);">© 2026 Monaco USA. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>',
|
||||
'Your RSVP for {{event_title}} is confirmed! See you on {{event_date}} at {{event_location}}.',
|
||||
true
|
||||
),
|
||||
(
|
||||
'payment_received',
|
||||
'Payment Received',
|
||||
'dues',
|
||||
'Payment Received - Monaco USA',
|
||||
'<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f172a;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background: linear-gradient(135deg, rgba(15, 23, 42, 0.9) 0%, rgba(30, 41, 59, 0.85) 50%, rgba(127, 29, 29, 0.8) 100%); background-color: #0f172a;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 20px;">
|
||||
<!-- Logo Section -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<div style="background: rgba(255, 255, 255, 0.95); border-radius: 16px; padding: 12px; display: inline-block; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);">
|
||||
<img src="{{logo_url}}" alt="Monaco USA" width="80" height="80" style="display: block; border-radius: 8px;">
|
||||
</div>
|
||||
<h1 style="margin: 16px 0 4px 0; font-size: 24px; font-weight: bold; color: #ffffff; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);">Monaco <span style="color: #fca5a5;">USA</span></h1>
|
||||
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.8);">Americans in Monaco</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" style="max-width: 480px; width: 100%; background: #ffffff; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
|
||||
<tr>
|
||||
<td style="padding: 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; color: #CE1126; font-size: 24px;">Payment Received</h2>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 20px 0; color: #334155; line-height: 1.6;">We have received your payment. Thank you!</p>
|
||||
<div style="background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #166534; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Payment Details</p>
|
||||
<p style="margin: 0 0 8px 0; color: #334155;"><strong>Amount:</strong> ${{amount}}</p>
|
||||
<p style="margin: 0 0 8px 0; color: #334155;"><strong>Date:</strong> {{payment_date}}</p>
|
||||
<p style="margin: 0; color: #334155;"><strong>Reference:</strong> {{reference}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0; color: #334155; line-height: 1.6;">Your membership dues are now paid through <strong>{{due_date}}</strong>.</p>
|
||||
<p style="margin: 0 0 20px 0; color: #334155; line-height: 1.6;">Thank you for your continued support of Monaco USA!</p>
|
||||
<p style="margin: 0; color: #334155;">Best regards,<br><strong style="color: #CE1126;">The Monaco USA Team</strong></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 24px;">
|
||||
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.5);">© 2026 Monaco USA. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>',
|
||||
'Payment of ${{amount}} received on {{payment_date}}. Your dues are paid through {{due_date}}. Thank you!',
|
||||
true
|
||||
),
|
||||
(
|
||||
'dues_reminder',
|
||||
'Dues Reminder',
|
||||
'dues',
|
||||
'Monaco USA Membership Dues Reminder',
|
||||
'<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f172a;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background: linear-gradient(135deg, rgba(15, 23, 42, 0.9) 0%, rgba(30, 41, 59, 0.85) 50%, rgba(127, 29, 29, 0.8) 100%); background-color: #0f172a;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 20px;">
|
||||
<!-- Logo Section -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<div style="background: rgba(255, 255, 255, 0.95); border-radius: 16px; padding: 12px; display: inline-block; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);">
|
||||
<img src="{{logo_url}}" alt="Monaco USA" width="80" height="80" style="display: block; border-radius: 8px;">
|
||||
</div>
|
||||
<h1 style="margin: 16px 0 4px 0; font-size: 24px; font-weight: bold; color: #ffffff; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);">Monaco <span style="color: #fca5a5;">USA</span></h1>
|
||||
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.8);">Americans in Monaco</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" style="max-width: 480px; width: 100%; background: #ffffff; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
|
||||
<tr>
|
||||
<td style="padding: 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; color: #CE1126; font-size: 24px;">Membership Dues Reminder</h2>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 20px 0; color: #334155; line-height: 1.6;">This is a friendly reminder that your Monaco USA membership dues {{status}}.</p>
|
||||
<div style="background: #fef3c7; border: 1px solid #fcd34d; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #92400e; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Dues Details</p>
|
||||
<p style="margin: 0 0 8px 0; color: #334155;"><strong>Amount Due:</strong> ${{amount}}</p>
|
||||
<p style="margin: 0; color: #334155;"><strong>Due Date:</strong> {{due_date}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0; color: #334155; line-height: 1.6;">Please log in to your member portal to view payment instructions or contact the treasurer for assistance.</p>
|
||||
<p style="margin: 0 0 20px 0; color: #334155; line-height: 1.6;">Thank you for your continued membership!</p>
|
||||
<p style="margin: 0; color: #334155;">Best regards,<br><strong style="color: #CE1126;">The Monaco USA Team</strong></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 24px;">
|
||||
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.5);">© 2026 Monaco USA. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>',
|
||||
'Reminder: Your Monaco USA membership dues ({{amount}}) {{status}}. Due date: {{due_date}}. Please log in to your portal for payment instructions.',
|
||||
true
|
||||
)
|
||||
ON CONFLICT (template_key) DO NOTHING;
|
||||
|
||||
-- Grant permissions
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON audit_logs TO authenticated;
|
||||
GRANT ALL ON audit_logs TO service_role;
|
||||
102
supabase/migrations/004_user_notification_preferences.sql
Normal file
102
supabase/migrations/004_user_notification_preferences.sql
Normal file
@@ -0,0 +1,102 @@
|
||||
-- User Notification Preferences
|
||||
-- Allows members to control what email notifications they receive
|
||||
|
||||
-- Create user notification preferences table
|
||||
CREATE TABLE IF NOT EXISTS user_notification_preferences (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
member_id UUID NOT NULL REFERENCES members(id) ON DELETE CASCADE UNIQUE,
|
||||
|
||||
-- Event notifications
|
||||
email_event_rsvp_confirmation BOOLEAN DEFAULT true,
|
||||
email_event_reminder BOOLEAN DEFAULT true,
|
||||
email_event_updates BOOLEAN DEFAULT true,
|
||||
email_waitlist_promotion BOOLEAN DEFAULT true,
|
||||
|
||||
-- Membership notifications
|
||||
email_dues_reminder BOOLEAN DEFAULT true,
|
||||
email_payment_confirmation BOOLEAN DEFAULT true,
|
||||
email_membership_updates BOOLEAN DEFAULT true,
|
||||
|
||||
-- General notifications
|
||||
email_announcements BOOLEAN DEFAULT true,
|
||||
email_newsletter BOOLEAN DEFAULT true,
|
||||
|
||||
-- Newsletter frequency (if subscribed)
|
||||
newsletter_frequency TEXT DEFAULT 'monthly' CHECK (newsletter_frequency IN ('weekly', 'monthly', 'quarterly', 'never')),
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_prefs_member ON user_notification_preferences(member_id);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE user_notification_preferences ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- RLS Policies
|
||||
-- Members can view their own preferences
|
||||
CREATE POLICY "Members can view own notification preferences"
|
||||
ON user_notification_preferences FOR SELECT
|
||||
USING (member_id = auth.uid());
|
||||
|
||||
-- Members can insert their own preferences
|
||||
CREATE POLICY "Members can insert own notification preferences"
|
||||
ON user_notification_preferences FOR INSERT
|
||||
WITH CHECK (member_id = auth.uid());
|
||||
|
||||
-- Members can update their own preferences
|
||||
CREATE POLICY "Members can update own notification preferences"
|
||||
ON user_notification_preferences FOR UPDATE
|
||||
USING (member_id = auth.uid())
|
||||
WITH CHECK (member_id = auth.uid());
|
||||
|
||||
-- Admins can view all preferences (for admin reports)
|
||||
CREATE POLICY "Admins can view all notification preferences"
|
||||
ON user_notification_preferences FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM members
|
||||
WHERE members.id = auth.uid()
|
||||
AND members.role = 'admin'
|
||||
)
|
||||
);
|
||||
|
||||
-- Function to create default preferences for new members
|
||||
CREATE OR REPLACE FUNCTION create_default_notification_preferences()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
INSERT INTO user_notification_preferences (member_id)
|
||||
VALUES (NEW.id)
|
||||
ON CONFLICT (member_id) DO NOTHING;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Trigger to create preferences when a new member is created
|
||||
DROP TRIGGER IF EXISTS on_member_created_create_notification_prefs ON members;
|
||||
CREATE TRIGGER on_member_created_create_notification_prefs
|
||||
AFTER INSERT ON members
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION create_default_notification_preferences();
|
||||
|
||||
-- Create default preferences for existing members
|
||||
INSERT INTO user_notification_preferences (member_id)
|
||||
SELECT id FROM members
|
||||
ON CONFLICT (member_id) DO NOTHING;
|
||||
|
||||
-- Add updated_at trigger
|
||||
CREATE OR REPLACE FUNCTION update_notification_prefs_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS set_notification_prefs_updated_at ON user_notification_preferences;
|
||||
CREATE TRIGGER set_notification_prefs_updated_at
|
||||
BEFORE UPDATE ON user_notification_preferences
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_notification_prefs_updated_at();
|
||||
37
supabase/migrations/005_fix_avatars_storage_policy.sql
Normal file
37
supabase/migrations/005_fix_avatars_storage_policy.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- Monaco USA Portal 2026
|
||||
-- Migration 005: Fix Avatars Storage Policy
|
||||
-- ================================================
|
||||
-- This fixes the RLS policy for avatars bucket to allow authenticated users to upload
|
||||
|
||||
-- Drop existing restrictive policies
|
||||
DROP POLICY IF EXISTS "avatars_insert_policy" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "avatars_update_policy" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "avatars_delete_policy" ON storage.objects;
|
||||
|
||||
-- Create new permissive policy for authenticated users
|
||||
-- Avatars bucket is public for reading, so we just need to ensure authenticated users can upload
|
||||
CREATE POLICY "avatars_insert_policy" ON storage.objects FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (bucket_id = 'avatars');
|
||||
|
||||
CREATE POLICY "avatars_update_policy" ON storage.objects FOR UPDATE
|
||||
TO authenticated
|
||||
USING (bucket_id = 'avatars');
|
||||
|
||||
CREATE POLICY "avatars_delete_policy" ON storage.objects FOR DELETE
|
||||
TO authenticated
|
||||
USING (bucket_id = 'avatars');
|
||||
|
||||
-- Ensure the avatars bucket exists and is public
|
||||
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||
VALUES (
|
||||
'avatars',
|
||||
'avatars',
|
||||
true,
|
||||
5242880, -- 5MB
|
||||
ARRAY['image/jpeg', 'image/png', 'image/webp', 'image/gif']
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
public = true,
|
||||
file_size_limit = EXCLUDED.file_size_limit,
|
||||
allowed_mime_types = EXCLUDED.allowed_mime_types;
|
||||
100
supabase/migrations/006_document_folders.sql
Normal file
100
supabase/migrations/006_document_folders.sql
Normal file
@@ -0,0 +1,100 @@
|
||||
-- Monaco USA Portal 2026 - Document Folders
|
||||
-- Adds hierarchical folder support for document organization
|
||||
|
||||
-- ============================================
|
||||
-- DOCUMENT FOLDERS TABLE
|
||||
-- ============================================
|
||||
CREATE TABLE public.document_folders (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
parent_id UUID REFERENCES public.document_folders(id) ON DELETE CASCADE,
|
||||
path TEXT, -- Full path for breadcrumb support
|
||||
visibility TEXT NOT NULL DEFAULT 'members'
|
||||
CHECK (visibility IN ('public', 'members', 'board', 'admin')),
|
||||
created_by UUID REFERENCES public.members(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
-- Ensure unique folder names within same parent
|
||||
UNIQUE(name, parent_id)
|
||||
);
|
||||
|
||||
-- Add updated_at trigger
|
||||
CREATE TRIGGER document_folders_updated_at
|
||||
BEFORE UPDATE ON public.document_folders
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at();
|
||||
|
||||
-- ============================================
|
||||
-- ADD FOLDER_ID TO DOCUMENTS TABLE
|
||||
-- ============================================
|
||||
ALTER TABLE public.documents
|
||||
ADD COLUMN folder_id UUID REFERENCES public.document_folders(id) ON DELETE SET NULL;
|
||||
|
||||
-- ============================================
|
||||
-- PATH UPDATE TRIGGER
|
||||
-- ============================================
|
||||
CREATE OR REPLACE FUNCTION update_folder_path()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
parent_path TEXT;
|
||||
BEGIN
|
||||
IF NEW.parent_id IS NULL THEN
|
||||
NEW.path = NEW.name;
|
||||
ELSE
|
||||
SELECT path INTO parent_path
|
||||
FROM public.document_folders
|
||||
WHERE id = NEW.parent_id;
|
||||
|
||||
NEW.path = parent_path || '/' || NEW.name;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER folder_path_trigger
|
||||
BEFORE INSERT OR UPDATE ON public.document_folders
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_folder_path();
|
||||
|
||||
-- ============================================
|
||||
-- RLS POLICIES FOR FOLDERS
|
||||
-- ============================================
|
||||
ALTER TABLE public.document_folders ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Everyone can view folders based on visibility
|
||||
CREATE POLICY "Folders visible based on visibility" ON public.document_folders
|
||||
FOR SELECT USING (
|
||||
visibility = 'public' OR
|
||||
(visibility = 'members' AND auth.uid() IS NOT NULL) OR
|
||||
(visibility = 'board' AND EXISTS (
|
||||
SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin')
|
||||
)) OR
|
||||
(visibility = 'admin' AND EXISTS (
|
||||
SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin'
|
||||
))
|
||||
);
|
||||
|
||||
-- Board and admin can create folders
|
||||
CREATE POLICY "Board/admin can create folders" ON public.document_folders
|
||||
FOR INSERT WITH CHECK (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin'))
|
||||
);
|
||||
|
||||
-- Board and admin can update folders
|
||||
CREATE POLICY "Board/admin can update folders" ON public.document_folders
|
||||
FOR UPDATE USING (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin'))
|
||||
);
|
||||
|
||||
-- Only admin can delete folders
|
||||
CREATE POLICY "Admin can delete folders" ON public.document_folders
|
||||
FOR DELETE USING (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role = 'admin')
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- INDEX FOR FOLDER QUERIES
|
||||
-- ============================================
|
||||
CREATE INDEX idx_document_folders_parent ON public.document_folders(parent_id);
|
||||
CREATE INDEX idx_documents_folder ON public.documents(folder_id);
|
||||
307
supabase/migrations/007_dues_reminders.sql
Normal file
307
supabase/migrations/007_dues_reminders.sql
Normal file
@@ -0,0 +1,307 @@
|
||||
-- Monaco USA Portal 2026 - Dues Reminders Enhancement
|
||||
-- Track sent reminders to avoid duplicates and enable analytics
|
||||
|
||||
-- ============================================
|
||||
-- DUES REMINDER LOGS TABLE
|
||||
-- ============================================
|
||||
CREATE TABLE public.dues_reminder_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
member_id UUID NOT NULL REFERENCES public.members(id) ON DELETE CASCADE,
|
||||
reminder_type TEXT NOT NULL CHECK (reminder_type IN ('due_soon_30', 'due_soon_7', 'due_soon_1', 'overdue', 'grace_period', 'inactive_notice')),
|
||||
due_date DATE NOT NULL,
|
||||
sent_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
email_log_id UUID REFERENCES public.email_logs(id),
|
||||
-- Prevent duplicate reminders for same member/type/period
|
||||
UNIQUE(member_id, reminder_type, due_date)
|
||||
);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE public.dues_reminder_logs ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Board and admin can view reminder logs
|
||||
CREATE POLICY "Board/admin can view reminder logs" ON public.dues_reminder_logs
|
||||
FOR SELECT USING (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin'))
|
||||
);
|
||||
|
||||
-- Only service role can insert reminder logs (from cron/server)
|
||||
CREATE POLICY "Service role can manage reminder logs" ON public.dues_reminder_logs
|
||||
FOR ALL USING (true)
|
||||
WITH CHECK (true);
|
||||
|
||||
-- Index for fast lookups
|
||||
CREATE INDEX idx_reminder_logs_member_date ON public.dues_reminder_logs(member_id, due_date);
|
||||
CREATE INDEX idx_reminder_logs_type_sent ON public.dues_reminder_logs(reminder_type, sent_at);
|
||||
|
||||
-- ============================================
|
||||
-- ADD EMAIL TEMPLATES FOR DUES REMINDERS
|
||||
-- ============================================
|
||||
|
||||
-- 30 days before due reminder
|
||||
INSERT INTO public.email_templates (template_key, template_name, category, subject, body_html, body_text, is_active, is_system, variables_schema) VALUES
|
||||
(
|
||||
'dues_reminder_30',
|
||||
'Dues Reminder - 30 Days',
|
||||
'payment',
|
||||
'Your Monaco USA Membership Dues Are Coming Up',
|
||||
'<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">This is a friendly reminder that your Monaco USA membership dues will be due on <strong>{{due_date}}</strong>.</p>
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #334155; font-size: 14px; font-weight: 600;">Payment Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Amount Due:</strong> {{amount}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Due Date:</strong> {{due_date}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Member ID:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<div style="background: #fef3c7; border: 1px solid #fcd34d; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #92400e; font-size: 14px; font-weight: 600;">Bank Transfer Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Account Holder:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Bank:</strong> {{bank_name}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">You can also view your payment status and history in the member portal:</p>
|
||||
<p style="margin: 0 0 20px 0;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">View My Account</a>
|
||||
</p>
|
||||
<p style="margin: 0; color: #64748b; font-size: 14px;">Thank you for being a valued member of Monaco USA!</p>',
|
||||
'Dear {{first_name}},
|
||||
|
||||
This is a friendly reminder that your Monaco USA membership dues will be due on {{due_date}}.
|
||||
|
||||
Payment Details:
|
||||
- Amount Due: {{amount}}
|
||||
- Due Date: {{due_date}}
|
||||
- Member ID: {{member_id}}
|
||||
|
||||
Bank Transfer Details:
|
||||
- Account Holder: {{account_holder}}
|
||||
- Bank: {{bank_name}}
|
||||
- IBAN: {{iban}}
|
||||
- Reference: {{member_id}}
|
||||
|
||||
Visit the member portal to view your payment status: {{portal_url}}
|
||||
|
||||
Thank you for being a valued member of Monaco USA!',
|
||||
true,
|
||||
true,
|
||||
'{"first_name": "Member first name", "due_date": "Dues due date", "amount": "Amount due", "member_id": "Member ID for reference", "account_holder": "Bank account holder", "bank_name": "Bank name", "iban": "IBAN number", "portal_url": "Portal URL"}'
|
||||
),
|
||||
(
|
||||
'dues_reminder_7',
|
||||
'Dues Reminder - 7 Days',
|
||||
'payment',
|
||||
'Reminder: Monaco USA Dues Due in 7 Days',
|
||||
'<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Your Monaco USA membership dues will be due in <strong>7 days</strong> on {{due_date}}.</p>
|
||||
<div style="background: #fef3c7; border: 1px solid #fcd34d; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #92400e; font-size: 14px; font-weight: 600;">Payment Information:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Amount:</strong> {{amount}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Due Date:</strong> {{due_date}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Pay Now</a>
|
||||
</p>
|
||||
<p style="margin: 0; color: #64748b; font-size: 14px;">Questions? Contact us at contact@monacousa.org</p>',
|
||||
'Dear {{first_name}},
|
||||
|
||||
Your Monaco USA membership dues will be due in 7 days on {{due_date}}.
|
||||
|
||||
Amount: {{amount}}
|
||||
IBAN: {{iban}}
|
||||
Reference: {{member_id}}
|
||||
|
||||
Visit the portal to pay: {{portal_url}}',
|
||||
true,
|
||||
true,
|
||||
'{"first_name": "Member first name", "due_date": "Dues due date", "amount": "Amount due", "member_id": "Member ID", "iban": "IBAN number", "portal_url": "Portal URL"}'
|
||||
),
|
||||
(
|
||||
'dues_reminder_1',
|
||||
'Dues Reminder - 1 Day',
|
||||
'payment',
|
||||
'URGENT: Monaco USA Dues Due Tomorrow',
|
||||
'<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;"><strong style="color: #dc2626;">Your Monaco USA membership dues are due tomorrow ({{due_date}}).</strong></p>
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #991b1b; font-size: 14px; font-weight: 600;">Payment Required:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Amount:</strong> {{amount}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">To maintain your active membership status and continued access to member benefits, please ensure payment is made by the due date.</p>
|
||||
<p style="margin: 0 0 20px 0;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #dc2626; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Pay Now</a>
|
||||
</p>',
|
||||
'Dear {{first_name}},
|
||||
|
||||
URGENT: Your Monaco USA membership dues are due tomorrow ({{due_date}}).
|
||||
|
||||
Amount: {{amount}}
|
||||
IBAN: {{iban}}
|
||||
Reference: {{member_id}}
|
||||
|
||||
To maintain your active membership, please pay by the due date.
|
||||
|
||||
Pay now: {{portal_url}}',
|
||||
true,
|
||||
true,
|
||||
'{"first_name": "Member first name", "due_date": "Dues due date", "amount": "Amount due", "member_id": "Member ID", "iban": "IBAN number", "portal_url": "Portal URL"}'
|
||||
),
|
||||
(
|
||||
'dues_overdue',
|
||||
'Dues Overdue Notice',
|
||||
'payment',
|
||||
'ACTION REQUIRED: Monaco USA Dues Are Now Overdue',
|
||||
'<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Your Monaco USA membership dues are now <strong style="color: #dc2626;">{{days_overdue}} days overdue</strong>.</p>
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #991b1b; font-size: 14px; font-weight: 600;">Overdue Payment:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Amount:</strong> {{amount}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Original Due Date:</strong> {{due_date}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Days Overdue:</strong> {{days_overdue}}</p>
|
||||
</div>
|
||||
<div style="background: #fffbeb; border: 1px solid #fcd34d; border-radius: 12px; padding: 16px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0; color: #92400e; font-size: 14px;"><strong>Grace Period:</strong> You have {{grace_days_remaining}} days remaining in your grace period. After this, your membership status will be changed to inactive.</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Please remit payment as soon as possible to maintain your membership benefits.</p>
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #334155; font-size: 14px; font-weight: 600;">Payment Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Account:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #dc2626; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Pay Now</a>
|
||||
</p>',
|
||||
'Dear {{first_name}},
|
||||
|
||||
Your Monaco USA membership dues are now {{days_overdue}} days overdue.
|
||||
|
||||
Amount: {{amount}}
|
||||
Original Due Date: {{due_date}}
|
||||
Days Overdue: {{days_overdue}}
|
||||
|
||||
Grace Period: You have {{grace_days_remaining}} days remaining. After this, your membership will be marked inactive.
|
||||
|
||||
Payment Details:
|
||||
- Account: {{account_holder}}
|
||||
- IBAN: {{iban}}
|
||||
- Reference: {{member_id}}
|
||||
|
||||
Pay now: {{portal_url}}',
|
||||
true,
|
||||
true,
|
||||
'{"first_name": "Member first name", "due_date": "Original due date", "amount": "Amount due", "days_overdue": "Number of days overdue", "grace_days_remaining": "Days left in grace period", "member_id": "Member ID", "account_holder": "Account holder", "iban": "IBAN", "portal_url": "Portal URL"}'
|
||||
),
|
||||
(
|
||||
'dues_grace_warning',
|
||||
'Grace Period Ending Warning',
|
||||
'payment',
|
||||
'WARNING: Monaco USA Grace Period Ending Soon',
|
||||
'<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;"><strong style="color: #dc2626;">Your grace period ends in {{grace_days_remaining}} days.</strong></p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Your membership dues of <strong>{{amount}}</strong> were due on {{due_date}} and are now {{days_overdue}} days overdue.</p>
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0; color: #991b1b; font-size: 14px;"><strong>If payment is not received by {{grace_end_date}}, your membership status will automatically change to INACTIVE and you will lose access to member benefits.</strong></p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Please make your payment immediately to avoid interruption:</p>
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #dc2626; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Pay Now - Urgent</a>
|
||||
</p>',
|
||||
'Dear {{first_name}},
|
||||
|
||||
WARNING: Your grace period ends in {{grace_days_remaining}} days.
|
||||
|
||||
Your dues of {{amount}} were due on {{due_date}} and are now {{days_overdue}} days overdue.
|
||||
|
||||
If payment is not received by {{grace_end_date}}, your membership will become INACTIVE.
|
||||
|
||||
IBAN: {{iban}}
|
||||
Reference: {{member_id}}
|
||||
|
||||
Pay now: {{portal_url}}',
|
||||
true,
|
||||
true,
|
||||
'{"first_name": "Member first name", "due_date": "Original due date", "amount": "Amount due", "days_overdue": "Days overdue", "grace_days_remaining": "Days until grace period ends", "grace_end_date": "Date grace period ends", "member_id": "Member ID", "iban": "IBAN", "portal_url": "Portal URL"}'
|
||||
),
|
||||
(
|
||||
'dues_inactive_notice',
|
||||
'Membership Marked Inactive',
|
||||
'payment',
|
||||
'Notice: Your Monaco USA Membership Is Now Inactive',
|
||||
'<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Due to non-payment of membership dues, your Monaco USA membership has been marked as <strong style="color: #dc2626;">INACTIVE</strong>.</p>
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #991b1b; font-size: 14px; font-weight: 600;">Status Change:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Previous Status:</strong> Active</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>New Status:</strong> Inactive</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Outstanding Amount:</strong> {{amount}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">As an inactive member, you will no longer have access to:</p>
|
||||
<ul style="margin: 0 0 16px 0; padding-left: 20px; color: #334155;">
|
||||
<li>Member-only events</li>
|
||||
<li>Member directory</li>
|
||||
<li>Member communications</li>
|
||||
<li>Voting rights</li>
|
||||
</ul>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">To reactivate your membership, please pay your outstanding dues:</p>
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Account:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Reactivate My Membership</a>
|
||||
</p>
|
||||
<p style="margin: 0; color: #64748b; font-size: 14px;">If you believe this is an error or have questions, please contact us at contact@monacousa.org</p>',
|
||||
'Dear {{first_name}},
|
||||
|
||||
Due to non-payment of membership dues, your Monaco USA membership has been marked as INACTIVE.
|
||||
|
||||
Outstanding Amount: {{amount}}
|
||||
|
||||
As an inactive member, you no longer have access to member-only events, directory, communications, or voting rights.
|
||||
|
||||
To reactivate, please pay your dues:
|
||||
- Account: {{account_holder}}
|
||||
- IBAN: {{iban}}
|
||||
- Reference: {{member_id}}
|
||||
|
||||
Reactivate: {{portal_url}}
|
||||
|
||||
Questions? Contact contact@monacousa.org',
|
||||
true,
|
||||
true,
|
||||
'{"first_name": "Member first name", "amount": "Outstanding amount", "member_id": "Member ID", "account_holder": "Account holder", "iban": "IBAN", "portal_url": "Portal URL"}'
|
||||
)
|
||||
ON CONFLICT (template_key) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- HELPER FUNCTION: Get dues settings
|
||||
-- ============================================
|
||||
CREATE OR REPLACE FUNCTION get_dues_settings()
|
||||
RETURNS TABLE (
|
||||
reminder_days_before INTEGER[],
|
||||
grace_period_days INTEGER,
|
||||
auto_inactive_enabled BOOLEAN,
|
||||
payment_iban TEXT,
|
||||
payment_account_holder TEXT,
|
||||
payment_bank_name TEXT
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
COALESCE((SELECT (setting_value)::INTEGER[] FROM app_settings WHERE category = 'dues' AND setting_key = 'reminder_days_before'), ARRAY[30, 7, 1])::INTEGER[],
|
||||
COALESCE((SELECT (setting_value)::INTEGER FROM app_settings WHERE category = 'dues' AND setting_key = 'grace_period_days'), 30)::INTEGER,
|
||||
COALESCE((SELECT (setting_value)::BOOLEAN FROM app_settings WHERE category = 'dues' AND setting_key = 'auto_inactive_enabled'), true)::BOOLEAN,
|
||||
COALESCE((SELECT setting_value::TEXT FROM app_settings WHERE category = 'dues' AND setting_key = 'payment_iban'), '')::TEXT,
|
||||
COALESCE((SELECT setting_value::TEXT FROM app_settings WHERE category = 'dues' AND setting_key = 'payment_account_holder'), '')::TEXT,
|
||||
COALESCE((SELECT setting_value::TEXT FROM app_settings WHERE category = 'dues' AND setting_key = 'payment_bank_name'), '')::TEXT;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
11
supabase/migrations/008_s3_public_endpoint.sql
Normal file
11
supabase/migrations/008_s3_public_endpoint.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- ============================================
|
||||
-- S3 PUBLIC ENDPOINT SETTING
|
||||
-- Separate URL for browser-accessible S3 files
|
||||
-- ============================================
|
||||
|
||||
-- Add S3 public endpoint setting for browser access
|
||||
-- The regular s3_endpoint is for server-to-S3 communication (internal Docker)
|
||||
-- The s3_public_endpoint is for browser access to files (external URL)
|
||||
INSERT INTO public.app_settings (category, setting_key, setting_value, setting_type, display_name, description, is_public) VALUES
|
||||
('storage', 's3_public_endpoint', '""', 'text', 'Public Endpoint URL', 'Browser-accessible S3 URL (e.g., http://localhost:9000). Leave empty to use the same as S3 Endpoint.', false)
|
||||
ON CONFLICT (category, setting_key) DO NOTHING;
|
||||
22
supabase/migrations/009_dual_avatar_urls.sql
Normal file
22
supabase/migrations/009_dual_avatar_urls.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- ============================================
|
||||
-- DUAL AVATAR URL COLUMNS
|
||||
-- Separate columns for S3 and local storage URLs
|
||||
-- ============================================
|
||||
|
||||
-- Add separate columns for S3 and local (Supabase Storage) avatar URLs
|
||||
-- This allows switching between storage backends without losing URLs
|
||||
|
||||
-- Add local avatar URL column
|
||||
ALTER TABLE public.members ADD COLUMN IF NOT EXISTS avatar_url_local TEXT;
|
||||
|
||||
-- Add S3 avatar URL column
|
||||
ALTER TABLE public.members ADD COLUMN IF NOT EXISTS avatar_url_s3 TEXT;
|
||||
|
||||
-- Add avatar storage path column (for deletion purposes)
|
||||
ALTER TABLE public.members ADD COLUMN IF NOT EXISTS avatar_path TEXT;
|
||||
|
||||
-- Comment explaining the columns
|
||||
COMMENT ON COLUMN public.members.avatar_url IS 'Current active avatar URL (computed based on storage setting)';
|
||||
COMMENT ON COLUMN public.members.avatar_url_local IS 'Avatar URL when stored in Supabase Storage';
|
||||
COMMENT ON COLUMN public.members.avatar_url_s3 IS 'Avatar URL when stored in S3/MinIO';
|
||||
COMMENT ON COLUMN public.members.avatar_path IS 'Storage path for avatar file (e.g., member_id/avatar.jpg)';
|
||||
79
supabase/migrations/010_storage_service_role_policies.sql
Normal file
79
supabase/migrations/010_storage_service_role_policies.sql
Normal file
@@ -0,0 +1,79 @@
|
||||
-- ============================================
|
||||
-- STORAGE SERVICE ROLE POLICIES
|
||||
-- Allow service_role to perform all operations on avatars bucket
|
||||
-- This fixes RLS issues when using supabaseAdmin for storage operations
|
||||
-- ============================================
|
||||
|
||||
-- First, drop any existing service role policies (in case they exist with different names)
|
||||
DROP POLICY IF EXISTS "Service role can insert avatars" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "Service role can update avatars" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "Service role can delete avatars" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "Service role can read avatars" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "service_role_insert_avatars" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "service_role_update_avatars" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "service_role_delete_avatars" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "service_role_select_avatars" ON storage.objects;
|
||||
|
||||
-- Service role INSERT policy for avatars
|
||||
CREATE POLICY "service_role_insert_avatars" ON storage.objects
|
||||
FOR INSERT TO service_role
|
||||
WITH CHECK (bucket_id = 'avatars');
|
||||
|
||||
-- Service role UPDATE policy for avatars
|
||||
CREATE POLICY "service_role_update_avatars" ON storage.objects
|
||||
FOR UPDATE TO service_role
|
||||
USING (bucket_id = 'avatars');
|
||||
|
||||
-- Service role DELETE policy for avatars
|
||||
CREATE POLICY "service_role_delete_avatars" ON storage.objects
|
||||
FOR DELETE TO service_role
|
||||
USING (bucket_id = 'avatars');
|
||||
|
||||
-- Service role SELECT policy for avatars
|
||||
CREATE POLICY "service_role_select_avatars" ON storage.objects
|
||||
FOR SELECT TO service_role
|
||||
USING (bucket_id = 'avatars');
|
||||
|
||||
-- Also add service_role policies for documents bucket
|
||||
DROP POLICY IF EXISTS "service_role_insert_documents" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "service_role_update_documents" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "service_role_delete_documents" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "service_role_select_documents" ON storage.objects;
|
||||
|
||||
CREATE POLICY "service_role_insert_documents" ON storage.objects
|
||||
FOR INSERT TO service_role
|
||||
WITH CHECK (bucket_id = 'documents');
|
||||
|
||||
CREATE POLICY "service_role_update_documents" ON storage.objects
|
||||
FOR UPDATE TO service_role
|
||||
USING (bucket_id = 'documents');
|
||||
|
||||
CREATE POLICY "service_role_delete_documents" ON storage.objects
|
||||
FOR DELETE TO service_role
|
||||
USING (bucket_id = 'documents');
|
||||
|
||||
CREATE POLICY "service_role_select_documents" ON storage.objects
|
||||
FOR SELECT TO service_role
|
||||
USING (bucket_id = 'documents');
|
||||
|
||||
-- Also add service_role policies for event-images bucket
|
||||
DROP POLICY IF EXISTS "service_role_insert_event_images" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "service_role_update_event_images" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "service_role_delete_event_images" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "service_role_select_event_images" ON storage.objects;
|
||||
|
||||
CREATE POLICY "service_role_insert_event_images" ON storage.objects
|
||||
FOR INSERT TO service_role
|
||||
WITH CHECK (bucket_id = 'event-images');
|
||||
|
||||
CREATE POLICY "service_role_update_event_images" ON storage.objects
|
||||
FOR UPDATE TO service_role
|
||||
USING (bucket_id = 'event-images');
|
||||
|
||||
CREATE POLICY "service_role_delete_event_images" ON storage.objects
|
||||
FOR DELETE TO service_role
|
||||
USING (bucket_id = 'event-images');
|
||||
|
||||
CREATE POLICY "service_role_select_event_images" ON storage.objects
|
||||
FOR SELECT TO service_role
|
||||
USING (bucket_id = 'event-images');
|
||||
98
supabase/migrations/011_fix_service_role_rls.sql
Normal file
98
supabase/migrations/011_fix_service_role_rls.sql
Normal file
@@ -0,0 +1,98 @@
|
||||
-- ============================================
|
||||
-- FIX SERVICE ROLE RLS BYPASS
|
||||
-- Ensure service_role can properly bypass RLS for storage operations
|
||||
-- ============================================
|
||||
|
||||
-- The service_role should have BYPASSRLS attribute in Supabase
|
||||
-- But in self-hosted setups, this might not be configured correctly
|
||||
-- This migration ensures proper access through multiple approaches
|
||||
|
||||
-- Approach 1: Grant service_role BYPASSRLS (if not already set)
|
||||
-- Note: This requires superuser privileges, which the migration might not have
|
||||
-- If this fails, the explicit policies below will still work
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Check if service_role exists and doesn't have bypassrls
|
||||
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role' AND NOT rolbypassrls) THEN
|
||||
ALTER ROLE service_role BYPASSRLS;
|
||||
RAISE NOTICE 'Granted BYPASSRLS to service_role';
|
||||
ELSE
|
||||
RAISE NOTICE 'service_role already has BYPASSRLS or does not exist';
|
||||
END IF;
|
||||
EXCEPTION
|
||||
WHEN insufficient_privilege THEN
|
||||
RAISE NOTICE 'Could not grant BYPASSRLS (insufficient privileges) - using explicit policies instead';
|
||||
WHEN OTHERS THEN
|
||||
RAISE NOTICE 'Error granting BYPASSRLS: % - using explicit policies instead', SQLERRM;
|
||||
END $$;
|
||||
|
||||
-- Approach 2: Ensure RLS is properly configured on storage.objects
|
||||
-- Check if RLS is enabled and ensure our policies exist
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Ensure RLS is enabled on storage.objects (it should be by default)
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_tables
|
||||
WHERE schemaname = 'storage' AND tablename = 'objects' AND rowsecurity = true
|
||||
) THEN
|
||||
ALTER TABLE storage.objects ENABLE ROW LEVEL SECURITY;
|
||||
RAISE NOTICE 'Enabled RLS on storage.objects';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Approach 3: Create permissive policies for service_role on ALL storage buckets
|
||||
-- These use a single policy per operation type that covers all buckets
|
||||
|
||||
-- First, clean up any existing service_role policies
|
||||
DROP POLICY IF EXISTS "service_role_all_select" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "service_role_all_insert" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "service_role_all_update" ON storage.objects;
|
||||
DROP POLICY IF EXISTS "service_role_all_delete" ON storage.objects;
|
||||
|
||||
-- Create universal service_role policies (allow access to ALL buckets)
|
||||
CREATE POLICY "service_role_all_select" ON storage.objects
|
||||
FOR SELECT TO service_role
|
||||
USING (true);
|
||||
|
||||
CREATE POLICY "service_role_all_insert" ON storage.objects
|
||||
FOR INSERT TO service_role
|
||||
WITH CHECK (true);
|
||||
|
||||
CREATE POLICY "service_role_all_update" ON storage.objects
|
||||
FOR UPDATE TO service_role
|
||||
USING (true);
|
||||
|
||||
CREATE POLICY "service_role_all_delete" ON storage.objects
|
||||
FOR DELETE TO service_role
|
||||
USING (true);
|
||||
|
||||
-- Approach 4: Grant necessary table permissions to service_role
|
||||
GRANT ALL ON storage.objects TO service_role;
|
||||
GRANT ALL ON storage.buckets TO service_role;
|
||||
GRANT USAGE ON SCHEMA storage TO service_role;
|
||||
|
||||
-- Also ensure service_role can use sequences in storage schema
|
||||
DO $$
|
||||
DECLARE
|
||||
seq_name text;
|
||||
BEGIN
|
||||
FOR seq_name IN
|
||||
SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'storage'
|
||||
LOOP
|
||||
EXECUTE format('GRANT USAGE, SELECT ON SEQUENCE storage.%I TO service_role', seq_name);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- Verify the setup
|
||||
DO $$
|
||||
DECLARE
|
||||
policy_count int;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO policy_count
|
||||
FROM pg_policies
|
||||
WHERE schemaname = 'storage'
|
||||
AND tablename = 'objects'
|
||||
AND roles @> ARRAY['service_role']::name[];
|
||||
|
||||
RAISE NOTICE 'service_role has % policies on storage.objects', policy_count;
|
||||
END $$;
|
||||
49
supabase/migrations/012_dual_document_urls.sql
Normal file
49
supabase/migrations/012_dual_document_urls.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
-- ============================================
|
||||
-- DUAL STORAGE SUPPORT FOR DOCUMENTS
|
||||
-- Store URLs for both Supabase Storage and S3 backends
|
||||
-- Mirrors the avatar dual-storage pattern from migration 009
|
||||
-- ============================================
|
||||
|
||||
-- Add local storage URL column
|
||||
ALTER TABLE public.documents ADD COLUMN IF NOT EXISTS file_url_local TEXT;
|
||||
|
||||
-- Add S3 storage URL column
|
||||
ALTER TABLE public.documents ADD COLUMN IF NOT EXISTS file_url_s3 TEXT;
|
||||
|
||||
-- Add storage path column (relative path used for both backends)
|
||||
ALTER TABLE public.documents ADD COLUMN IF NOT EXISTS storage_path TEXT;
|
||||
|
||||
-- Add comments for documentation
|
||||
COMMENT ON COLUMN public.documents.file_path IS 'Current active file URL (computed based on storage setting) - kept for backwards compatibility';
|
||||
COMMENT ON COLUMN public.documents.file_url_local IS 'File URL when stored in Supabase Storage';
|
||||
COMMENT ON COLUMN public.documents.file_url_s3 IS 'File URL when stored in S3/MinIO';
|
||||
COMMENT ON COLUMN public.documents.storage_path IS 'Storage path for file (e.g., timestamp-random-filename.pdf)';
|
||||
|
||||
-- Migrate existing file_path values to storage_path and file_url_local
|
||||
-- This handles documents uploaded before dual-storage was implemented
|
||||
UPDATE public.documents
|
||||
SET
|
||||
storage_path = CASE
|
||||
WHEN file_path LIKE 'http%' THEN
|
||||
-- Extract filename from URL
|
||||
CASE
|
||||
WHEN file_path LIKE '%/storage/v1/object/public/documents/%' THEN
|
||||
substring(file_path from '/storage/v1/object/public/documents/([^?]+)')
|
||||
WHEN file_path LIKE '%/documents/%' THEN
|
||||
substring(file_path from '/documents/([^?]+)')
|
||||
ELSE file_path
|
||||
END
|
||||
ELSE file_path
|
||||
END,
|
||||
file_url_local = CASE
|
||||
WHEN file_path LIKE 'http%' AND file_path LIKE '%/storage/v1/object/public/documents/%' THEN file_path
|
||||
ELSE NULL
|
||||
END,
|
||||
file_url_s3 = CASE
|
||||
WHEN file_path LIKE 'http%' AND file_path NOT LIKE '%/storage/v1/object/public/documents/%' THEN file_path
|
||||
ELSE NULL
|
||||
END
|
||||
WHERE storage_path IS NULL;
|
||||
|
||||
-- Create index for faster lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_documents_storage_path ON public.documents(storage_path);
|
||||
671
supabase/migrations/013_email_background_images.sql
Normal file
671
supabase/migrations/013_email_background_images.sql
Normal file
@@ -0,0 +1,671 @@
|
||||
-- ============================================
|
||||
-- Migration 013: Update Email Templates with Background Image
|
||||
-- Adds S3-hosted Monaco background image to all email templates
|
||||
-- Matches login screen styling: image + gradient overlay
|
||||
-- ============================================
|
||||
|
||||
-- Background image URL
|
||||
-- Using: https://s3.monacousa.org/public/monaco_high_res.jpg
|
||||
-- Gradient overlay: from-slate-900/80 via-slate-900/60 to-monaco-900/70
|
||||
|
||||
-- =====================
|
||||
-- Update Welcome Email Template
|
||||
-- =====================
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td { font-family: Arial, sans-serif !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f172a;">
|
||||
<!--[if gte mso 9]>
|
||||
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
|
||||
<v:fill type="tile" src="https://s3.monacousa.org/public/monaco_high_res.jpg" color="#0f172a"/>
|
||||
</v:background>
|
||||
<![endif]-->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-image: url(''https://s3.monacousa.org/public/monaco_high_res.jpg''); background-size: cover; background-position: center; background-color: #0f172a;">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="background: linear-gradient(135deg, rgba(15, 23, 42, 0.8) 0%, rgba(15, 23, 42, 0.6) 50%, rgba(127, 29, 29, 0.7) 100%);">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 20px;">
|
||||
<!-- Logo Section -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<div style="background: rgba(255, 255, 255, 0.95); border-radius: 16px; padding: 12px; display: inline-block; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);">
|
||||
<img src="{{logo_url}}" alt="Monaco USA" width="80" height="80" style="display: block; border-radius: 8px;">
|
||||
</div>
|
||||
<h1 style="margin: 16px 0 4px 0; font-size: 24px; font-weight: bold; color: #ffffff; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);">Monaco <span style="color: #fca5a5;">USA</span></h1>
|
||||
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.8);">Americans in Monaco</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" style="max-width: 480px; width: 100%; background: #ffffff; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
|
||||
<tr>
|
||||
<td style="padding: 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; color: #CE1126; font-size: 24px;">Welcome to Monaco USA!</h2>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">We are thrilled to welcome you to the Monaco USA community! Your membership has been created and you can now access all member features.</p>
|
||||
<p style="margin: 0 0 12px 0; color: #334155; font-weight: 600;">To get started:</p>
|
||||
<ol style="margin: 0 0 20px 20px; padding: 0; color: #334155; line-height: 1.8;">
|
||||
<li>Set up your password using the separate email we sent</li>
|
||||
<li>Complete your profile with your details</li>
|
||||
<li>Explore upcoming events and connect with fellow members</li>
|
||||
</ol>
|
||||
<p style="margin: 0 0 20px 0; color: #334155; line-height: 1.6;">If you have any questions, please don''t hesitate to reach out to our board members.</p>
|
||||
<p style="margin: 0; color: #334155;">Best regards,<br><strong style="color: #CE1126;">The Monaco USA Team</strong></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 24px;">
|
||||
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.5);">© 2026 Monaco USA. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>'
|
||||
WHERE template_key = 'welcome';
|
||||
|
||||
-- =====================
|
||||
-- Update RSVP Confirmation Template
|
||||
-- =====================
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td { font-family: Arial, sans-serif !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f172a;">
|
||||
<!--[if gte mso 9]>
|
||||
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
|
||||
<v:fill type="tile" src="https://s3.monacousa.org/public/monaco_high_res.jpg" color="#0f172a"/>
|
||||
</v:background>
|
||||
<![endif]-->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-image: url(''https://s3.monacousa.org/public/monaco_high_res.jpg''); background-size: cover; background-position: center; background-color: #0f172a;">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="background: linear-gradient(135deg, rgba(15, 23, 42, 0.8) 0%, rgba(15, 23, 42, 0.6) 50%, rgba(127, 29, 29, 0.7) 100%);">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 20px;">
|
||||
<!-- Logo Section -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<div style="background: rgba(255, 255, 255, 0.95); border-radius: 16px; padding: 12px; display: inline-block; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);">
|
||||
<img src="{{logo_url}}" alt="Monaco USA" width="80" height="80" style="display: block; border-radius: 8px;">
|
||||
</div>
|
||||
<h1 style="margin: 16px 0 4px 0; font-size: 24px; font-weight: bold; color: #ffffff; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);">Monaco <span style="color: #fca5a5;">USA</span></h1>
|
||||
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.8);">Americans in Monaco</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" style="max-width: 480px; width: 100%; background: #ffffff; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
|
||||
<tr>
|
||||
<td style="padding: 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; color: #CE1126; font-size: 24px;">RSVP Confirmed!</h2>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Your RSVP has been confirmed for the following event:</p>
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #CE1126; font-size: 18px; font-weight: 600;">{{event_title}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Date:</strong> {{event_date}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Location:</strong> {{event_location}}</p>
|
||||
<p style="margin: 0; color: #334155; font-size: 14px;"><strong>Guests:</strong> {{guest_count}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">We look forward to seeing you there!</p>
|
||||
<p style="margin: 0; color: #334155;">Best regards,<br><strong style="color: #CE1126;">The Monaco USA Team</strong></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 24px;">
|
||||
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.5);">© 2026 Monaco USA. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>'
|
||||
WHERE template_key = 'rsvp_confirmation';
|
||||
|
||||
-- =====================
|
||||
-- Update Payment Received Template
|
||||
-- =====================
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td { font-family: Arial, sans-serif !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f172a;">
|
||||
<!--[if gte mso 9]>
|
||||
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
|
||||
<v:fill type="tile" src="https://s3.monacousa.org/public/monaco_high_res.jpg" color="#0f172a"/>
|
||||
</v:background>
|
||||
<![endif]-->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-image: url(''https://s3.monacousa.org/public/monaco_high_res.jpg''); background-size: cover; background-position: center; background-color: #0f172a;">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="background: linear-gradient(135deg, rgba(15, 23, 42, 0.8) 0%, rgba(15, 23, 42, 0.6) 50%, rgba(127, 29, 29, 0.7) 100%);">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 20px;">
|
||||
<!-- Logo Section -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<div style="background: rgba(255, 255, 255, 0.95); border-radius: 16px; padding: 12px; display: inline-block; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);">
|
||||
<img src="{{logo_url}}" alt="Monaco USA" width="80" height="80" style="display: block; border-radius: 8px;">
|
||||
</div>
|
||||
<h1 style="margin: 16px 0 4px 0; font-size: 24px; font-weight: bold; color: #ffffff; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);">Monaco <span style="color: #fca5a5;">USA</span></h1>
|
||||
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.8);">Americans in Monaco</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" style="max-width: 480px; width: 100%; background: #ffffff; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
|
||||
<tr>
|
||||
<td style="padding: 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; color: #CE1126; font-size: 24px;">Payment Received</h2>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Thank you! We have received your membership dues payment.</p>
|
||||
<div style="background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #166534; font-size: 14px; font-weight: 600;">Payment Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Amount:</strong> {{amount}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Payment Date:</strong> {{payment_date}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Period:</strong> {{period_start}} - {{period_end}}</p>
|
||||
<p style="margin: 0; color: #334155; font-size: 14px;"><strong>Member ID:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Your membership is now active through {{period_end}}. Thank you for your continued support of Monaco USA!</p>
|
||||
<p style="margin: 0; color: #334155;">Best regards,<br><strong style="color: #CE1126;">The Monaco USA Team</strong></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 24px;">
|
||||
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.5);">© 2026 Monaco USA. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>'
|
||||
WHERE template_key = 'payment_received';
|
||||
|
||||
-- =====================
|
||||
-- Update Waitlist Promotion Template
|
||||
-- =====================
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td { font-family: Arial, sans-serif !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f172a;">
|
||||
<!--[if gte mso 9]>
|
||||
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
|
||||
<v:fill type="tile" src="https://s3.monacousa.org/public/monaco_high_res.jpg" color="#0f172a"/>
|
||||
</v:background>
|
||||
<![endif]-->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-image: url(''https://s3.monacousa.org/public/monaco_high_res.jpg''); background-size: cover; background-position: center; background-color: #0f172a;">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="background: linear-gradient(135deg, rgba(15, 23, 42, 0.8) 0%, rgba(15, 23, 42, 0.6) 50%, rgba(127, 29, 29, 0.7) 100%);">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 20px;">
|
||||
<!-- Logo Section -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<div style="background: rgba(255, 255, 255, 0.95); border-radius: 16px; padding: 12px; display: inline-block; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);">
|
||||
<img src="{{logo_url}}" alt="Monaco USA" width="80" height="80" style="display: block; border-radius: 8px;">
|
||||
</div>
|
||||
<h1 style="margin: 16px 0 4px 0; font-size: 24px; font-weight: bold; color: #ffffff; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);">Monaco <span style="color: #fca5a5;">USA</span></h1>
|
||||
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.8);">Americans in Monaco</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" style="max-width: 480px; width: 100%; background: #ffffff; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
|
||||
<tr>
|
||||
<td style="padding: 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; color: #CE1126; font-size: 24px;">Great News!</h2>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">A spot has opened up! You have been promoted from the waitlist for:</p>
|
||||
<div style="background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #166534; font-size: 18px; font-weight: 600;">{{event_title}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Date:</strong> {{event_date}}</p>
|
||||
<p style="margin: 0; color: #334155; font-size: 14px;"><strong>Location:</strong> {{event_location}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Your attendance is now confirmed. We look forward to seeing you!</p>
|
||||
<p style="margin: 0; color: #334155;">Best regards,<br><strong style="color: #CE1126;">The Monaco USA Team</strong></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 24px;">
|
||||
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.5);">© 2026 Monaco USA. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>'
|
||||
WHERE template_key = 'waitlist_promotion';
|
||||
|
||||
-- =====================
|
||||
-- Update Dues Reminder 30 Days Template
|
||||
-- =====================
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td { font-family: Arial, sans-serif !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f172a;">
|
||||
<!--[if gte mso 9]>
|
||||
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
|
||||
<v:fill type="tile" src="https://s3.monacousa.org/public/monaco_high_res.jpg" color="#0f172a"/>
|
||||
</v:background>
|
||||
<![endif]-->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-image: url(''https://s3.monacousa.org/public/monaco_high_res.jpg''); background-size: cover; background-position: center; background-color: #0f172a;">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="background: linear-gradient(135deg, rgba(15, 23, 42, 0.8) 0%, rgba(15, 23, 42, 0.6) 50%, rgba(127, 29, 29, 0.7) 100%);">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 20px;">
|
||||
<!-- Logo Section -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<div style="background: rgba(255, 255, 255, 0.95); border-radius: 16px; padding: 12px; display: inline-block; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);">
|
||||
<img src="{{logo_url}}" alt="Monaco USA" width="80" height="80" style="display: block; border-radius: 8px;">
|
||||
</div>
|
||||
<h1 style="margin: 16px 0 4px 0; font-size: 24px; font-weight: bold; color: #ffffff; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);">Monaco <span style="color: #fca5a5;">USA</span></h1>
|
||||
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.8);">Americans in Monaco</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" style="max-width: 480px; width: 100%; background: #ffffff; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
|
||||
<tr>
|
||||
<td style="padding: 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; color: #CE1126; font-size: 24px;">Dues Reminder</h2>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">This is a friendly reminder that your Monaco USA membership dues will be due on <strong>{{due_date}}</strong>.</p>
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #334155; font-size: 14px; font-weight: 600;">Payment Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Amount Due:</strong> {{amount}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Due Date:</strong> {{due_date}}</p>
|
||||
<p style="margin: 0; color: #334155; font-size: 14px;"><strong>Member ID:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<div style="background: #fef3c7; border: 1px solid #fcd34d; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #92400e; font-size: 14px; font-weight: 600;">Bank Transfer Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Account Holder:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Bank:</strong> {{bank_name}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0; color: #78350f; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">You can also view your payment status and history in the member portal:</p>
|
||||
<p style="margin: 0 0 20px 0;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">View My Account</a>
|
||||
</p>
|
||||
<p style="margin: 0; color: #64748b; font-size: 14px;">Thank you for being a valued member of Monaco USA!</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 24px;">
|
||||
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.5);">© 2026 Monaco USA. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>'
|
||||
WHERE template_key = 'dues_reminder_30';
|
||||
|
||||
-- =====================
|
||||
-- Update Dues Reminder 7 Days Template
|
||||
-- =====================
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td { font-family: Arial, sans-serif !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f172a;">
|
||||
<!--[if gte mso 9]>
|
||||
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
|
||||
<v:fill type="tile" src="https://s3.monacousa.org/public/monaco_high_res.jpg" color="#0f172a"/>
|
||||
</v:background>
|
||||
<![endif]-->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-image: url(''https://s3.monacousa.org/public/monaco_high_res.jpg''); background-size: cover; background-position: center; background-color: #0f172a;">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="background: linear-gradient(135deg, rgba(15, 23, 42, 0.8) 0%, rgba(15, 23, 42, 0.6) 50%, rgba(127, 29, 29, 0.7) 100%);">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 20px;">
|
||||
<!-- Logo Section -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<div style="background: rgba(255, 255, 255, 0.95); border-radius: 16px; padding: 12px; display: inline-block; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);">
|
||||
<img src="{{logo_url}}" alt="Monaco USA" width="80" height="80" style="display: block; border-radius: 8px;">
|
||||
</div>
|
||||
<h1 style="margin: 16px 0 4px 0; font-size: 24px; font-weight: bold; color: #ffffff; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);">Monaco <span style="color: #fca5a5;">USA</span></h1>
|
||||
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.8);">Americans in Monaco</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" style="max-width: 480px; width: 100%; background: #ffffff; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
|
||||
<tr>
|
||||
<td style="padding: 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; color: #CE1126; font-size: 24px;">Dues Due Soon</h2>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Your Monaco USA membership dues are due in <strong>7 days</strong> on {{due_date}}.</p>
|
||||
<div style="background: #fef3c7; border: 1px solid #fcd34d; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #92400e; font-size: 14px; font-weight: 600;">Payment Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Amount Due:</strong> {{amount}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Due Date:</strong> {{due_date}}</p>
|
||||
<p style="margin: 0; color: #78350f; font-size: 14px;"><strong>Member ID:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<div style="background: #fef3c7; border: 1px solid #fcd34d; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #92400e; font-size: 14px; font-weight: 600;">Bank Transfer Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Account Holder:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Bank:</strong> {{bank_name}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0; color: #78350f; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Pay Now</a>
|
||||
</p>
|
||||
<p style="margin: 0; color: #64748b; font-size: 14px;">Thank you for your continued support!</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 24px;">
|
||||
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.5);">© 2026 Monaco USA. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>'
|
||||
WHERE template_key = 'dues_reminder_7';
|
||||
|
||||
-- =====================
|
||||
-- Update Dues Reminder 1 Day Template
|
||||
-- =====================
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td { font-family: Arial, sans-serif !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f172a;">
|
||||
<!--[if gte mso 9]>
|
||||
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
|
||||
<v:fill type="tile" src="https://s3.monacousa.org/public/monaco_high_res.jpg" color="#0f172a"/>
|
||||
</v:background>
|
||||
<![endif]-->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-image: url(''https://s3.monacousa.org/public/monaco_high_res.jpg''); background-size: cover; background-position: center; background-color: #0f172a;">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="background: linear-gradient(135deg, rgba(15, 23, 42, 0.8) 0%, rgba(15, 23, 42, 0.6) 50%, rgba(127, 29, 29, 0.7) 100%);">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 20px;">
|
||||
<!-- Logo Section -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<div style="background: rgba(255, 255, 255, 0.95); border-radius: 16px; padding: 12px; display: inline-block; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);">
|
||||
<img src="{{logo_url}}" alt="Monaco USA" width="80" height="80" style="display: block; border-radius: 8px;">
|
||||
</div>
|
||||
<h1 style="margin: 16px 0 4px 0; font-size: 24px; font-weight: bold; color: #ffffff; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);">Monaco <span style="color: #fca5a5;">USA</span></h1>
|
||||
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.8);">Americans in Monaco</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" style="max-width: 480px; width: 100%; background: #ffffff; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
|
||||
<tr>
|
||||
<td style="padding: 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; color: #CE1126; font-size: 24px;">Final Reminder</h2>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">This is a final reminder that your Monaco USA membership dues are due <strong>tomorrow</strong> ({{due_date}}).</p>
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #991b1b; font-size: 14px; font-weight: 600;">Urgent - Payment Required:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #991b1b; font-size: 14px;"><strong>Amount Due:</strong> {{amount}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #991b1b; font-size: 14px;"><strong>Due Date:</strong> {{due_date}}</p>
|
||||
<p style="margin: 0; color: #991b1b; font-size: 14px;"><strong>Member ID:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<div style="background: #fef3c7; border: 1px solid #fcd34d; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #92400e; font-size: 14px; font-weight: 600;">Bank Transfer Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Account Holder:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Bank:</strong> {{bank_name}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0; color: #78350f; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Pay Now</a>
|
||||
</p>
|
||||
<p style="margin: 0; color: #64748b; font-size: 14px;">Please disregard this email if you have already made your payment.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 24px;">
|
||||
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.5);">© 2026 Monaco USA. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>'
|
||||
WHERE template_key = 'dues_reminder_1';
|
||||
|
||||
-- =====================
|
||||
-- Update Dues Reminder (Generic) Template if exists
|
||||
-- =====================
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td { font-family: Arial, sans-serif !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f172a;">
|
||||
<!--[if gte mso 9]>
|
||||
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
|
||||
<v:fill type="tile" src="https://s3.monacousa.org/public/monaco_high_res.jpg" color="#0f172a"/>
|
||||
</v:background>
|
||||
<![endif]-->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-image: url(''https://s3.monacousa.org/public/monaco_high_res.jpg''); background-size: cover; background-position: center; background-color: #0f172a;">
|
||||
<tr>
|
||||
<td>
|
||||
<div style="background: linear-gradient(135deg, rgba(15, 23, 42, 0.8) 0%, rgba(15, 23, 42, 0.6) 50%, rgba(127, 29, 29, 0.7) 100%);">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 40px 20px;">
|
||||
<!-- Logo Section -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 30px;">
|
||||
<div style="background: rgba(255, 255, 255, 0.95); border-radius: 16px; padding: 12px; display: inline-block; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);">
|
||||
<img src="{{logo_url}}" alt="Monaco USA" width="80" height="80" style="display: block; border-radius: 8px;">
|
||||
</div>
|
||||
<h1 style="margin: 16px 0 4px 0; font-size: 24px; font-weight: bold; color: #ffffff; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);">Monaco <span style="color: #fca5a5;">USA</span></h1>
|
||||
<p style="margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.8);">Americans in Monaco</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<table role="presentation" cellspacing="0" cellpadding="0" style="max-width: 480px; width: 100%; background: #ffffff; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);">
|
||||
<tr>
|
||||
<td style="padding: 40px;">
|
||||
<h2 style="margin: 0 0 20px 0; color: #CE1126; font-size: 24px;">Membership Dues Reminder</h2>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">This is a reminder about your Monaco USA membership dues.</p>
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #334155; font-size: 14px; font-weight: 600;">Payment Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Amount Due:</strong> {{amount}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Due Date:</strong> {{due_date}}</p>
|
||||
<p style="margin: 0; color: #334155; font-size: 14px;"><strong>Member ID:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">View My Account</a>
|
||||
</p>
|
||||
<p style="margin: 0; color: #64748b; font-size: 14px;">Thank you for being a valued member of Monaco USA!</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer -->
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 24px;">
|
||||
<p style="margin: 0; font-size: 12px; color: rgba(255, 255, 255, 0.5);">© 2026 Monaco USA. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>'
|
||||
WHERE template_key = 'dues_reminder';
|
||||
133
supabase/migrations/014_event_reminders.sql
Normal file
133
supabase/migrations/014_event_reminders.sql
Normal file
@@ -0,0 +1,133 @@
|
||||
-- Monaco USA Portal 2026 - Event Reminder Emails
|
||||
-- Automated reminders sent 24 hours before events to RSVPed members
|
||||
|
||||
-- ============================================
|
||||
-- EVENT REMINDER LOGS TABLE
|
||||
-- ============================================
|
||||
CREATE TABLE public.event_reminder_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id UUID NOT NULL REFERENCES public.events(id) ON DELETE CASCADE,
|
||||
rsvp_id UUID NOT NULL REFERENCES public.event_rsvps(id) ON DELETE CASCADE,
|
||||
member_id UUID NOT NULL REFERENCES public.members(id) ON DELETE CASCADE,
|
||||
reminder_type TEXT NOT NULL DEFAULT '24hr' CHECK (reminder_type IN ('24hr', '1hr', 'day_of')),
|
||||
sent_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
email_log_id UUID REFERENCES public.email_logs(id),
|
||||
-- Prevent duplicate reminders for same event/member/type
|
||||
UNIQUE(event_id, member_id, reminder_type)
|
||||
);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE public.event_reminder_logs ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Board and admin can view reminder logs
|
||||
CREATE POLICY "Board/admin can view event reminder logs" ON public.event_reminder_logs
|
||||
FOR SELECT USING (
|
||||
EXISTS (SELECT 1 FROM public.members WHERE id = auth.uid() AND role IN ('board', 'admin'))
|
||||
);
|
||||
|
||||
-- Service role can manage reminder logs (from cron/server)
|
||||
CREATE POLICY "Service role can manage event reminder logs" ON public.event_reminder_logs
|
||||
FOR ALL USING (true)
|
||||
WITH CHECK (true);
|
||||
|
||||
-- Indexes for fast lookups
|
||||
CREATE INDEX idx_event_reminder_logs_event ON public.event_reminder_logs(event_id);
|
||||
CREATE INDEX idx_event_reminder_logs_member ON public.event_reminder_logs(member_id);
|
||||
CREATE INDEX idx_event_reminder_logs_sent ON public.event_reminder_logs(sent_at);
|
||||
|
||||
-- ============================================
|
||||
-- ADD APP SETTINGS FOR EVENT REMINDERS
|
||||
-- ============================================
|
||||
INSERT INTO public.app_settings (category, setting_key, setting_value, display_name, description, is_public)
|
||||
VALUES
|
||||
('events', 'event_reminders_enabled', 'true', 'Event Reminders Enabled', 'Enable automated event reminder emails', false),
|
||||
('events', 'event_reminder_hours_before', '24', 'Reminder Hours Before', 'Hours before event to send reminder', false)
|
||||
ON CONFLICT (category, setting_key) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- ADD EMAIL TEMPLATE FOR EVENT REMINDER
|
||||
-- ============================================
|
||||
INSERT INTO public.email_templates (template_key, template_name, category, subject, body_html, body_text, is_active, is_system, variables_schema) VALUES
|
||||
(
|
||||
'event_reminder_24hr',
|
||||
'Event Reminder - 24 Hours',
|
||||
'events',
|
||||
'Reminder: {{event_title}} is Tomorrow!',
|
||||
'<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Hi {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">This is a friendly reminder that <strong>{{event_title}}</strong> is happening tomorrow!</p>
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #334155; font-size: 14px; font-weight: 600;">Event Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Date:</strong> {{event_date}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Time:</strong> {{event_time}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Location:</strong> {{event_location}}</p>
|
||||
{{#if guest_count}}
|
||||
<p style="margin: 8px 0 0 0; color: #64748b; font-size: 14px;"><em>You''re bringing {{guest_count}} guest(s)</em></p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">We look forward to seeing you there!</p>
|
||||
<p style="margin: 0 0 20px 0;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">View Event Details</a>
|
||||
</p>
|
||||
<p style="margin: 0; color: #64748b; font-size: 12px;">Can''t make it? Please update your RSVP so we can offer your spot to someone on the waitlist.</p>',
|
||||
'Hi {{first_name}},
|
||||
|
||||
This is a friendly reminder that {{event_title}} is happening tomorrow!
|
||||
|
||||
Event Details:
|
||||
- Date: {{event_date}}
|
||||
- Time: {{event_time}}
|
||||
- Location: {{event_location}}
|
||||
{{#if guest_count}}
|
||||
- You''re bringing {{guest_count}} guest(s)
|
||||
{{/if}}
|
||||
|
||||
We look forward to seeing you there!
|
||||
|
||||
View event: {{portal_url}}
|
||||
|
||||
Can''t make it? Please update your RSVP so we can offer your spot to someone on the waitlist.',
|
||||
true,
|
||||
true,
|
||||
'{"first_name": "Member first name", "event_title": "Event title", "event_date": "Event date", "event_time": "Event start time", "event_location": "Event location", "guest_count": "Number of guests", "portal_url": "Event URL in portal"}'
|
||||
)
|
||||
ON CONFLICT (template_key) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- VIEW: Events needing reminders
|
||||
-- ============================================
|
||||
CREATE OR REPLACE VIEW public.events_needing_reminders AS
|
||||
SELECT
|
||||
e.id AS event_id,
|
||||
e.title AS event_title,
|
||||
e.start_datetime,
|
||||
e.end_datetime,
|
||||
e.location,
|
||||
e.timezone,
|
||||
r.id AS rsvp_id,
|
||||
r.member_id,
|
||||
r.guest_count,
|
||||
r.status AS rsvp_status,
|
||||
m.first_name,
|
||||
m.last_name,
|
||||
m.email
|
||||
FROM public.events e
|
||||
JOIN public.event_rsvps r ON r.event_id = e.id
|
||||
JOIN public.members m ON m.id = r.member_id
|
||||
WHERE
|
||||
-- Event is published
|
||||
e.status = 'published'
|
||||
-- Event starts within 24-25 hours from now (hourly cron window)
|
||||
AND e.start_datetime > NOW()
|
||||
AND e.start_datetime <= NOW() + INTERVAL '25 hours'
|
||||
AND e.start_datetime > NOW() + INTERVAL '23 hours'
|
||||
-- Member has confirmed RSVP
|
||||
AND r.status = 'confirmed'
|
||||
-- No reminder already sent for this event/member
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM public.event_reminder_logs erl
|
||||
WHERE erl.event_id = e.id
|
||||
AND erl.member_id = r.member_id
|
||||
AND erl.reminder_type = '24hr'
|
||||
)
|
||||
-- Member has email
|
||||
AND m.email IS NOT NULL;
|
||||
192
supabase/migrations/015_fix_email_template_styling.sql
Normal file
192
supabase/migrations/015_fix_email_template_styling.sql
Normal file
@@ -0,0 +1,192 @@
|
||||
-- Monaco USA Portal 2026 - Fix Email Template Styling
|
||||
-- Update all email templates with proper text centering and styling
|
||||
|
||||
-- ============================================
|
||||
-- UPDATE DUES REMINDER TEMPLATES
|
||||
-- ============================================
|
||||
|
||||
-- 30 days before due reminder
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">This is a friendly reminder that your Monaco USA membership dues will be due on <strong>{{due_date}}</strong>.</p>
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #334155; font-size: 14px; font-weight: 600;">Payment Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Amount Due:</strong> {{amount}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Due Date:</strong> {{due_date}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Member ID:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<div style="background: #dbeafe; border: 1px solid #93c5fd; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #1e40af; font-size: 14px; font-weight: 600;">Bank Transfer Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Account Holder:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Bank:</strong> {{bank_name}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">You can also view your payment status and history in the member portal:</p>
|
||||
<p style="margin: 0 0 20px 0; text-align: center;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">View My Account</a>
|
||||
</p>
|
||||
<p style="margin: 0; color: #64748b; font-size: 14px; text-align: center;">Thank you for being a valued member of Monaco USA!</p>'
|
||||
WHERE template_key = 'dues_reminder_30';
|
||||
|
||||
-- 7 days before due reminder
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Your Monaco USA membership dues will be due in <strong>7 days</strong> on {{due_date}}.</p>
|
||||
<div style="background: #fef3c7; border: 1px solid #fcd34d; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #92400e; font-size: 14px; font-weight: 600;">Payment Information:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Amount:</strong> {{amount}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Due Date:</strong> {{due_date}}</p>
|
||||
</div>
|
||||
<div style="background: #dbeafe; border: 1px solid #93c5fd; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #1e40af; font-size: 14px; font-weight: 600;">Bank Transfer Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Account Holder:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Bank:</strong> {{bank_name}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0; text-align: center;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Pay Now</a>
|
||||
</p>
|
||||
<p style="margin: 0; color: #64748b; font-size: 14px; text-align: center;">Questions? Contact us at contact@monacousa.org</p>'
|
||||
WHERE template_key = 'dues_reminder_7';
|
||||
|
||||
-- 1 day before due reminder
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;"><strong style="color: #dc2626;">Your Monaco USA membership dues are due tomorrow ({{due_date}}).</strong></p>
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #991b1b; font-size: 14px; font-weight: 600;">Payment Required:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Amount:</strong> {{amount}}</p>
|
||||
</div>
|
||||
<div style="background: #dbeafe; border: 1px solid #93c5fd; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #1e40af; font-size: 14px; font-weight: 600;">Bank Transfer Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Account Holder:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Bank:</strong> {{bank_name}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">To maintain your active membership status and continued access to member benefits, please ensure payment is made by the due date.</p>
|
||||
<p style="margin: 0 0 20px 0; text-align: center;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #dc2626; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Pay Now</a>
|
||||
</p>'
|
||||
WHERE template_key = 'dues_reminder_1';
|
||||
|
||||
-- Overdue notice
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Your Monaco USA membership dues are now <strong style="color: #dc2626;">{{days_overdue}} days overdue</strong>.</p>
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #991b1b; font-size: 14px; font-weight: 600;">Overdue Payment:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Amount:</strong> {{amount}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Original Due Date:</strong> {{due_date}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Days Overdue:</strong> {{days_overdue}}</p>
|
||||
</div>
|
||||
<div style="background: #fffbeb; border: 1px solid #fcd34d; border-radius: 12px; padding: 16px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0; color: #92400e; font-size: 14px;"><strong>Grace Period:</strong> You have {{grace_days_remaining}} days remaining in your grace period. After this, your membership status will be changed to inactive.</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Please remit payment as soon as possible to maintain your membership benefits.</p>
|
||||
<div style="background: #dbeafe; border: 1px solid #93c5fd; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #1e40af; font-size: 14px; font-weight: 600;">Bank Transfer Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Account Holder:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Bank:</strong> {{bank_name}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0; text-align: center;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #dc2626; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Pay Now</a>
|
||||
</p>'
|
||||
WHERE template_key = 'dues_overdue';
|
||||
|
||||
-- Grace period warning
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;"><strong style="color: #dc2626;">Your grace period ends in {{grace_days_remaining}} days.</strong></p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Your membership dues of <strong>{{amount}}</strong> were due on {{due_date}} and are now {{days_overdue}} days overdue.</p>
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0; color: #991b1b; font-size: 14px;"><strong>If payment is not received by {{grace_end_date}}, your membership status will automatically change to INACTIVE and you will lose access to member benefits.</strong></p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Please make your payment immediately to avoid interruption:</p>
|
||||
<div style="background: #dbeafe; border: 1px solid #93c5fd; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #1e40af; font-size: 14px; font-weight: 600;">Bank Transfer Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Account Holder:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Bank:</strong> {{bank_name}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0; text-align: center;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #dc2626; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Pay Now - Urgent</a>
|
||||
</p>'
|
||||
WHERE template_key = 'dues_grace_warning';
|
||||
|
||||
-- Inactive notice
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Due to non-payment of membership dues, your Monaco USA membership has been marked as <strong style="color: #dc2626;">INACTIVE</strong>.</p>
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #991b1b; font-size: 14px; font-weight: 600;">Status Change:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Previous Status:</strong> Active</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>New Status:</strong> Inactive</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Outstanding Amount:</strong> {{amount}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">As an inactive member, you will no longer have access to:</p>
|
||||
<ul style="margin: 0 0 16px 0; padding-left: 20px; color: #334155;">
|
||||
<li>Member-only events</li>
|
||||
<li>Member directory</li>
|
||||
<li>Member communications</li>
|
||||
<li>Voting rights</li>
|
||||
</ul>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">To reactivate your membership, please pay your outstanding dues:</p>
|
||||
<div style="background: #dbeafe; border: 1px solid #93c5fd; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #1e40af; font-size: 14px; font-weight: 600;">Bank Transfer Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Account Holder:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Bank:</strong> {{bank_name}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0; text-align: center;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Reactivate My Membership</a>
|
||||
</p>
|
||||
<p style="margin: 0; color: #64748b; font-size: 14px; text-align: center;">If you believe this is an error or have questions, please contact us at contact@monacousa.org</p>'
|
||||
WHERE template_key = 'dues_inactive_notice';
|
||||
|
||||
-- ============================================
|
||||
-- UPDATE EVENT REMINDER TEMPLATE
|
||||
-- ============================================
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Hi {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">This is a friendly reminder that <strong>{{event_title}}</strong> is happening tomorrow!</p>
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #334155; font-size: 14px; font-weight: 600;">Event Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Date:</strong> {{event_date}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Time:</strong> {{event_time}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #334155; font-size: 14px;"><strong>Location:</strong> {{event_location}}</p>
|
||||
{{#if guest_count}}
|
||||
<p style="margin: 8px 0 0 0; color: #64748b; font-size: 14px;"><em>You''re bringing {{guest_count}} guest(s)</em></p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">We look forward to seeing you there!</p>
|
||||
<p style="margin: 0 0 20px 0; text-align: center;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">View Event Details</a>
|
||||
</p>
|
||||
<p style="margin: 0; color: #64748b; font-size: 12px; text-align: center;">Can''t make it? Please update your RSVP so we can offer your spot to someone on the waitlist.</p>'
|
||||
WHERE template_key = 'event_reminder_24hr';
|
||||
|
||||
-- ============================================
|
||||
-- UPDATE RSVP CONFIRMATION TEMPLATE (content-only version)
|
||||
-- ============================================
|
||||
-- Note: The original is a full HTML template, so we'll create a content-only version
|
||||
-- that works with wrapInMonacoTemplate
|
||||
|
||||
-- Update waitlist promotion to be content-only with proper styling
|
||||
UPDATE public.email_templates
|
||||
SET body_html = '<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 20px 0; color: #334155; line-height: 1.6;">Great news! A spot has opened up for <strong>{{event_title}}</strong> and you have been moved from the waitlist to confirmed!</p>
|
||||
<div style="background: #f8fafc; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #64748b; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Event Details</p>
|
||||
<p style="margin: 0 0 8px 0; color: #334155;"><strong>Date:</strong> {{event_date}}</p>
|
||||
<p style="margin: 0; color: #334155;"><strong>Location:</strong> {{event_location}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0; color: #334155; line-height: 1.6;">We look forward to seeing you there!</p>
|
||||
<p style="margin: 0; color: #334155; text-align: center;">Best regards,<br><strong style="color: #CE1126;">The Monaco USA Team</strong></p>'
|
||||
WHERE template_key = 'waitlist_promotion';
|
||||
237
supabase/migrations/016_onboarding_payment_tracking.sql
Normal file
237
supabase/migrations/016_onboarding_payment_tracking.sql
Normal file
@@ -0,0 +1,237 @@
|
||||
-- Monaco USA Portal 2026 - Onboarding Payment Tracking
|
||||
-- Track new member payment deadlines for the 30-day payment window
|
||||
|
||||
-- ============================================
|
||||
-- ADD PAYMENT TRACKING COLUMNS TO MEMBERS
|
||||
-- ============================================
|
||||
|
||||
-- Payment deadline for new signups (30 days from onboarding completion)
|
||||
ALTER TABLE public.members ADD COLUMN IF NOT EXISTS payment_deadline TIMESTAMPTZ;
|
||||
|
||||
-- Track when onboarding was completed
|
||||
ALTER TABLE public.members ADD COLUMN IF NOT EXISTS onboarding_completed_at TIMESTAMPTZ;
|
||||
|
||||
-- Index for efficient reminder queries (only index non-null deadlines)
|
||||
CREATE INDEX IF NOT EXISTS idx_members_payment_deadline ON public.members(payment_deadline)
|
||||
WHERE payment_deadline IS NOT NULL;
|
||||
|
||||
-- ============================================
|
||||
-- ADD ONBOARDING REMINDER TYPES TO LOGS TABLE
|
||||
-- ============================================
|
||||
|
||||
-- Update the check constraint on dues_reminder_logs to include onboarding types
|
||||
ALTER TABLE public.dues_reminder_logs DROP CONSTRAINT IF EXISTS dues_reminder_logs_reminder_type_check;
|
||||
ALTER TABLE public.dues_reminder_logs ADD CONSTRAINT dues_reminder_logs_reminder_type_check
|
||||
CHECK (reminder_type IN (
|
||||
'due_soon_30', 'due_soon_7', 'due_soon_1', 'overdue', 'grace_period', 'inactive_notice',
|
||||
'onboarding_welcome', 'onboarding_reminder_7', 'onboarding_reminder_1', 'onboarding_expired'
|
||||
));
|
||||
|
||||
-- ============================================
|
||||
-- ADD EMAIL TEMPLATES FOR ONBOARDING REMINDERS
|
||||
-- ============================================
|
||||
|
||||
-- Welcome email with payment instructions (sent immediately after onboarding)
|
||||
INSERT INTO public.email_templates (template_key, template_name, category, subject, body_html, body_text, is_active, is_system, variables_schema) VALUES
|
||||
(
|
||||
'onboarding_welcome',
|
||||
'Welcome - Complete Your Membership',
|
||||
'onboarding',
|
||||
'Welcome to Monaco USA - Complete Your Membership',
|
||||
'<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Welcome to Monaco USA! We''re thrilled to have you join our community of Americans living in and connected to Monaco.</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Your account has been created and you now have <strong>30 days</strong> to complete your membership by paying your annual dues.</p>
|
||||
<div style="background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #166534; font-size: 14px; font-weight: 600;">Your Membership Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #166534; font-size: 14px;"><strong>Member ID:</strong> {{member_id}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #166534; font-size: 14px;"><strong>Annual Dues:</strong> {{amount}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #166534; font-size: 14px;"><strong>Payment Deadline:</strong> {{payment_deadline}}</p>
|
||||
</div>
|
||||
<div style="background: #dbeafe; border: 1px solid #93c5fd; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #1e40af; font-size: 14px; font-weight: 600;">Bank Transfer Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Account Holder:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Bank:</strong> {{bank_name}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">In the meantime, explore your new member dashboard and connect with our community:</p>
|
||||
<p style="margin: 0 0 20px 0; text-align: center;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Go to Dashboard</a>
|
||||
</p>
|
||||
<p style="margin: 0; color: #64748b; font-size: 14px; text-align: center;">Questions? Contact us at contact@monacousa.org</p>',
|
||||
'Dear {{first_name}},
|
||||
|
||||
Welcome to Monaco USA! We''re thrilled to have you join our community of Americans living in and connected to Monaco.
|
||||
|
||||
Your account has been created and you now have 30 days to complete your membership by paying your annual dues.
|
||||
|
||||
Your Membership Details:
|
||||
- Member ID: {{member_id}}
|
||||
- Annual Dues: {{amount}}
|
||||
- Payment Deadline: {{payment_deadline}}
|
||||
|
||||
Bank Transfer Details:
|
||||
- Account Holder: {{account_holder}}
|
||||
- Bank: {{bank_name}}
|
||||
- IBAN: {{iban}}
|
||||
- Reference: {{member_id}}
|
||||
|
||||
Visit your dashboard: {{portal_url}}
|
||||
|
||||
Questions? Contact us at contact@monacousa.org',
|
||||
true,
|
||||
true,
|
||||
'{"first_name": "Member first name", "member_id": "Member ID", "amount": "Annual dues amount", "payment_deadline": "Payment deadline date", "account_holder": "Bank account holder", "bank_name": "Bank name", "iban": "IBAN number", "portal_url": "Portal URL"}'
|
||||
) ON CONFLICT (template_key) DO NOTHING;
|
||||
|
||||
-- 7 days left reminder
|
||||
INSERT INTO public.email_templates (template_key, template_name, category, subject, body_html, body_text, is_active, is_system, variables_schema) VALUES
|
||||
(
|
||||
'onboarding_reminder_7',
|
||||
'Onboarding Reminder - 7 Days Left',
|
||||
'onboarding',
|
||||
'7 Days Left to Complete Your Monaco USA Membership',
|
||||
'<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">You have <strong style="color: #d97706;">7 days left</strong> to complete your Monaco USA membership by paying your annual dues.</p>
|
||||
<div style="background: #fef3c7; border: 1px solid #fcd34d; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #92400e; font-size: 14px; font-weight: 600;">Payment Due:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Amount:</strong> {{amount}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #78350f; font-size: 14px;"><strong>Deadline:</strong> {{payment_deadline}}</p>
|
||||
</div>
|
||||
<div style="background: #dbeafe; border: 1px solid #93c5fd; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #1e40af; font-size: 14px; font-weight: 600;">Bank Transfer Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Account Holder:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Bank:</strong> {{bank_name}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">After the deadline, your account will be marked as inactive and you''ll lose access to member features.</p>
|
||||
<p style="margin: 0 0 20px 0; text-align: center;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">View My Account</a>
|
||||
</p>',
|
||||
'Dear {{first_name}},
|
||||
|
||||
You have 7 days left to complete your Monaco USA membership by paying your annual dues.
|
||||
|
||||
Payment Due:
|
||||
- Amount: {{amount}}
|
||||
- Deadline: {{payment_deadline}}
|
||||
|
||||
Bank Transfer Details:
|
||||
- Account Holder: {{account_holder}}
|
||||
- Bank: {{bank_name}}
|
||||
- IBAN: {{iban}}
|
||||
- Reference: {{member_id}}
|
||||
|
||||
After the deadline, your account will be marked as inactive.
|
||||
|
||||
Visit: {{portal_url}}',
|
||||
true,
|
||||
true,
|
||||
'{"first_name": "Member first name", "member_id": "Member ID", "amount": "Annual dues amount", "payment_deadline": "Payment deadline date", "account_holder": "Bank account holder", "bank_name": "Bank name", "iban": "IBAN number", "portal_url": "Portal URL"}'
|
||||
) ON CONFLICT (template_key) DO NOTHING;
|
||||
|
||||
-- Last day reminder (urgent)
|
||||
INSERT INTO public.email_templates (template_key, template_name, category, subject, body_html, body_text, is_active, is_system, variables_schema) VALUES
|
||||
(
|
||||
'onboarding_reminder_1',
|
||||
'Onboarding Reminder - Last Day',
|
||||
'onboarding',
|
||||
'URGENT: Last Day to Complete Your Monaco USA Membership',
|
||||
'<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;"><strong style="color: #dc2626;">Today is your last day</strong> to complete your Monaco USA membership payment.</p>
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #991b1b; font-size: 14px; font-weight: 600;">Payment Required Today:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Amount:</strong> {{amount}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Deadline:</strong> {{payment_deadline}}</p>
|
||||
</div>
|
||||
<div style="background: #dbeafe; border: 1px solid #93c5fd; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #1e40af; font-size: 14px; font-weight: 600;">Bank Transfer Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Account Holder:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Bank:</strong> {{bank_name}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #dc2626; line-height: 1.6; font-weight: 600;">If we don''t receive your payment today, your account will be marked as inactive tomorrow.</p>
|
||||
<p style="margin: 0 0 20px 0; text-align: center;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #dc2626; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Complete Payment Now</a>
|
||||
</p>',
|
||||
'Dear {{first_name}},
|
||||
|
||||
TODAY IS YOUR LAST DAY to complete your Monaco USA membership payment.
|
||||
|
||||
Payment Required Today:
|
||||
- Amount: {{amount}}
|
||||
- Deadline: {{payment_deadline}}
|
||||
|
||||
Bank Transfer Details:
|
||||
- Account Holder: {{account_holder}}
|
||||
- Bank: {{bank_name}}
|
||||
- IBAN: {{iban}}
|
||||
- Reference: {{member_id}}
|
||||
|
||||
If we don''t receive your payment today, your account will be marked as inactive tomorrow.
|
||||
|
||||
Visit: {{portal_url}}',
|
||||
true,
|
||||
true,
|
||||
'{"first_name": "Member first name", "member_id": "Member ID", "amount": "Annual dues amount", "payment_deadline": "Payment deadline date", "account_holder": "Bank account holder", "bank_name": "Bank name", "iban": "IBAN number", "portal_url": "Portal URL"}'
|
||||
) ON CONFLICT (template_key) DO NOTHING;
|
||||
|
||||
-- Account marked inactive (deadline passed)
|
||||
INSERT INTO public.email_templates (template_key, template_name, category, subject, body_html, body_text, is_active, is_system, variables_schema) VALUES
|
||||
(
|
||||
'onboarding_expired',
|
||||
'Onboarding Expired - Account Inactive',
|
||||
'onboarding',
|
||||
'Your Monaco USA Account Has Been Marked Inactive',
|
||||
'<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Dear {{first_name}},</p>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">Your 30-day payment window has expired and your Monaco USA account has been marked as <strong style="color: #dc2626;">INACTIVE</strong>.</p>
|
||||
<div style="background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #991b1b; font-size: 14px; font-weight: 600;">Account Status:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Status:</strong> Inactive</p>
|
||||
<p style="margin: 0 0 4px 0; color: #7f1d1d; font-size: 14px;"><strong>Outstanding Amount:</strong> {{amount}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">As an inactive member, you no longer have access to:</p>
|
||||
<ul style="margin: 0 0 16px 0; padding-left: 20px; color: #334155;">
|
||||
<li>Member-only events</li>
|
||||
<li>Member directory</li>
|
||||
<li>Member communications</li>
|
||||
</ul>
|
||||
<p style="margin: 0 0 16px 0; color: #334155; line-height: 1.6;">To reactivate your membership, please complete your dues payment:</p>
|
||||
<div style="background: #dbeafe; border: 1px solid #93c5fd; border-radius: 12px; padding: 20px; margin: 0 0 20px 0;">
|
||||
<p style="margin: 0 0 8px 0; color: #1e40af; font-size: 14px; font-weight: 600;">Bank Transfer Details:</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Account Holder:</strong> {{account_holder}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Bank:</strong> {{bank_name}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>IBAN:</strong> {{iban}}</p>
|
||||
<p style="margin: 0 0 4px 0; color: #1e3a8a; font-size: 14px;"><strong>Reference:</strong> {{member_id}}</p>
|
||||
</div>
|
||||
<p style="margin: 0 0 20px 0; text-align: center;">
|
||||
<a href="{{portal_url}}" style="display: inline-block; background: #CE1126; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Reactivate My Account</a>
|
||||
</p>
|
||||
<p style="margin: 0; color: #64748b; font-size: 14px; text-align: center;">Questions? Contact us at contact@monacousa.org</p>',
|
||||
'Dear {{first_name}},
|
||||
|
||||
Your 30-day payment window has expired and your Monaco USA account has been marked as INACTIVE.
|
||||
|
||||
Account Status:
|
||||
- Status: Inactive
|
||||
- Outstanding Amount: {{amount}}
|
||||
|
||||
As an inactive member, you no longer have access to member-only events, directory, and communications.
|
||||
|
||||
To reactivate your membership, please pay your dues:
|
||||
|
||||
Bank Transfer Details:
|
||||
- Account Holder: {{account_holder}}
|
||||
- Bank: {{bank_name}}
|
||||
- IBAN: {{iban}}
|
||||
- Reference: {{member_id}}
|
||||
|
||||
Visit: {{portal_url}}
|
||||
|
||||
Questions? Contact us at contact@monacousa.org',
|
||||
true,
|
||||
true,
|
||||
'{"first_name": "Member first name", "member_id": "Member ID", "amount": "Annual dues amount", "account_holder": "Bank account holder", "bank_name": "Bank name", "iban": "IBAN number", "portal_url": "Portal URL"}'
|
||||
) ON CONFLICT (template_key) DO NOTHING;
|
||||
Reference in New Issue
Block a user