(modeling-commons): Durable Notifications + In-App Notifications - #48
Draft
omargfh wants to merge 20 commits into
Draft
(modeling-commons): Durable Notifications + In-App Notifications#48omargfh wants to merge 20 commits into
omargfh wants to merge 20 commits into
Conversation
- Add UserNotification keyed by (eventId, recipientId, category) for delivery idempotency - Add UserNotificationPreference as sparse per-category overrides - Add attempts and lastError to Event for poison-event handling
- Add markFailed to the event repository, incrementing attempts and storing the message - Let findUnprocessed take an attempts ceiling so poison events stop consuming batch slots - Carry attempts and lastError through the event record, domain type and mapper
- Add the delivery ledger repository, skipping inserts that hit the idempotency key - Add the notification preference repository reading a user's overrides in one query - Register both on the Dependencies interface; nothing consumes them yet
- Switch the two send sites to sendMailAsync so SMTP rejections reach the surrounding Promise.allSettled instead of awaiting a void return - Retarget the unit mock and the e2e mail spy at the same method
- Add a category catalog with per-category defaults and a sparse-override merge - Expose GET/PATCH /v1/me/notification-preferences, both session-scoped - Reject a category absent from the catalog with 400 before any write
- Resolve intents with allSettled so one failing notifier no longer discards the intents another already produced - Deliver the surviving intents, then rethrow so the event is retried
Move truncatePreview/buildEmailModel/recipient-resolution out of model-comment.service.ts into modelCommentNotifier, which returns NotificationIntent[] for the durable pipeline instead of sending mail inline. model-comment.service.ts is now purely transactional; the two e2e mail scenarios move to user-notification.feature and drain via the event processor queue, plus a new opt-out scenario.
- Write the row whenever either channel is on, so the unique key still guards against a retried event resending the email - Previously a category defaulting to email-only had no idempotency key at all
- Add the design doc and the five per-PR task files the stack was built from
…on domain user-notification.service.ts's deliver() inlined recipient eligibility, channel-suppression, and dedupe checks as silent early returns. Move each into userNotificationDomain as an explicit assertion backed by a named error (RecipientNotFoundError, RecipientDeletedError, RecipientBannedError, RecipientEmailNotFoundError, NotificationSuppressedError, RecipientEmailDisabledError, NotificationAlreadyDeliveredError), and combine the dedupe/email-channel checks into one assertEmailDeliverable call. The service now classifies caught errors via isSkippableDeliveryError instead of relying on control flow to mean "skip this recipient".
…cation domain - Expose the category catalog as the NOTIFICATION_CATEGORIES tuple behind NotificationCategory, so DTOs can derive the literal union from it - Add inAppEnabledCategories: the ledger row is written whenever any channel is on, so the feed has to re-derive which categories the user wants to see - Add assertOwnedByRecipient and NotificationNotFoundError, which 404s rather than 403s so a mark-read call cannot confirm an id exists
- findAllByRecipient takes a NotificationFeedFilters (categories, since, unreadOnly) and orders by params.orderBy, keeping Prisma types out of the port - Add findOneById and countUnread for the mark-read guard and the unread badge - Rename markNotificationRead to markRead
- listUserNotificationsQuery resolves the caller's preferences into the set of in-app categories and filters the ledger by it, so a category muted in-app is still emailed but never reaches the feed - Muting every category short-circuits to an empty page with no ledger read - unreadCount rides in the paginated response and ignores since/unreadOnly/paging, so polling the badge is one request
- markRead is idempotent and rejects a notification the caller does not own - The write stays outside transactionManager and the outbox, like markEmailSent: a read receipt carries no audit value and would feed the event processor its own noise
- GET /v1/me/notifications, paginated with since and unreadOnly filters - PATCH /v1/me/notifications/:id/read, 204 on success and 404 on another user's notification
- A comment reaching the recipient's feed, mark-read clearing the unread count, and a cross-user mark-read returning 404 - A category muted in-app is still emailed but stays out of the feed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR lands a few key features:
EventDispatcherhandles events by calling into listenersUserNotificationServicelistens to events and delegates them to module-levelNotifierNotifierreturns a list ofNotificationIntentwhen calledNotificationIntentcontains instructions on recipient and content of notificationsUserNotificationServicedelivers email/in-app notifications according to user preferencesUserNotificationServiceexposes user preferences and notifications to the frontend