-- Reminder system redesign (t-paliad-064). -- -- Three things land here, in service of the new "zero-overdue SLO" model: -- -- 1. paliad.users gets two new columns: -- * reminder_warning_offset_days — how many days before each deadline -- the +N-day warning email fires. Default 7 matches the prior -- Monday-weekly-digest behaviour. Per-user customisable on the -- Settings → Notifications page. -- * escalation_contact_id — optional pointer to another paliad.users -- row that should also be cc'd on overdue / DRINGEND emails. NULL -- means "fall back to global_admins" (the default escalation -- channel). The Settings UI dropdown is a follow-up task; the -- column ships now to avoid a second migration when wiring lands. -- -- 2. paliad.reminder_log gains slot + slot_date for digest dedup. The new -- service writes one row per (user_id, slot, slot_date), enforced by a -- partial UNIQUE index. Legacy per-deadline rows (slot IS NULL) coexist -- and are ignored by the new dedup index — they're kept on disk for -- audit and a separate housekeeping migration will prune them once the -- new path has soaked. -- -- 3. The reminder_type CHECK constraint widens to admit the two new -- digest values ('morning_digest', 'evening_digest'). Existing values -- ('overdue', 'tomorrow', 'weekly') stay valid so the legacy code path -- can run alongside during deploy if needed. -- -- All operations are idempotent (IF NOT EXISTS on columns and index; -- DROP/RE-ADD on the named constraints) so a re-run is safe. -- 1) Per-user warning offset (default 7). ALTER TABLE paliad.users ADD COLUMN IF NOT EXISTS reminder_warning_offset_days INT NOT NULL DEFAULT 7; -- Range check: 1..30. A 0-day "warning" is just the same as the morning -- reminder, and 30+ days is so far out the email becomes noise. ALTER TABLE paliad.users DROP CONSTRAINT IF EXISTS users_reminder_warning_offset_check; ALTER TABLE paliad.users ADD CONSTRAINT users_reminder_warning_offset_check CHECK (reminder_warning_offset_days BETWEEN 1 AND 30); -- 2) Optional escalation contact. ON DELETE SET NULL so deleting the -- chosen escalation user doesn't cascade-delete the source user — they -- just lose their override and fall back to global_admins. ALTER TABLE paliad.users ADD COLUMN IF NOT EXISTS escalation_contact_id UUID REFERENCES paliad.users(id) ON DELETE SET NULL; -- A user can't be their own escalation contact (would just bounce mail -- back to themselves on overdue). ALTER TABLE paliad.users DROP CONSTRAINT IF EXISTS users_escalation_contact_self_check; ALTER TABLE paliad.users ADD CONSTRAINT users_escalation_contact_self_check CHECK (escalation_contact_id IS NULL OR escalation_contact_id <> id); -- 3) Slot-based dedup on reminder_log. ALTER TABLE paliad.reminder_log ADD COLUMN IF NOT EXISTS slot TEXT, ADD COLUMN IF NOT EXISTS slot_date DATE; -- slot must be 'morning' | 'evening' when present (NULL = legacy row). ALTER TABLE paliad.reminder_log DROP CONSTRAINT IF EXISTS reminder_log_slot_check; ALTER TABLE paliad.reminder_log ADD CONSTRAINT reminder_log_slot_check CHECK (slot IS NULL OR slot IN ('morning', 'evening')); -- Widen the reminder_type CHECK to admit the new digest values. Keeps the -- legacy values valid so coexistence during deploy is harmless. ALTER TABLE paliad.reminder_log DROP CONSTRAINT IF EXISTS reminder_log_reminder_type_check; ALTER TABLE paliad.reminder_log ADD CONSTRAINT reminder_log_reminder_type_check CHECK (reminder_type IN ( 'overdue', 'tomorrow', 'weekly', 'morning_digest', 'evening_digest' )); -- Partial unique index — one digest row per (user, slot, local-date). -- Partial on slot IS NOT NULL so legacy rows (which have NULL slot) don't -- conflict with the new model. CREATE UNIQUE INDEX IF NOT EXISTS reminder_log_slot_dedup_idx ON paliad.reminder_log (user_id, slot, slot_date) WHERE slot IS NOT NULL;