F 13 Notifications
Status: ๐ข ready to test Owner: Adilet Last updated: 2026-05-08 Last verified on production: โ
Goal
Every user has an in-app bell that surfaces unread notifications, links each to the source resource (issue / cycle), and supports "mark all read". The bell fires for 11 distinct event types across the QA platform.
Actors
- Primary โ Any signed-in user
- Secondary โ Whatever upstream system fired the event (triage, payouts, run-batch, comment thread, role assignment)
Preconditions
- The user is signed in.
- An upstream event has fired that should notify this user (e.g. an admin triaged their issue, they were added to a cycle, etc.)
Trigger
The user clicks the bell icon in the sidebar.
Notification event types
| Type | When it fires | Audience | Source flow |
|---|---|---|---|
issue_filed | Tester submits a new issue | Cycle leads + admins (excl. reporter) | F-08 |
issue_approved | Admin approves in triage | Reporter | F-09 |
issue_rejected | Admin rejects in triage | Reporter | F-09 |
issue_paid | Admin pays an issue (any path) | Reporter | F-11 |
issue_reclassified | Admin changes severity | Reporter | F-09 |
issue_awaiting_verification | (reserved โ not yet wired) | Reporter | F-10 |
issue_verified | Admin/lead marks verified | Reporter | F-10 |
issue_info_requested | Admin uses Request Info in triage | Reporter | F-09 |
issue_info_provided | Reporter replies on info-requested issue | Cycle leads + admins (excl. reporter) | F-09 |
cycle_status_changed | Cycle activates/closes/etc. | All cycle members (excl. actor) | F-05 |
tester_assigned | Admin adds a tester to a cycle | The newly assigned tester | F-05 |
Happy path โ read + mark read
| # | Actor | UI surface | Action | API call | DB writes | Side effects | Suggested event |
|---|---|---|---|---|---|---|---|
| 1 | User | sidebar bell | Open dropdown | GET /api/me/notifications | โ | โ | notification.viewed |
| 2 | Frontend | dropdown | Render up to N rows newest-first; unread highlighted | โ | โ | โ | โ |
| 3 | User | a row | Click | PATCH /api/me/notifications/:id body { readAt: NOW } (or via mark-on-click) | UPDATE notifications SET read_at = NOW() WHERE id = :id | navigate to linked resource | notification.opened |
| 4 | User | dropdown | Click "Mark all read" | POST /api/me/notifications/mark-all-read | UPDATE notifications SET read_at = NOW() WHERE user_id = :uid AND read_at IS NULL | bell badge clears | notification.mark_all_read |
Notification creation pattern
Every upstream API handler calls createNotification(...) or
createNotificationsBulk(...) from src/lib/notifications.ts after
the primary write succeeds. Failures are swallowed and logged โ a
notification table outage cannot block the primary action.
Self-notify is suppressed: actions an actor takes on their own resources do not generate a row addressed to themselves.
The bulk helper deduplicates by (user_id, type, issue_id, cycle_id) so
the same person never gets two copies of one event (e.g. an admin who is
also a cycle lead).
Acceptance criteria
- AC-1 โ Given a tester files an issue, when the POST returns,
then leads + admins of the cycle (excluding the reporter) each have a
new
notificationsrow withtype='issue_filed',cycle_id+issue_idset. - AC-2 โ Given the reporter replies on an
info_requestedissue, then leads + admins are notified viaissue_info_provided, and the reporter is not in the audience list. - AC-3 โ Given the user opens the bell dropdown, when there are 0 unread, then the bell badge is hidden.
- AC-4 โ Given a user has 5 unread notifications, when they click
"Mark all read", then all 5 rows have
read_atset and the badge disappears. - AC-5 โ Given a notification has
cycle_id + issue_id, when the user clicks the row, then they navigate to/app/test-cycles/:cycleId/issues/:issueId. - AC-6 โ Given the upstream notification insert fails (DB error),
when the primary action succeeded, then the user still sees the
primary success path; the failure is
console.warn'd. - AC-7 โ Given an admin runs a period batch and pays 10 issues from
3 testers, then exactly 10
issue_paidrows are inserted, distributed across the 3 reporters; no admin gets a row for issues they paid themselves.
Test data / fixtures
- Baseline seed
- Two users: an admin and a tester member of an active cycle
Negative paths
| # | Scenario | Expected behavior |
|---|---|---|
| N-1 | Notifications dropdown opens with thousands of rows (long-lived account) | Pagination or cap at 50 newest-first; verify limit |
| N-2 | User clicks a notification linking to a deleted issue | API 404, dropdown does not crash; offer to mark-read-and-dismiss |
| N-3 | Two admins fire approval on different issues at exact same time | Both notifications insert independently; no race |
| N-4 | DB insert fails | Logged via console.warn; primary action succeeds; user gets no bell ping (graceful) |
Manual QA checklist
- As tester, file an issue (F-08)
- As admin, sign in โ bell badge shows "1"; dropdown shows "Issue filed"
- Click the row โ navigate to issue detail
- Refresh bell โ that row is now "read" (not bold)
- As admin, approve the issue โ reporter's bell badge shows "1"
- As reporter, click "Issue approved" notification โ land on issue detail
- As admin, run a period batch (F-11)
- As reporter, bell shows "Paid: " with method + reference body
- In bell dropdown, click "Mark all read" โ badge clears, all rows greyed
- Verify
notificationstable:read_atpopulated for all the user's rows
Automated test outline
test.describe("F-13 notifications", () => {
test("issue.filed โ leads+admins notified, not reporter", async ({ request }) => { โฆ });
test("self-notify suppressed", async ({ request }) => { โฆ });
test("dedup across multi-role users", async ({ request }) => { โฆ });
test("mark-all-read clears badge", async ({ page }) => { โฆ });
test("graceful insert failure", async ({ request }) => { /* simulate DB issue */ });
});Code references
- UI: sidebar bell button + dropdown (find via
useNotificationshook usage; lives in the QA section sidebar) - API:
src/app/api/me/notifications/route.ts(GET list, POST/DELETE bulk?)src/app/api/me/notifications/[id]/route.ts(PATCH read)src/app/api/me/notifications/mark-all-read/route.ts
- Hooks:
src/hooks/test-cycles/notifications.ts - Lib:
src/lib/notifications.ts(createNotification,createNotificationsBulk,leadAndAdminAudience,cycleMemberAudience) - Schema:
notifications,NOTIFICATION_TYPES
Events emitted (proposed)
notification.createdโ fired implicitly inside each upstream flow (via thecreateNotification(s)*helpers). Telemetry can hook the helper itself once we wire atrack().notification.viewedโ{ user_id, unread_count }(when dropdown opens)notification.openedโ{ user_id, notification_id, type }(when row clicked)notification.mark_all_readโ{ user_id, marked_count }
Open questions / known gaps
- No real-time push (no WebSocket / SSE) โ bell refreshes on tab focus / poll. Verify the polling interval; if 0, only refreshes on navigation.
- No email delivery (deferred โ
feature-matrix.mdยง3.7). - No Telegram delivery (deferred).
issue_awaiting_verificationexists in the enum but isn't wired โ could fire when an issue is approved + gate active so the reporter knows their bug is queued for fix re-test.