docs: align flow docs with code reality + create 35 implementation issue files
Flow docs updated (11 files): - Delivery Confirmation: reversed actor roles (buyer generates, seller verifies), fixed endpoint paths (/delivery-code/generate, /delivery-code/verify) - Passkey (WebAuthn): removed stub/simulated-key claims; real @simplewebauthn/server attestation is implemented; refresh tokens are persisted - Dispute: corrected resolve schema (action enum), removed non-existent statuses, documented security gaps (no role guards on status/resolve/assign), route shadowing, all socket events are TODO stubs - Seller Offer: corrected all endpoint paths, removed 'active' status, documented withdraw dead code, missing seller history page, select-offer notification gap - Notification: corrected mark-all-read method+path, fixed GET /:id broken lookup, added unread-count-update socket event - Authentication: corrected rate limiter (counts all attempts), axios 403 not handled, deleteAccount wrong endpoint bug, changePassword no UI - Password Reset: corrected 6-digit code (not 8), documented no-complexity gap on reset-with-code vs token reset - Payment Flow DePay: /create→/save, removed phantom sub-routes, SIM_ bypass risk, PaymentProvider type gap, getProviderIntentEndpoint routing bug - Payment Flow SHKeeper: removed phantom polling endpoint, fixed release/refund paths - Purchase Request: added pending_payment/active statuses, fixed sellers/attachments endpoints, corrected socket events, PUT→PATCH bug - Escrow: documented dispute resolve does not touch escrow, route shadowing, confirm-delivery auth gap Issues created (35 files in Issues/): - 9 security issues (critical) including: dispute privilege escalation ×4, unauthenticated payment/scanner endpoints ×2, SIM_ production bypass, confirm-delivery ownership gap - 26 additional major/critical bugs covering broken endpoints, missing features, data integrity gaps, and frontend-backend mismatches Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,9 +2,11 @@
|
||||
title: Notification Flow
|
||||
tags: [flow, notification, socket-io, email]
|
||||
related_models: ["[[Notification]]", "[[User]]"]
|
||||
related_apis: ["GET /api/notifications", "PATCH /api/notifications/:id/read", "POST /api/notifications/read-all", "DELETE /api/notifications/:id"]
|
||||
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**.
|
||||
@@ -27,7 +29,7 @@ Cross-cutting flow that powers the bell icon, toast pop-ups, badge counters, and
|
||||
- **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).
|
||||
- **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).
|
||||
|
||||
@@ -58,10 +60,10 @@ Cross-cutting flow that powers the bell icon, toast pop-ups, badge counters, and
|
||||
|
||||
### Reading
|
||||
|
||||
8. User opens the bell-icon dropdown — frontend calls `POST /api/notifications/mark-read` for each viewed entry (or `POST /api/notifications/read-all`).
|
||||
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 })`.
|
||||
- Emits `notification-read` (or recomputes unread count) so other open tabs sync.
|
||||
- After updating, the backend emits `unread-count-update` to `user-{userId}` so all open tabs (and other devices) immediately sync their badge counter.
|
||||
|
||||
### Preferences
|
||||
|
||||
@@ -97,27 +99,34 @@ sequenceDiagram
|
||||
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 |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/notifications` | Paginated list with `unreadCount` |
|
||||
| `GET` | `/api/notifications/unread-count` | Just the unread count for badge |
|
||||
| `PATCH` | `/api/notifications/:id/read` | Mark single notification read |
|
||||
| `POST` | `/api/notifications/read-all` | Mark all read |
|
||||
| `DELETE` | `/api/notifications/:id` | Remove from list |
|
||||
| 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).
|
||||
@@ -131,10 +140,11 @@ sequenceDiagram
|
||||
## 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.
|
||||
- **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.
|
||||
@@ -152,4 +162,4 @@ sequenceDiagram
|
||||
- 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`)
|
||||
- Frontend: socket provider (joins `user-{id}` and listens for `new-notification` and `unread-count-update`)
|
||||
|
||||
Reference in New Issue
Block a user