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:
2026-01-25 02:19:49 +01:00
commit e7338d1a70
457 changed files with 54912 additions and 0 deletions

196
supabase/docker/kong.yml Normal file
View File

@@ -0,0 +1,196 @@
_format_version: "2.1"
_transform: true
###
### Consumers / Users
###
consumers:
- username: ANON
keyauth_credentials:
- key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.b_lMH2mc5km7S9Lw_sRGGqE9IeiahYu-caevDcacKiY
- username: SERVICE_ROLE
keyauth_credentials:
- key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.kcyKZAiwnnBG9t6IVGO17bcVw574pVynTHYVdF4q-p0
###
### Access Control Lists
###
acls:
- consumer: ANON
group: anon
- consumer: SERVICE_ROLE
group: admin
###
### API Routes
###
services:
## Redirect /auth/verify to SvelteKit app for email links
- name: auth-verify-redirect
url: http://portal:3000/auth/verify
routes:
- name: auth-verify-redirect
strip_path: false
paths:
- /auth/verify
preserve_host: false
plugins:
- name: cors
## Auth Service (GoTrue)
- name: auth-v1-open
url: http://auth:9999/verify
routes:
- name: auth-v1-open
strip_path: true
paths:
- /auth/v1/verify
plugins:
- name: cors
- name: auth-v1-open-callback
url: http://auth:9999/callback
routes:
- name: auth-v1-open-callback
strip_path: true
paths:
- /auth/v1/callback
plugins:
- name: cors
- name: auth-v1-open-authorize
url: http://auth:9999/authorize
routes:
- name: auth-v1-open-authorize
strip_path: true
paths:
- /auth/v1/authorize
plugins:
- name: cors
- name: auth-v1
url: http://auth:9999/
routes:
- name: auth-v1
strip_path: true
paths:
- /auth/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon
## REST Service (PostgREST)
- name: rest-v1
url: http://rest:3000/
routes:
- name: rest-v1
strip_path: true
paths:
- /rest/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon
## Realtime Service
- name: realtime-v1-ws
url: http://realtime:4000/socket
routes:
- name: realtime-v1-ws
strip_path: true
paths:
- /realtime/v1/websocket
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon
- name: realtime-v1
url: http://realtime:4000/
routes:
- name: realtime-v1
strip_path: true
paths:
- /realtime/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon
## Storage Service - Public objects (no auth required)
- name: storage-v1-public
url: http://storage:5000/object/public
routes:
- name: storage-v1-public
strip_path: true
paths:
- /storage/v1/object/public
plugins:
- name: cors
## Storage Service - All other operations (auth required)
- name: storage-v1
url: http://storage:5000/
routes:
- name: storage-v1
strip_path: true
paths:
- /storage/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: false
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon
## PostgreSQL Meta (for Studio)
- name: meta
url: http://meta:8080/
routes:
- name: meta
strip_path: true
paths:
- /pg/
plugins:
- name: key-auth
config:
hide_credentials: false
- name: acl
config:
hide_groups_header: true
allow:
- admin

146
supabase/fix_rls_now.sql Normal file
View File

@@ -0,0 +1,146 @@
-- ============================================
-- IMMEDIATE FIX FOR RLS ISSUES
-- Run this SQL directly in Supabase Studio SQL Editor
-- ============================================
-- =====================
-- STEP 1: FIX STORAGE.OBJECTS POLICIES
-- =====================
-- Drop any existing service_role policies with various 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;
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 for ALL storage operations
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);
-- Grant permissions
GRANT ALL ON storage.objects TO service_role;
GRANT ALL ON storage.buckets TO service_role;
GRANT USAGE ON SCHEMA storage TO service_role;
-- =====================
-- STEP 2: FIX PUBLIC.MEMBERS POLICIES
-- =====================
-- Drop any existing service_role policies on members
DROP POLICY IF EXISTS "service_role_all_members" ON public.members;
DROP POLICY IF EXISTS "service_role_select_members" ON public.members;
DROP POLICY IF EXISTS "service_role_insert_members" ON public.members;
DROP POLICY IF EXISTS "service_role_update_members" ON public.members;
DROP POLICY IF EXISTS "service_role_delete_members" ON public.members;
-- Create universal service_role policy for members table
CREATE POLICY "service_role_all_members" ON public.members
FOR ALL TO service_role
USING (true)
WITH CHECK (true);
-- Grant permissions
GRANT ALL ON public.members TO service_role;
-- =====================
-- STEP 3: ENSURE STORAGE BUCKETS EXIST
-- =====================
-- Avatars bucket (public)
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
VALUES (
'avatars',
'avatars',
true,
5242880,
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;
-- 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,
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;
-- =====================
-- STEP 4: TRY TO GRANT BYPASSRLS (may fail, that's OK)
-- =====================
DO $$
BEGIN
ALTER ROLE service_role BYPASSRLS;
RAISE NOTICE 'SUCCESS: Granted BYPASSRLS to service_role';
EXCEPTION
WHEN insufficient_privilege THEN
RAISE NOTICE 'INFO: Could not grant BYPASSRLS (using explicit policies instead)';
WHEN OTHERS THEN
RAISE NOTICE 'INFO: BYPASSRLS not needed or already set';
END $$;
-- =====================
-- STEP 5: VERIFY SETUP
-- =====================
-- Check service_role policies on storage.objects
SELECT
policyname,
permissive,
roles,
cmd,
qual,
with_check
FROM pg_policies
WHERE schemaname = 'storage'
AND tablename = 'objects'
AND 'service_role' = ANY(roles);
-- Check service_role policies on public.members
SELECT
policyname,
permissive,
roles,
cmd,
qual,
with_check
FROM pg_policies
WHERE schemaname = 'public'
AND tablename = 'members'
AND 'service_role' = ANY(roles);
-- Check if service_role has BYPASSRLS
SELECT rolname, rolbypassrls
FROM pg_roles
WHERE rolname = 'service_role';

View 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);

View 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;

View 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);">&copy; 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);">&copy; 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);">&copy; 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);">&copy; 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);">&copy; 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;

View 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();

View 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;

View 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);

View 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;

View 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;

View 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)';

View 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');

View 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 $$;

View 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);

View 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);">&copy; 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);">&copy; 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);">&copy; 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);">&copy; 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);">&copy; 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);">&copy; 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);">&copy; 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);">&copy; 2026 Monaco USA. All rights reserved.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</td>
</tr>
</table>
</body>
</html>'
WHERE template_key = 'dues_reminder';

View 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;

View 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';

View 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;