-- Tier 2 Block F — reminders + broadcasts. -- -- The Tier 2 plan called this the messages pipeline. Two tables: -- -- scheduled_messages — one row per message envelope. Status moves -- scheduled -> sending -> sent (or cancelled / failed). -- message_deliveries — one row per recipient. Lets a partial-failure -- batch keep the rows that did succeed and surface -- the rest in the UI. -- -- The scheduler worker (cmd/notifier) polls scheduled_messages by -- send_at; the index supports that without a sequential scan even on -- thousands of pending rows. DO $$ BEGIN CREATE TYPE message_audience AS ENUM ('all', 'attending', 'pending', 'declined', 'maybe'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE message_channel AS ENUM ('email', 'sms', 'both'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE message_status AS ENUM ('draft', 'scheduled', 'sending', 'sent', 'cancelled', 'failed'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; CREATE TABLE IF NOT EXISTS scheduled_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE, send_at TIMESTAMPTZ NOT NULL, audience message_audience NOT NULL, channel message_channel NOT NULL, -- template_key tags the auto-seeded reminders ('reminder_7d', -- 'reminder_1d', 'reminder_dayof', 'last_call'). NULL for hand- -- composed broadcasts. template_key TEXT, subject TEXT, body TEXT NOT NULL, status message_status NOT NULL DEFAULT 'draft', sent_at TIMESTAMPTZ, recipient_count INTEGER, -- created_by is the user who scheduled or composed the message. -- NULL for system-seeded auto-reminders. created_by UUID REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- The scheduler's hot query: "what's due right now?" Partial index keeps -- it small even on events with hundreds of historical sent messages. CREATE INDEX IF NOT EXISTS idx_messages_due ON scheduled_messages (send_at) WHERE status = 'scheduled'; -- "Show me this event's communications history" — used by the -- Communications tab. Sorted newest-first so the index covers ORDER BY. CREATE INDEX IF NOT EXISTS idx_messages_event ON scheduled_messages (event_id, created_at DESC); CREATE TABLE IF NOT EXISTS message_deliveries ( message_id UUID NOT NULL REFERENCES scheduled_messages(id) ON DELETE CASCADE, guest_id UUID NOT NULL REFERENCES guests(id) ON DELETE CASCADE, status TEXT NOT NULL, -- 'pending' | 'sent' | 'bounced' | 'skipped' | 'failed' sent_at TIMESTAMPTZ, error TEXT, PRIMARY KEY (message_id, guest_id) ); -- "Which delivers succeeded for this message?" — used by the per-message -- drill-down in the UI. CREATE INDEX IF NOT EXISTS idx_deliveries_message ON message_deliveries (message_id, status);