feat(notifications): P2e member self-view on /me + docs

getMyNotifications (service-client self-view, mirrors getMyBilling) +
a read-only Notifications section on /me. Docs updated in the same
change: ARCHITECTURE (Phase 2 subsystem + limitation #5), API_REFERENCE
(notifications section, fixed the stale no-auth-middleware claim),
DATABASE_SCHEMA + DB_SCHEMA_MAP (migration 041 + table), DEPLOYMENT
(env vars + dispatcher crontab), HANDOFF pass-45.
This commit is contained in:
Moheeb Zara
2026-05-17 04:58:46 -07:00
parent 75097a601c
commit 7eb83676e7
9 changed files with 138 additions and 4 deletions
+23 -1
View File
@@ -645,6 +645,28 @@ policy** (webhook/service-client only); the member self-view is a validated
action. No new permission code. Phases 2-3 (transactional notifications,
broader self-serve) build on this.
### Transactional notifications (migration 041; product spine Phase 2)
Outbox + dispatcher, not inline send. The Stripe webhook only writes a
`notifications` row (`invoice.paid` → `dues_renewed`, `invoice.payment_failed`
→ `dues_payment_failed`, lapse-to-`late` → `dues_lapsed`); it never calls the
mail provider, so the money path stays fast and retry-safe. The
`(space_id, dedupe_key)` unique index makes a Stripe event replay a no-op
(`ignoreDuplicates`). Pure render + dedupe logic in
`lib/notifications-logic.ts` is unit-tested (HTML-escaped, brand-neutral
copy). `lib/email/send.ts` is a one-function transport seam over Resend's
HTTP API (no SDK; `fetch`) returning a `retryable` flag; a self-hosted
deploy can swap it for SMTP without touching callers, and an unset
`RESEND_API_KEY` is a clean non-retryable no-op. The dispatcher
`POST /api/cron/notifications` (constant-time `CRON_SECRET` bearer, no
session — `proxy.ts` whitelists `/api/cron`) drains ≤20 pending rows per
run, passing each row id as Resend's `Idempotency-Key` so an overlapping
run cannot double-send; transient failures stay `pending` until the attempt
budget is spent, permanent ones go `failed`. `notifications` SELECT =
admin/board/treasurer, no client write policy; the member self-view
(`getMyNotifications`, surfaced on `/me`) is a validated action. The
droplet's crontab POSTs once a minute. No new permission code.
---
## 13. Known Limitations
@@ -653,6 +675,6 @@ broader self-serve) build on this.
2. **Payment integrations** - Manual import/CSV reconciliation is the primary path. A PayPal sync endpoint exists (`app/api/paypal/sync/route.ts`); Stripe/other live APIs are not integrated. See `docs/` and the Payments module section.
3. **Social auth** - GitHub/Google sign-in is wired, gated by the `NEXT_PUBLIC_OAUTH_GITHUB`/`NEXT_PUBLIC_OAUTH_GOOGLE` env flags.
4. **Webhooks** - The HMAC signing contract and secret rotation exist; per-event delivery is not implemented (see `docs/WEBHOOKS.md`).
5. **Email notifications** - Not implemented.
5. **Email notifications** - Dues-lifecycle transactional email is implemented (migration 041: notifications outbox + Resend transport + dispatcher cron). Other domains (bookings, forms) are not wired yet but reuse the same outbox.
6. **Search** - Client-side filtering only, no server-side full-text search.
7. **Single space per user** - The auth resolver assumes one active membership per user; a user in 2+ spaces is not supported (no space switcher). Fails closed.