Files
silo-server/migrations/sql/20260612100000_request_lifecycle_notifications.sql
bcf0253c09 feat(notifications): notify requesters of request status changes (#143)
Requests previously only notified the community server channels for
submitted/approved/declined and the requester personally for fulfilled.
This closes the gap and makes request posts addressable:

- New request.approved / request.declined delivery types ride the
  operational dispatch path to the requesting profile: inbox, websocket
  toast, email, Discord DM, personal webhooks (gated by the existing
  notify_requests flag), and web push. Submitted stays broadcast-only
  (the requester performed the action themselves). Title/year/decline
  reason travel in reason_flags since no catalog item exists yet.
- Request status notices are transactional: digest-mode recipients get
  an off-schedule early send (watermark-durable, last_digest_at left
  alone) instead of waiting for the digest hour. Per-episode recipients
  were already immediate via the dispatch nudge.
- At-most-once per (profile, request, type) via a partial unique index
  (migration 20260612100000), mirroring the fulfilled dedupe.
- Server-channel Discord request posts can @mention the requester via
  their OAuth-linked identity (notifications.server_channels.
  mention_requesters, default off). Resolved lazily in the sweep worker
  only when a Discord destination is about to receive the event; the
  ping uses content-level mention with pinned allowed_mentions, and the
  Discord identity never leaks into generic webhook payloads.

Android/Apple clients render the new inbox types with their generic
fallback until they add them.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:20:55 -04:00

17 lines
797 B
SQL

-- +goose Up
-- +goose StatementBegin
-- request.approved / request.declined notifications: notify the requesting
-- profile when an admin (or auto-approval) resolves their request.
-- At-most-once per (profile, request, type): the operational insert path uses
-- ON CONFLICT DO NOTHING, so an API retry or multi-node race dedupes here
-- instead of double-notifying. Mirrors the request.fulfilled index.
CREATE UNIQUE INDEX notification_deliveries_profile_request_lifecycle_key
ON public.notification_deliveries (profile_id, (reason_flags->>'request_id'), type)
WHERE type IN ('request.approved', 'request.declined');
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS public.notification_deliveries_profile_request_lifecycle_key;
-- +goose StatementEnd