Push Notifications — QA Plan
1. Token lifecycle
| # | Scenario | Expected |
|---|---|---|
| 1.1 | Mobile registers a token on first sign-in | 201; row exists with platform; last_seen_at = now |
| 1.2 | Same device re-registers (re-launch) | last_seen_at updated; same row |
| 1.3 | User A signs out; User B signs in on same device | Token row’s user_id flips to B; deleted_at cleared |
| 1.4 | Sign-out flow calls revoke | deleted_at set |
| 1.5 | Caller tries to revoke a token they don’t own | 403 |
| 1.6 | Current build registers with expoProjectId | Row stores it; re-registering from an older build that omits it leaves the stored value alone |
| 1.7 | Client sends a project id the binary doesn’t use | 201 — registration still succeeds, expo_project_id stays NULL, warn logged |
2. Send paths
| # | Scenario | Expected |
|---|---|---|
| 2.1 | notifyUser(memberId) with category newMessage | Job enqueued; push arrives on device |
| 2.2 | User opted out of newMessage push | No enqueue |
| 2.3 | User has no active tokens | No enqueue |
| 2.4 | User has 3 devices | Job carries 3 tokens; all receive |
| 2.5 | Bulk fan-out (announcement to 500 members) | Chunked across 5+ Expo HTTP calls; all eventually delivered |
| 2.6 | Expo returns DeviceNotRegistered for one token | Token soft-deleted in DB; next send excludes it |
| 2.7 | Expo HTTP 500 | BullMQ retries 3× with exponential backoff; eventual log only |
| 2.8 | Audience mixes old- and new-project tokens | One request per project; every project’s push delivers |
| 2.9 | Audience includes tokens with no expo_project_id | Each such token gets its own request; a rejection cannot spread |
| 2.10 | One project’s request fails, another’s succeeds | Job completes; successful project is not re-sent on retry; Sentry push_delivery context names the failed project and Expo code, and carries no tokens |
| 2.11 | Queued job with no tokenProjects and no organizationId | Grouped by each token’s recorded project; no crash on the missing fields |
3. Receipt processing
| # | Scenario | Expected |
|---|---|---|
| 3.1 | Push send succeeds → 15min later, receipts fetched | Jobs run; DeviceNotRegistered tokens cleaned |
| 3.2 | Receipt fetch times out | Logged; retry per BullMQ options |
4. Scheduled push
| # | Scenario | Expected |
|---|---|---|
| 4.1 | scheduleNotifyUser with sendAt=+30min | BullMQ job with delay |
| 4.2 | Caller cancels job by id before delay elapses | Push not sent |
| 4.3 | sendAt in the past | Synchronous notifyUser call (delay=0) |
5. Preferences
| # | Scenario | Expected |
|---|---|---|
| 5.1 | First PATCH for a user | Row inserted |
| 5.2 | Toggle announcement.push = false | Future announcement sends skip this user |
| 5.3 | Missing key in JSONB | Treated as opt-in |
| 5.4 | Add a new category to the catalog | Backward-compatible (opt-in default) |
| 5.5 | PATCH { billing: { push: false } } | Accepted; billing pushes skip this user, billing emails still send; billing.email = false stops the emails. Other categories’ opt-outs never mute billing |
6. Integration QA across features
| # | Scenario | Expected |
|---|---|---|
| 6.1 | Coach assigns a workout | workoutAssigned push arrives |
| 6.2 | Coach posts a message | newMessage push arrives |
| 6.3 | Member opted out of newMessage | No push; web/mobile in-app still updates |
| 6.4 | Coach publishes announcement | announcement push to every member except author |
| 6.5 | Booking class reminder fires at T-30min | classReminder push arrives |
| 6.6 | Member’s renewal declines | Right away, one billing push (route /(tabs)/profile/payments, data.stage = 'opened') and one email (debt opened, amount + pay-by day); then one last-day notice on the pay-by day and one cancelled notice after day N; replays send nothing |
| 6.7 | Saved card stops working in 14 / 7 / 1 org days | One billing push + email each time; a re-run the same day sends nothing (Redis NX) |
| 6.8 | Renewal finds the card expired | Card-unusable push + email at once; the 08:15 run does not repeat it |
7. Smoke
- Real device with Expo dev client receives a test push.
- DeviceNotRegistered receipt soft-deletes token; verified in DB.
- Pref toggle visible immediately in send behavior.