-- M489 #5346: notifications by email, grouped. Nothing is emailed per event. -- -- New music (request_completed) goes out as at most one summary a day, at a set -- hour in each user's own timezone. Everything else is batched: a batch opens -- at the first un-emailed item and one email goes out a window later, holding -- whatever accumulated. internal/notifications/digest.go is the one sender. -- The two knobs, in admin Settings (rule 25). Singleton in the style of -- fingerprint_settings (0061). CREATE TABLE notification_email_settings ( id boolean PRIMARY KEY DEFAULT true, -- The local hour (0-23, in each user's timezone) the daily new-music -- summary goes out. summary_hour integer NOT NULL DEFAULT 9, -- How long a batch stays open after its first item before it is sent. batch_window_minutes integer NOT NULL DEFAULT 60, updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT notification_email_settings_singleton CHECK (id = true), CONSTRAINT notification_email_settings_hour_range CHECK (summary_hour >= 0 AND summary_hour <= 23), CONSTRAINT notification_email_settings_window_range CHECK (batch_window_minutes >= 15 AND batch_window_minutes <= 1440) ); INSERT INTO notification_email_settings (id) VALUES (true) ON CONFLICT (id) DO NOTHING; -- Per user, per email group: where the digest stands. A missing row is a user -- who has never had a batch open or a summary sent. CREATE TABLE user_notification_email_state ( user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, email_group text NOT NULL CHECK (email_group IN ('batch', 'summary')), -- When the open batch started. Held here rather than read off the items, -- because a coalesced item moves its created_at forward on every update -- and would otherwise keep a batch from ever coming due. batch_opened_at timestamptz, -- The last email of this group the mailer accepted. A summary is due once -- per local day, after the summary hour, if this is before it. last_sent_at timestamptz, -- A failed send backs off: nothing is tried for this group before -- retry_after, and the gap doubles with each failure in a row. failures integer NOT NULL DEFAULT 0, retry_after timestamptz, PRIMARY KEY (user_id, email_group) ); -- The digest's selection: unread rows not yet emailed. CREATE INDEX user_notifications_email_pending_idx ON user_notifications (user_id, created_at) WHERE read_at IS NULL AND emailed_at IS NULL; -- Rows written before this migration were never judged for email. Treat them -- as already handled, so the first digest after the upgrade doesn't send a -- backlog nobody asked for. UPDATE user_notifications SET emailed_at = now() WHERE emailed_at IS NULL;