Files
hackerspace.sh/scripts/041_notifications.sql
T
Moheeb Zara ddb790abb9 feat(notifications): P2a outbox table + pure dues-email logic
Migration 041 + schema.sql section 26: notifications outbox (service-only
writer, admin/board/treasurer SELECT, member self-view via validated
service action like member_billing; (space_id, dedupe_key) unique makes
the webhook enqueue idempotent). lib/notifications-logic.ts is pure:
deterministic dedupe keys, dues email rendering (HTML-escaped, brand-
neutral), terminal-attempt rule. 13 tests. Inert until P2c wires it.
2026-05-17 04:49:03 -07:00

53 lines
2.9 KiB
SQL

-- =============================================================================
-- 041: Transactional notifications outbox (Product spine Phase 2)
-- =============================================================================
-- Dues-lifecycle emails (renewal receipt, payment-failed, lapse-to-late) are
-- enqueued by the Stripe webhook and sent by a separate dispatcher cron. The
-- webhook NEVER sends inline: it only writes an idempotent outbox row, so the
-- money path stays fast and safe to retry (a duplicate enqueue is a no-op via
-- the (space_id, dedupe_key) unique index).
--
-- * notifications: one row per message. Service client (webhook enqueue +
-- dispatcher) is the only writer; SELECT = admin/board/treasurer (mirrors
-- member_billing / payments visibility). Member self-view of their own
-- notifications is a validated service-client action, not an RLS path
-- (same convention as getMyBilling).
--
-- Idempotent: CREATE TABLE/INDEX IF NOT EXISTS, DROP POLICY IF EXISTS then
-- CREATE. Apply as-is (Supabase SQL editor / psql); re-runs are no-ops.
-- =============================================================================
CREATE TABLE IF NOT EXISTS public.notifications (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
member_id uuid REFERENCES public.space_members(id) ON DELETE CASCADE,
type text NOT NULL,
channel text NOT NULL DEFAULT 'email',
recipient text NOT NULL,
subject text NOT NULL,
body_html text NOT NULL,
body_text text NOT NULL,
status text NOT NULL DEFAULT 'pending',
attempts integer NOT NULL DEFAULT 0,
last_error text,
dedupe_key text NOT NULL,
sent_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_notifications_dedupe ON public.notifications (space_id, dedupe_key);
CREATE INDEX IF NOT EXISTS idx_notifications_pending ON public.notifications (created_at) WHERE status = 'pending';
CREATE INDEX IF NOT EXISTS idx_notifications_member ON public.notifications (space_id, member_id);
ALTER TABLE public.notifications ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS notifications_select ON public.notifications;
CREATE POLICY notifications_select ON public.notifications FOR SELECT
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
-- No INSERT/UPDATE/DELETE policy: the validated service-client webhook enqueue
-- and dispatcher cron are the only writers.
DROP TRIGGER IF EXISTS trg_notifications_touch ON public.notifications;
CREATE TRIGGER trg_notifications_touch
BEFORE UPDATE ON public.notifications
FOR EACH ROW EXECUTE FUNCTION public.touch_updated_at();