Remaining docs updated to match code (the docs that the first pass had not covered):
- Flows: Chat, Referral, Rating, Registration, Google OAuth, Negotiation, Payout,
Trezor Safekeeping — corrected endpoints, socket events, status enums, auth gaps
- API Reference: User API, Trezor API — admin route prefix/verb/status corrections,
added undocumented endpoints (ton-proof challenge, profile email verify,
GET /trezor/account, POST /trezor/verify-operation)
- Data Models: Chat, Notification, Payment, PointTransaction, User — corrected
enums (PaymentProvider, escrowState, PointTransaction.type, User.status),
90-day notification TTL, soft-delete semantics, wallet fields
Trezor "zero frontend" finding (audit C31/C32) corrected as STALE:
- Verified current code HAS a full frontend Trezor implementation (admin/trezor
page, TrezorSettingsView, trezorConnector via @trezor/connect-web,
TrezorSignDialog, actions/trezor.ts building the {message,signature} object)
- Fixed Trezor Safekeeping Flow doc (removed false "no frontend" warnings)
- Reclassified ISSUE-012 as invalid/superseded with explanation
Issue set reconciled to a single canonical numbering (ISSUE-001..054):
- Adopted the comprehensive 51-issue set (long-slug, fully indexed)
- Removed 35 superseded short-slug duplicates from the first pass
- Removed a duplicate ISSUE-046 file
- Added 3 issues the 51-set lacked: ISSUE-052 (completed-not-counted-in-stats),
ISSUE-053 (axios 401-only interceptor), ISSUE-054 (rate limiter counts all attempts)
- Regenerated Issues Index: 53 open (14 critical, 39 major) + 1 invalid
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
170 lines
10 KiB
Markdown
170 lines
10 KiB
Markdown
---
|
|
title: Notification Flow
|
|
tags: [flow, notification, socket-io, email]
|
|
related_models: ["[[Notification]]", "[[User]]"]
|
|
related_apis: ["GET /api/notifications", "PATCH /api/notifications/:id/read", "PATCH /api/notifications/mark-all-read", "DELETE /api/notifications/:id"]
|
|
---
|
|
|
|
> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md))
|
|
|
|
# Notification Flow
|
|
|
|
Cross-cutting flow that powers the bell icon, toast pop-ups, badge counters, and (optionally) email digests. Notifications are created by many services and travel to the user via both **MongoDB persistence** and **Socket.IO push**.
|
|
|
|
## Trigger sources
|
|
|
|
- **Purchase request lifecycle** — created, status changed, cancelled (`PurchaseRequestService`).
|
|
- **Offer lifecycle** — new offer, accepted, rejected, withdrawn (`SellerOfferService`).
|
|
- **Payment lifecycle** — confirmed, refunded, payout sent (`shkeeperWebhook`, `PurchaseRequestService.notifyPaymentConfirmed`).
|
|
- **Chat** — `chat-notification` events (see [[Chat Flow]]).
|
|
- **Dispute** — created, assigned, resolved (TODO in `DisputeService`; the chat itself notifies).
|
|
- **Delivery** — code generated, delivery confirmed (`DeliveryService`).
|
|
- **Referral** — sign-up via referral code (`AuthController.verifyEmailWithCode`, `googleSignUp`).
|
|
- **Points / levels** — `level-up` event when crossing a tier (`PointsService.addPoints:91-99`).
|
|
- **Admin actions** — e.g. dispute resolution, manual payouts.
|
|
|
|
## Actors
|
|
|
|
- **System** — the various services calling `NotificationService.createNotification`.
|
|
- **User** — the recipient.
|
|
- **Frontend** — bell-icon dropdown and toast subscribers in `frontend/src/layouts/components/notifications-drawer/` and the global socket provider.
|
|
- **Backend** — `NotificationService` (`backend/src/services/notification/NotificationService.ts`), routes at `/api/notifications`.
|
|
- **MongoDB** — `notifications` collection (one document per notification). Notifications are **auto-deleted after 90 days** (TTL index on `createdAt`).
|
|
- **Socket.IO** — emits `new-notification` to `user-{userId}`.
|
|
- **Email** (optional) — periodic digest worker (not implemented today; planned).
|
|
|
|
## Step-by-step narrative
|
|
|
|
### Creating a notification
|
|
|
|
1. Any service builds a `NotificationCreateData` object:
|
|
```
|
|
{
|
|
userId, title, message,
|
|
type: 'info' | 'success' | 'warning' | 'error',
|
|
category: 'purchase_request' | 'offer' | 'payment' | 'delivery' | 'system',
|
|
relatedId?, metadata?, actionUrl?
|
|
}
|
|
```
|
|
2. Calls `await notificationService.createNotification(data)`.
|
|
3. `NotificationService.createNotification` (`NotificationService.ts:18-37`):
|
|
- `Notification.create({ ...data, isRead: false, createdAt: now })`.
|
|
- Calls `this.emitRealTimeNotification(userId, saved)` which `global.io.to('user-${userId}').emit('new-notification', payload)`.
|
|
4. The notification is persisted and pushed simultaneously.
|
|
|
|
### Frontend reception
|
|
|
|
5. The frontend's global Socket.IO provider listens for `new-notification` events on its `user-{me}` room (joined on app mount via `socket.emit('join-user-room', userId)`).
|
|
6. On event: increment the bell-icon badge, optionally show a toast (`notistack`), and prepend the entry into the cached notifications list (React Query cache).
|
|
7. The bell-icon dropdown also fetches `GET /api/notifications?page=1&limit=20` for paginated history.
|
|
|
|
### Reading
|
|
|
|
8. User opens the bell-icon dropdown — frontend calls `PATCH /api/notifications/:id/read` for each viewed entry, or `PATCH /api/notifications/mark-all-read` to clear all at once.
|
|
9. `NotificationService.markAsRead(notificationId, userId)` (`NotificationService.ts:74-90`):
|
|
- `Notification.findOneAndUpdate({ _id, userId }, { isRead: true, readAt: now })`.
|
|
- After updating, the backend emits `unread-count-update` to `user-{userId}` so all open tabs (and other devices) immediately sync their badge counter.
|
|
|
|
### Purchase request status coverage gap
|
|
|
|
`NotificationService.notifyRequestStatusChanged` handles many purchase-request statuses but does **not** emit notifications for `pending_payment` or `seller_paid`. If a buyer moves to `pending_payment` or a seller is marked `seller_paid`, no notification is created. This is a known coverage gap; add dedicated helper methods (or extend the switch-case) if those transitions need to surface to recipients.
|
|
|
|
### Preferences
|
|
|
|
- `User.preferences.notifications` (in the User schema) can hold per-category opt-outs (`emailNotifications`, `pushNotifications`, etc.). The current implementation does not enforce preferences at send-time — all enabled notifications fire. Add a check in `createNotification` to short-circuit when the user has opted out of a category.
|
|
|
|
### Email digest (planned)
|
|
|
|
- A scheduled worker should `Notification.find({ userId, emailDigested: false, createdAt: { $gte: yesterday } })`, batch by user, render a digest email via `emailService`, mark `emailDigested: true`. Not implemented today.
|
|
|
|
## Sequence diagram
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
autonumber
|
|
participant Svc as Originating Service<br/>(SellerOfferService / Webhook / ...)
|
|
participant NS as NotificationService
|
|
participant DB as MongoDB
|
|
participant IO as Socket.IO
|
|
participant FE as Frontend
|
|
actor U as User
|
|
|
|
Svc->>NS: createNotification({userId, title, message, ...})
|
|
NS->>DB: Notification.create
|
|
NS->>IO: emit user-{userId} 'new-notification'
|
|
IO-->>FE: 'new-notification' payload
|
|
FE-->>U: badge++, toast, prepend to list
|
|
|
|
U->>FE: open bell dropdown
|
|
FE->>NS: GET /api/notifications?page=1&limit=20
|
|
NS->>DB: Notification.find({userId}).sort({createdAt:-1})
|
|
NS-->>FE: { notifications, total, unreadCount }
|
|
|
|
U->>FE: click notification
|
|
FE->>NS: PATCH /api/notifications/{id}/read
|
|
NS->>DB: Notification.findOneAndUpdate(isRead:true)
|
|
NS->>IO: emit user-{userId} 'unread-count-update'
|
|
IO-->>FE: badge sync across tabs
|
|
FE-->>U: badge--, mark item as read
|
|
FE-->>U: navigate to notification.actionUrl
|
|
```
|
|
|
|
## API calls
|
|
|
|
| Method | Endpoint | Purpose | Notes |
|
|
|---|---|---|---|
|
|
| `GET` | `/api/notifications` | Paginated list with `unreadCount` | |
|
|
| `GET` | `/api/notifications/unread-count` | Just the unread count for badge | |
|
|
| `GET` | `/api/notifications/:id` | Single notification | ⚠️ **Known bug** — see below |
|
|
| `PATCH` | `/api/notifications/:id/read` | Mark single notification read | |
|
|
| `PATCH` | `/api/notifications/mark-all-read` | Mark all notifications read | Previously documented incorrectly as `POST /api/notifications/read-all` |
|
|
| `DELETE` | `/api/notifications/:id` | Remove from list | |
|
|
|
|
> ⚠️ **Known bug — `GET /api/notifications/:id`**: The backend controller does **not** perform a direct DB lookup by ID. Instead it calls `getUserNotifications(userId, 1, 1)` (fetches only 1 record for the user) and then does an in-memory `_id` comparison. Any notification that is not the user's single most-recent record will return `404` erroneously. Do not rely on this endpoint for arbitrary notification lookups until the controller is fixed to use a direct `findOne({ _id, userId })`.
|
|
|
|
## Database writes
|
|
|
|
- **`notifications`** — insert on create, update on read, delete on remove.
|
|
- **TTL**: notifications are automatically deleted after **90 days** via a MongoDB TTL index on `createdAt`.
|
|
|
|
## Socket events emitted
|
|
|
|
- **`new-notification`** → `user-{userId}`. Payload includes the full notification document (so the frontend doesn't need to re-fetch).
|
|
- **`unread-count-update`** → `user-{userId}`. Emitted whenever the unread count changes (e.g. after `markAsRead` or `markAllRead`). Used for cross-tab and cross-device badge synchronisation. There is **no** `notification-read` event — `unread-count-update` is the correct event to listen to for badge sync.
|
|
- **`level-up`** → `user-{userId}` from `PointsService.addPoints`.
|
|
- **`referral-signup`** → `user-{referrerId}` from auth verify.
|
|
- **`chat-notification`** → `user-{participantId}` from `ChatService.sendMessage` (these are not stored in the `notifications` collection — they live alongside but drive only the chat-list badge).
|
|
|
|
## Side effects
|
|
|
|
- Bell badge count is derived from `unreadCount` returned by the GET endpoint or computed client-side as items arrive.
|
|
- Notification actions deep-link via `actionUrl` (e.g. `/dashboard/buyer/requests/{id}`).
|
|
- Sentry breadcrumbs capture failed notification creations.
|
|
|
|
## Error / edge cases
|
|
|
|
- **User offline** → notification is persisted in MongoDB; the user sees it when they next sign in. The socket emit is lossy (no replay).
|
|
- **Multiple tabs / devices** → the same `user-{id}` room receives the event in each socket; all tabs update. Badge sync is driven by `unread-count-update`, not a per-item `notification-read` event.
|
|
- **Disabled categories** (planned) → service should early-return without DB write if the user has opted out, otherwise persist but don't push (so it shows in history but not as a toast).
|
|
- **High volume** (e.g. fan-out to thousands of sellers) → today every notification is a separate Mongo insert + socket emit. For mass announcements, consider `insertMany` + per-room broadcast. The 50ms stagger in [[Purchase Request Flow]] mitigates the worst case.
|
|
- **Stale unread count** → if the frontend trusts a stale React Query cache, it can show wrong numbers; always reconcile against `unread-count` on bell-icon open.
|
|
- **90-day TTL** → notifications older than 90 days are silently removed from MongoDB. The frontend should not assume a notification persists indefinitely.
|
|
|
|
> [!tip] Always set `actionUrl`
|
|
> Every notification should have a deep-link target. Notifications without `actionUrl` lead to dead clicks. The factory methods in `NotificationService` (e.g. `notifyNewOfferReceived`) already enforce this — keep the pattern when adding new helpers.
|
|
|
|
## Linked flows
|
|
|
|
- Every other flow in this folder emits notifications via this service.
|
|
- [[Chat Flow]] — separate `chat-notification` socket channel for chat badges.
|
|
|
|
## Source files
|
|
|
|
- Backend: `backend/src/services/notification/NotificationService.ts`
|
|
- Backend: `backend/src/services/notification/notificationController.ts`
|
|
- Backend: `backend/src/services/notification/notificationControllerRoutes.ts`
|
|
- Backend: `backend/src/services/notification/routes.ts`
|
|
- Backend: `backend/src/models/Notification.ts`
|
|
- Frontend: `frontend/src/layouts/components/notifications-drawer/`
|
|
- Frontend: socket provider (joins `user-{id}` and listens for `new-notification` and `unread-count-update`)
|