Files
baya-monorepo/archive/docs/flows/notifications.md
T
2026-08-02 20:01:31 +03:30

12 KiB

Flow — notifications

Last verified: 2026-08-02 against commit c841bde

Actor(s): customer · nurse (admin/partner have no bell) · Status: partial Client: real · Server: real Business source: product/business/14-notifications-and-admin.md Integration: docs/integration/domains/notifications.md

What it does

Every state change another domain makes — a request arrives, a payment captures, a refund settles, a ticket gets a reply — leaves a record the user can find later. A bell in the header carries the unread count; tapping it opens a day-grouped feed; tapping a row marks it read and, when the type is one the client recognises, jumps to the thing it is about. In-app only: there is no push, SMS or email channel, and nothing is dispatched client-side — every notification is server-raised.

Screens

Step Route Component / notes
Bell (customer) any (customer) route NotificationBell role="customer" mounted in CustomerLayout.tsx:34 — badge from the polled count; always navigates, never opens a popover
Bell (nurse) any /nurse/* route same, NurseLayout.tsx:74
Feed (customer) /fa/notifications NotificationCenter role="customer" — day groups «امروز» / «دیروز» / «این هفته» then Shamsi dates, «نمایش بیشتر» grows page 1, «علامت‌گذاری همه…» bulk-clears
Feed (nurse) /fa/nurse/notifications same component, role="nurse"
Feed (admin) /fa/admin/notifications PlaceholderScreen stub — icon + generic body, no data (page.tsx). Unreachable: AdminLayout renders no bell and nothing links to it
Row NotificationRow.tsx — navigable rows are a ButtonBase with a chevron; kind: 'none' rows render as a static surface (no ripple, no chevron) but still mark read

API

Call Endpoint Notes
list GET /notifications/get_notifications unread-first then newest-first (NotificationService.cs:24); client grows pageSize instead of paging (useNotifications.ts)
badge GET /notifications/get_unread_count own cheap query; polled every 60 s, stale 45 s (constants.ts:25-27), gated on auth
mark one POST /notifications/mark_notification_read optimistic — flips the row and decrements the badge, rolls back on error (useMarkNotificationRead.ts)
mark all POST /notifications/mark_all_read optimistic, zeroes the badge; bulk ExecuteUpdate server-side

Chain traced end to end: NotificationCenter.tsx → hooks → apis/index.ts (USE_NOTIFICATIONS_MOCK = false) → clientApi.ts:45-73NotificationsController.csListMyNotifications / GetUnreadCount / MarkNotificationRead / MarkAllRead handlers. No phantom, no gap. Shapes live in the integration doc.

The type vocabulary — probed live

The server emits 14 type codes. parse.ts recognises 3 of them; the other 11 fall to { kind: 'none' } and render as untappable rows.

Server type Raised by To dataJson Client verdict
booking_confirmed ConfirmPaymentAndPostLedger.Handler.cs:109, SettleBnplOrder.Handler.cs:142 customer booking_id /bookings/{id}
refund_completed ConfirmRefundSettlement.Handler.cs:70 customer booking_id, refund_id /bookings/{id}/refund
ticket_message PostMessage.Handler.cs:73 ticket participants ticketId → the thread
booking_confirmed_nurse ConfirmPaymentAndPostLedger.Handler.cs:117 nurse booking_id unknown type — the nurse's own "you have a paid booking" is dead
booking_request_received CreateBookingRequest.Handler.cs:97 nurse booking_request_id unknown type and unknown key
booking_request_accepted AcceptBookingRequest.Handler.cs:57 customer booking_request_id, payment_deadline_at — the pay-now prompt does not link to checkout
booking_request_rejected RejectBookingRequest.Handler.cs:45 customer booking_request_id
booking_request_expired_no_response ExpireBookingRequests.Handler.cs:26 customer booking_request_id
booking_request_payment_window_expired ExpireBookingRequests.Handler.cs:35 customer booking_request_id
evv_location_mismatch CheckInVisit.Handler.cs:105 customer booking_id, session_id type unknown (the id would have resolved)
evv_no_show DetectNoShowSessions.Handler.cs:67 customer booking_id, session_id same
refund_issued CreateRefund.Handler.cs:256 customer booking_id, refund_id client knows refund_processed, not refund_issued
review_moderated ModerateReview.Handler.cs:54 customer reviewId client knows review_published, and keys off nurse_profile_id
verification_expiry_prompt ScanExpiringCredentials.Handler.cs:81 nurse (none) — no payload at all, so nothing to link to

Ten codes go the other way: parse.ts:21-43 handles booking_reminder, session_reminder, payment_captured, booking_cancelled, refund_processed, payout_paid, payout_failed, ticket_opened, review_published, ticket_closedno server code emits any of them. Consequently the payout and nurse_profile branches of deepLink.ts are unreachable, and a nurse never gets a payout notification at all.

Rules that must hold

Rule Value Source
In-app only; no push at launch NotificationChannel.Sms/Push exist in the enum, nothing implements them business/14 §MVP · INotificationDispatcher.cs
Read notifications hard-deleted after 90 days; unread never deleted RetentionDays = 90, swept every 24 h — a hardcoded constant, not a platform_configs row NotificationRetentionJob.cs; business/14 §(a)
A notification never exists for a rolled-back transaction the dispatcher self-commits its own row and is called after the handler's CommitAsync InAppNotificationDispatcher.cs; e.g. ExpireBookingRequests.Handler.cs:76-85
Every read is scoped to the caller userId from ICurrentUser; no by-id fetch exists NotificationService.cs:24,38
Unread count is its own query, never derived from a page badge must be right without a list fetch integration doc
Adding a type is safe; changing a type's dataJson is not the client keys deep-links off the payload and degrades to non-tappable integration doc

How to test

  1. Log in as 09120000010 (سارا محمدی, customer) — see testing-setup.md.
  2. Look at the header bell. Expect a numeric badge. Live at this stamp: GET /notifications/get_unread_count{"count":10} (the seeded 6 plus requests other testers raised today, so treat the number as ≥ 6, not exactly 6).
  3. Tap the bell → /fa/notifications. Expect total: 12, unread-first, grouped under «امروز» / «دیروز» / «این هفته» / Shamsi day headers, each unread row with a dot and a tinted icon.
  4. Find the top row — booking_confirmed, {"booking_id":10}. Expect a chevron; tapping it navigates to /fa/bookings/10 and the badge drops by one immediately (optimistic).
  5. Find any booking_request_accepted / evv_no_show row. Expect the opposite: neutral grey icon, no chevron, tap does nothing but mark read. That is the type-vocabulary gap, working as coded.
  6. Tap «علامت‌گذاری همه به‌عنوان خوانده‌شده». Expect the badge to hit 0 instantly and stay 0 after a refresh. This is destructive to the shared demo DB — the unread count does not come back. Skip it unless you own the world.
  7. Nurse side: log in as 09120000001 and open /fa/nurse/notifications. Expect 9 rows, mostly booking_request_receivedall of them untappable, and the two booking_confirmed_nurse rows untappable too.
  8. Admin: /fa/admin/notifications renders a placeholder. There is no link to it and no bell in the admin shell; reaching it means typing the URL.

Everything above except step 6 was executed live against :5002 with pre-minted bearer tokens on 2026-08-02.

Known gaps

  • 11 of the 14 server notification types are unknown to parse.ts, so their rows are untappable; only booking_confirmed, refund_completed and ticket_message deep-link.
  • booking_confirmed_nurse carries a valid booking_id but no client branch — the nurse's most important notification cannot be opened (parse.ts:21-26).
  • All five booking_request_* types carry booking_request_id, a key parse.ts never reads and NotificationData has no kind for — the whole request lifecycle is non-navigable, including the "pay within the window" prompt.
  • refund_issued vs the client's refund_processed, and review_moderated vs review_published: near-miss names on both sides of the same contract.
  • verification_expiry_prompt is dispatched with DataJson = null — even a matching client branch could not route it (ScanExpiringCredentials.Handler.cs:81).
  • The client handles 10 types no server code emits; the payout and nurse_profile deep-link classes in deepLink.ts (and their icons/tints) are dead code. No payout notification is ever raised, so the nurse is never told a payout paid or failed.
  • Every title and body is a server-side English literal ("Booking confirmed", "The nurse declined your request.") rendered verbatim into the Persian RTL feed — NotificationRow.tsx:62,67, no i18n path. Only the chrome (headers, buttons, empty state) is translated.
  • /fa/admin/notifications is a PlaceholderScreen, is not linked from anywhere, and no admin/partner shell mounts a bell — admins have no notification surface at all.
  • NotificationBellPopover.tsx (4.4 KB, full mark-read/deep-link logic) is exported but mounted nowhere — dead UI since the desktop branch was removed.
  • The list is not real pagination: useNotifications grows pageSize on page 1, so "load more" refetches the whole feed each time.
  • Notification retention (90 d / 24 h) is a hardcoded constant, not a platform_configs row, contrary to the repo's config-is-rows convention.
  • The two notification page.tsx files are 'use client' with no generateMetadata, departing from the thin-RSC-page convention in client/CLAUDE.md.
  • No unread-notification cap or archive: the feed grows until the 90-day sweep, and unread rows are never swept.