Implement FCM push notification dispatch
epic-pause-status-notifications-foundation-task-006 — Implement the FCM push channel dispatch method that sends a formatted notification payload to a resolved FCM token. Include token validity checks, HTTP error handling for 4xx/5xx FCM API responses, and structured logging of dispatch outcomes.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
This method must execute server-side (Supabase Edge Function in TypeScript/Deno) because FCM server credentials cannot be embedded in the Flutter client. The Flutter/Dart layer invokes the Edge Function via an authenticated Supabase RPC call, passing only the notification payload and the target user_id. The Edge Function resolves the FCM token from the database and makes the actual FCM HTTP v1 API call. Design DispatchResult as a sealed class (Dart 3) with subclasses Success, Failure, InvalidToken, DeadLetter — this makes exhaustive handling mandatory at call sites.
Use http package or dio for the HTTP call; set connectTimeout and receiveTimeout explicitly. Extract FCM response parsing into a private pure function for testability. Log structured JSON (not freeform strings) to make log aggregation (e.g. Supabase log drain) parseable.
Never log the token — log a hashed or truncated token fingerprint if correlation is needed for debugging.
Testing Requirements
Unit tests using flutter_test and a mocked HTTP client (mockito or manual stub). Required test cases: (1) successful dispatch returns DispatchResult.success with correct message_id, (2) empty token returns DispatchResult.invalidToken without HTTP call, (3) FCM 400 returns DispatchResult.invalidToken, (4) FCM 500 returns DispatchResult.failure with FCM_SERVER_ERROR, (5) network timeout returns DispatchResult.failure with NETWORK_TIMEOUT, (6) FCM 429 returns DispatchResult.failure with FCM_RATE_LIMITED. Verify that no test case writes the raw FCM token to any log output. Aim for 100% branch coverage of the dispatch method.
The org membership table structure used to resolve coordinator relationships may differ from what the repository assumes, causing incorrect coordinator lookup or missing rows for mentors in multi-chapter scenarios.
Mitigation & Contingency
Mitigation: Review the existing org membership table schema and RLS policies before writing repository queries. Align query logic with the patterns already used by peer-mentor-status-repository and multi-chapter-membership-service.
Contingency: If schema differs, add an adapter layer in the repository that normalises the membership resolution and document the discrepancy for the data team. Fall back to coordinator lookup via the feature's own stored coordinator_id field if org membership join fails.
Device tokens stored in the database may be stale or unregistered, causing FCM dispatch failures that silently drop coordinator notifications — the primary coordination safeguard of this feature.
Mitigation & Contingency
Mitigation: Implement token validation on every dispatch call and handle FCM's NOT_REGISTERED error by flagging the token as invalid in the database. Reuse the token refresh pattern already established by fcm-token-manager.
Contingency: If push delivery fails after retry, ensure the in-app notification record is always written regardless of push outcome so coordinators can still see the event in the notification centre.
The optional reason field may contain special characters, emoji, or non-Latin scripts that exceed the 200-character byte limit when FCM encodes the payload, causing delivery failures.
Mitigation & Contingency
Mitigation: Enforce the 200-character limit on Unicode code point count, not byte count, in the payload builder. Add a unit test with multi-byte input strings.
Contingency: If an oversized payload is detected at dispatch time, strip the reason field from the push notification body and note 'See in-app notification for full reason' to preserve delivery.