Integration test ApprovalNotificationService dual-channel delivery
epic-expense-approval-workflow-foundation-task-017 — Write integration tests for ApprovalNotificationService verifying: FCM notification dispatched when claim status changes to approved/rejected, Realtime event published to correct channel, coordinator notification includes required claim metadata (claimant name, amount, claim_id), FCM token refresh handled gracefully, and no duplicate notifications sent when both channels active simultaneously.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 5 - 253 tasks
Can start after Tier 4 completes
Implementation Notes
ApprovalNotificationService should depend on an FcmGateway interface and a RealtimeBroadcaster interface — both injectable — so tests can mock them independently. FCM calls must be routed through a Supabase Edge Function (never direct from client) because FCM server keys cannot be embedded in mobile apps. In the service: only dispatch FCM for terminal states (approved, rejected); use a Set of already-dispatched claim_ids per session to prevent duplicate delivery when both channels are open simultaneously. Token refresh: catch InvalidTokenException, call FcmGateway.refreshToken(userId), then retry once; surface FcmDeliveryFailure if retry also fails.
This dual-channel pattern (FCM for background, Realtime for foreground) ensures coordinators always receive notifications regardless of app state — critical for the HLF follow-up workflow.
Testing Requirements
Integration tests using flutter_test with mocktail. Mock both the FCM gateway interface and the Supabase Realtime broadcast interface. Group tests: (1) approved/rejected trigger FCM, (2) non-terminal transitions do not trigger FCM, (3) Realtime broadcast correctness, (4) FCM token refresh flow, (5) duplicate suppression with both channels active, (6) FCM failure isolation. Use verify(mockFcmGateway.send(captureAny)) to assert payload contents.
Use verifyNever to assert no spurious FCM calls. For token refresh: stub the first FCM call to throw InvalidTokenException, then verify the service fetches a new token and retries once.
Optimistic locking in ExpenseClaimStatusRepository may produce excessive concurrency exceptions in high-volume coordinator sessions where multiple coordinators process the same queue simultaneously, causing confusing UI errors and coordinator frustration.
Mitigation & Contingency
Mitigation: Design the locking strategy with a short retry window (1-2 automatic retries with 200ms back-off) before surfacing the error to the UI. Document the concurrency model clearly so the UI layer can display a contextual 'claim was already actioned' message rather than a generic error.
Contingency: If contention remains high under load testing, switch to a last-writer-wins update with a conflict notification rather than a hard block, and log all concurrent edits for audit purposes.
FCM device tokens stored for peer mentors may be stale (app reinstalled, token rotated) causing push notifications for claim status changes to silently fail, leaving submitters unaware their claim was approved or rejected.
Mitigation & Contingency
Mitigation: Implement token refresh on every app launch and store updated tokens in Supabase. ApprovalNotificationService should fall back to in-app Realtime delivery when FCM returns an invalid-token error and should queue a token refresh request.
Contingency: If FCM delivery rates fall below acceptable thresholds in production monitoring, add a polling fallback in the peer mentor claim list screen that checks status on foreground resume.
Supabase Realtime has per-project channel and connection limits. If many coordinators and peer mentors are simultaneously subscribed across multiple screens, the project may hit quota limits causing subscription failures.
Mitigation & Contingency
Mitigation: Design RealtimeApprovalSubscription to use a single shared channel per user session rather than per-screen subscriptions. Implement subscription reference counting so channels are only opened once and reused across screens.
Contingency: Upgrade the Supabase plan tier if limits are reached, and implement graceful degradation to polling with a 30-second interval when Realtime is unavailable.