Files
nick-doc/04 - Flows/Notification Flow.md
Siavash Sameni a1f056e6a5 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>
2026-05-29 14:47:49 +04:00

10 KiB

title, tags, related_models, related_apis
title tags related_models related_apis
Notification Flow
flow
notification
socket-io
email
Notification
User
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)

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).
  • Chatchat-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 / levelslevel-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.
  • BackendNotificationService (backend/src/services/notification/NotificationService.ts), routes at /api/notifications.
  • MongoDBnotifications 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

  1. 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)).
  2. On event: increment the bell-icon badge, optionally show a toast (notistack), and prepend the entry into the cached notifications list (React Query cache).
  3. The bell-icon dropdown also fetches GET /api/notifications?page=1&limit=20 for paginated history.

Reading

  1. 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.
  2. 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.

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

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-notificationuser-{userId}. Payload includes the full notification document (so the frontend doesn't need to re-fetch).
  • unread-count-updateuser-{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-upuser-{userId} from PointsService.addPoints.
  • referral-signupuser-{referrerId} from auth verify.
  • chat-notificationuser-{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)