14 — QA Checklist (Notifications Module)
- A. Ordering & Pagination
- B. Mark-Read Semantics
- C. Tenant & Soft-Delete Isolation
- D. Auth & RBAC
- E. Announcement Overlap
- F. Event Fan-Out & Idempotency
- G. Volume & Fatigue (planned-feature gate)
- H. Client UX Checks
Verify against
notifications.controller.ts,notifications.service.ts,repositories/notifications.repository.ts,notifications.handler.ts. Shared QA baseline in 00-shared/10.
A. Ordering & Pagination
-
List returns
createdAtdesc — first page newest first (repository.ts:66). -
metamath:totalPages = max(1, ceil(total/limit)),hasNext = page < totalPages,hasPrevious = page > 1(pagination-query.dto.ts:46-54). -
limitclamp:1..100(list-notifications.dto.ts:13-19);page ≥ 1. -
Empty total →
{ data: [], meta: { totalItems: 0, totalPages: 1, hasNext: false } }— not an error. -
Insert-on-top between page fetches: offset pagination can duplicate rows across pages — client dedupes by
_id(10 §5); confirm no server crash onskip> collection. -
unreadOnly=truereturns onlyreadAt: null(service.ts:45);false/absent returns all.
B. Mark-Read Semantics
-
PATCH /:id/readsetsreadAt+ incrementsversion(repository.ts:40). -
Cross-user id →
200 null, never 404/403 and never touches the row (service.ts:57). -
Already-read id → idempotent re-mark is harmless (
findOneAndUpdate). -
read-allonly updatesreadAt: nulldocs (repository.ts:46-49). -
Race: user taps item while
read-allis in flight → both succeed; final state read. Client must not double-decrement badge (reconcile via server count, 13 §3).
C. Tenant & Soft-Delete Isolation
-
All queries pass through
scopedFilter(tenantId + deletedAt) viaBaseRepository— verify a second tenant's notifications are invisible (repository.ts:24-26,38-39,47-48,61). -
Handler writes under synthetic tenant context from the event (
handler.ts:81-99) — tenantId must come from the event, never ambient state. - Soft-deleted recipient docs: notifications still queryable by id (hard delete not cascaded) — document intended behaviour.
D. Auth & RBAC
-
All 4 routes reject unauthenticated requests (
controller.ts:16). -
Gap:
notification.read/notification.updatedeclared (permissions.constants.ts:32-33) but no RBAC guard attached — confirm intended (JWT-only for v1) and log decision. -
(planned)
notification.sendmust be added toALL_PERMISSIONSbefore any send endpoint ships.
E. Announcement Overlap
-
AnnouncementPublished(announcement.service.ts:80-91) does not create inbox notifications today — confirm product intent (separate read receiptsannouncement.service.ts:95-102vs inbox). If inbox fan-out is wanted, map the event inEVENT_TYPE_MAPand define recipient expansion (audience resolution is inannouncement.service.ts:109-148).
F. Event Fan-Out & Idempotency
-
Handler is
onAny— only mapped types create notifications; unknown events are no-ops (handler.ts:71-74). Adding a new event requires an explicit map entry (feature toggle, 09 §5). -
Gap:
NotificationCreatedEventinterface exists (events/notification-events.ts:1-10) but is never emitted — either emit it (audit/websocket use) or delete it. -
Gap:
event-queue-map.ts:10-40routes ~18 events to thein-appBullMQ queue with no worker — creating notifications for them is (planned); do not half-wire (queue without worker = silent loss, violatesNotifications.md:57). -
Retry/DLQ for notification creation: handler failures are logged only (
handler.ts:62-67) — no retry today. Acceptable for in-process events; revisit with queue-based fan-out. -
Duplicate events (e.g. double
EmailVerifiedemit) create duplicate notifications — document idempotency key if dedupe is required.
G. Volume & Fatigue (planned-feature gate)
-
Before fanning out homework/exam/fee events, ship per-type preferences (
Notifications.md:27-28) — see 09 §5. -
Coaching push (test reminders, DPP, batch start) is gated on mobile-app decision (
IMPLEMENTATION_PLAN.md:859).
H. Client UX Checks
-
Badge ≤ 99+ display; hidden on
loaded=false(06 §2). - Badge agrees with list unread count on inbox open.
- Offline first-load → error panel + retry; pagination failure → footer retry, list intact (06 §1.6).
- a11y: unread suffix semantics, badge label, contrast (00-shared/09).
-
Analytics events
{module}.{screen}.{action}wired (proposed) (00-shared/10 §8).