Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesPush NotificationsPush Notifications — QA Plan

Push Notifications — QA Plan

1. Token lifecycle

#ScenarioExpected
1.1Mobile registers a token on first sign-in201; row exists with platform; last_seen_at = now
1.2Same device re-registers (re-launch)last_seen_at updated; same row
1.3User A signs out; User B signs in on same deviceToken row’s user_id flips to B; deleted_at cleared
1.4Sign-out flow calls revokedeleted_at set
1.5Caller tries to revoke a token they don’t own403
1.6Current build registers with expoProjectIdRow stores it; re-registering from an older build that omits it leaves the stored value alone
1.7Client sends a project id the binary doesn’t use201 — registration still succeeds, expo_project_id stays NULL, warn logged

2. Send paths

#ScenarioExpected
2.1notifyUser(memberId) with category newMessageJob enqueued; push arrives on device
2.2User opted out of newMessage pushNo enqueue
2.3User has no active tokensNo enqueue
2.4User has 3 devicesJob carries 3 tokens; all receive
2.5Bulk fan-out (announcement to 500 members)Chunked across 5+ Expo HTTP calls; all eventually delivered
2.6Expo returns DeviceNotRegistered for one tokenToken soft-deleted in DB; next send excludes it
2.7Expo HTTP 500BullMQ retries 3× with exponential backoff; eventual log only
2.8Audience mixes old- and new-project tokensOne request per project; every project’s push delivers
2.9Audience includes tokens with no expo_project_idEach such token gets its own request; a rejection cannot spread
2.10One project’s request fails, another’s succeedsJob 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.11Queued job with no tokenProjects and no organizationIdGrouped by each token’s recorded project; no crash on the missing fields

3. Receipt processing

#ScenarioExpected
3.1Push send succeeds → 15min later, receipts fetchedJobs run; DeviceNotRegistered tokens cleaned
3.2Receipt fetch times outLogged; retry per BullMQ options

4. Scheduled push

#ScenarioExpected
4.1scheduleNotifyUser with sendAt=+30minBullMQ job with delay
4.2Caller cancels job by id before delay elapsesPush not sent
4.3sendAt in the pastSynchronous notifyUser call (delay=0)

5. Preferences

#ScenarioExpected
5.1First PATCH for a userRow inserted
5.2Toggle announcement.push = falseFuture announcement sends skip this user
5.3Missing key in JSONBTreated as opt-in
5.4Add a new category to the catalogBackward-compatible (opt-in default)
5.5PATCH { 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

#ScenarioExpected
6.1Coach assigns a workoutworkoutAssigned push arrives
6.2Coach posts a messagenewMessage push arrives
6.3Member opted out of newMessageNo push; web/mobile in-app still updates
6.4Coach publishes announcementannouncement push to every member except author
6.5Booking class reminder fires at T-30minclassReminder push arrives
6.6Member’s renewal declinesRight 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.7Saved card stops working in 14 / 7 / 1 org daysOne billing push + email each time; a re-run the same day sends nothing (Redis NX)
6.8Renewal finds the card expiredCard-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.