Skip to content

feat(notifications): cross-channel digest subscriptions (#365) - #419

Open
BernardOnuh wants to merge 1 commit into
Neurowealth:mainfrom
BernardOnuh:feat/365-cross-channel-digest-notifications
Open

feat(notifications): cross-channel digest subscriptions (#365)#419
BernardOnuh wants to merge 1 commit into
Neurowealth:mainfrom
BernardOnuh:feat/365-cross-channel-digest-notifications

Conversation

@BernardOnuh

Copy link
Copy Markdown
Contributor

Summary

Implements Cross-Channel Digest Notifications (#365) — an opt-in daily / weekly / monthly portfolio summary delivered over a user's own channels. This PR wires WHATSAPP + WEBHOOK delivery end to end. TELEGRAM has no outbound push engine and EMAIL is a sibling issue (#367); both channel enum values are reserved but not yet wired.

A digest is a scheduled "here's how your money did this period" message (portfolio value change, yield, agent rebalances, goal progress, one risk line, notable transactions) — distinct from event-triggered alerts.

Changes Made

Schema & migration

  • Added DigestFrequency (DAILY|WEEKLY|MONTHLY) and DigestChannel (WHATSAPP|TELEGRAM|EMAIL|WEBHOOK) enums.
  • Added DigestSubscription model + User.digestSubscriptions relation.
  • Migration 20260904000000_add_digest_subscriptions + rollback. Validated with prisma migrate deploy against a local Postgres and prisma migrate diffzero digest-related drift.

Digest core (src/notifications/)

  • digest.ts — pure, deterministic buildDigest (channel-agnostic DigestModel). "Honesty discipline": insufficient snapshot coverage → null delta + caveat, never a fabricated number; no positions → short "deposit to get started" digest.
  • render.ts — per-channel rendering (WHATSAPP concise text; WEBHOOK/EMAIL return structured/null), isChannelDeliverable.
  • schedule.ts — UTC nextOccurrence, deferForQuietHours, isQuietHours; MONTHLY clamps to last day.
  • load.tsloadDigestData + periodFromFrequency (positions, snapshots, CONFIRMED txns, rebalances, computeGoalProgress).

Scheduled job (src/jobs/digests.ts)

  • Atomic per-occurrence claim (updateMany on {id, isActive, nextRunAt}) → no double-send.
  • Assemble once, deliver per channel; a failing channel never blocks others and is never rolled back.
  • Quiet hours defer, never drop; catch-up storm guard (missed runs send once, not N); all-channel hard-failure rolls nextRunAt back for retry.
  • Wired into src/index.ts startup + graceful shutdown; config.digests.intervalMs from DIGESTS_INTERVAL_MS.

Delivery path

  • digest.generated added as a socket-only event type on the alerts topic (src/events/types.ts) → durable stream + live sockets + the user's own webhook endpoints, never operator webhooks. Payload allowlist added in src/utils/api-formatters.ts.

API (src/routes/digests.ts + src/validators/digest-validators.ts)

  • POST /api/v1/notifications/digests, GET /, GET /preview, PATCH /:id, DELETE /:id.
  • Channel-link validation (WHATSAPP needs phone; WEBHOOK needs an active endpoint); WEEKLY requires weeklyDayUtc; 409 on duplicate active subscription.

Docs

  • docs/DIGESTS.md (new); docs/openapi.yaml paths + schemas; notes added to docs/WEBSOCKET_STREAMING.md and docs/USER_WEBHOOKS.md.

Test Plan

  • tests/unit/notifications/digest.test.ts (7) + schedule.test.ts (6) — 16 new tests, all pass.
  • Full suite: 1739 tests / 129 suites pass (also re-verified via the pre-push hook).
  • npx tsc --noEmit clean; npx eslint clean; prisma validate + prisma generate pass.

Breaking Changes

None — additive (new table, new enums, new socket-only event, new endpoints, new job).

Notes

  • docs/NOTIFICATIONS.md (the EMAIL-channel doc) is untouched; digest docs live in docs/DIGESTS.md to avoid clobbering the sibling channel's documentation.

Closes #365

)

Opt-in DAILY/WEEKLY/MONTHLY portfolio summaries delivered over a user's
chosen channels (WHATSAPP + WEBHOOK wired; TELEGRAM/EMAIL reserved).

- schema: DigestFrequency/DigestChannel enums + DigestSubscription model,
  migration + rollback (validated with prisma migrate diff — zero drift)
- src/notifications: pure buildDigest assembler, per-channel renderer,
  UTC schedule/quiet-hours helpers, DB/data loader
- src/jobs/digests.ts: atomic per-occurrence claim, assemble-once/deliver-
  per-channel, quiet-hour defer (never drop), catch-up storm guard
- digest.generated added as socket-only alert-topic event; delivered to the
  user's own webhook endpoints, never operator webhooks
- routes: /api/v1/notifications/digests CRUD + rate-limited preview,
  channel-link validation; Zod validators
- docs: DIGESTS.md, openapi.yaml paths/schemas, stream/webhook notes
- tests: 16 unit tests; full suite 1739 passing; tsc + eslint clean

Closes Neurowealth#365
@drips-wave

drips-wave Bot commented Sep 4, 2026

Copy link
Copy Markdown

@BernardOnuh Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cross-Channel Digest Notifications (Daily/Weekly Portfolio Summary)

2 participants