Implement in-app notification dispatch channel
epic-pause-status-notifications-foundation-task-007 — Implement the in-app notification dispatch channel that writes notification records to the Supabase notifications table, ensuring both push and in-app records are created atomically in a single operation. Handle Supabase write failures without blocking push dispatch.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Model the dual-channel dispatch as a coordinator function (not a class method on FCM dispatcher) that calls the FCM channel and the in-app channel concurrently using Future.wait where possible, then merges the two outcomes into a unified DispatchResult. The key design principle: the two channels are independent — a failure in one must not prevent the other from running. Use try/catch around each channel invocation independently. For the Supabase insert, use upsert with onConflict: 'user_id,event_id' to implement idempotency.
Define a NotificationRecord Dart model with fromJson/toJson and map it to the Supabase row. Keep the in-app channel as a separate injectable class so it can be mocked independently in tests. The coordinator function is what the BLoC event handler calls — it abstracts channel details from business logic.
Testing Requirements
Unit tests using flutter_test with mocked Supabase client. Required test cases: (1) both channels succeed — result is full_success with both push message_id and in-app record id, (2) FCM succeeds, Supabase fails — result is push_ok_inapp_failed, record not present in mock store, (3) FCM fails, Supabase succeeds — result is push_failed_inapp_ok, in-app record is present, (4) both fail — result is dead_letter, (5) duplicate event_id — second call skips insert and returns existing record id. Integration test against a Supabase test project: verify RLS allows recipient to read inserted record and blocks other users.
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.