Implement PauseNotificationService core dispatch logic
epic-peer-mentor-pause-management-core-workflows-task-005 — Build the notification dispatch service that resolves coordinator assignments for a given peer mentor and dispatches both push (FCM) and in-app notifications on status transitions. Implement methods: notifyCoordinatorOfPause(mentorId, pauseRecord), notifyCoordinatorOfReactivation(mentorId), and buildDeepLink(mentorId) that routes to the coordinator pause roster. Handle cases where a mentor belongs to multiple chapters.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Structure the service to first call a resolveCoordinators(mentorId) private method that queries chapter_memberships for the mentor, then fetches coordinator user_ids for each chapter. This separation makes testing easier. FCM dispatch should be implemented as a Supabase Edge Function (Deno) invoked via supabase.functions.invoke() — this keeps the FCM server key entirely server-side and avoids exposing it in client code. The Edge Function receives a list of device tokens and a notification payload.
Define an abstract NotificationDispatcher interface with a concrete FCMNotificationDispatcher and a MockNotificationDispatcher for tests. The in-app notifications table must already exist (check with database_specialist if schema needs extending). Deep links must match the router configuration — coordinate with the navigation/routing team to confirm the exact path schema before hardcoding.
Testing Requirements
Write unit tests using flutter_test with mocked repository, mocked FCM client, and mocked Supabase client. Test: (1) notifyCoordinatorOfPause resolves 1 coordinator and dispatches 1 in-app + 1 push notification. (2) notifyCoordinatorOfPause with 2 chapters dispatches notifications to both coordinators. (3) Coordinator with no device token receives in-app notification only, no exception thrown.
(4) FCM HTTP failure is caught, logged, and does not prevent in-app notification creation. (5) notifyCoordinatorOfReactivation dispatches correct notification type. (6) buildDeepLink returns correctly formatted URI with mentorId parameter. (7) Mentor with zero chapters results in no notifications and logs a warning.
Aim for 90%+ branch coverage on service class.
Concurrent status transitions (e.g., coordinator and automated scheduler both attempting to update the same mentor's status simultaneously) may produce race conditions or inconsistent state in the database, leading to audit log gaps or incorrect notifications.
Mitigation & Contingency
Mitigation: Implement all status transitions as atomic Postgres RPC functions with optimistic locking (version column or updated_at check). Use database-level constraints rather than application-level guards as the final enforcement point.
Contingency: Add a compensation job that reconciles status and log table consistency on each nightly scheduler run, surfacing any discrepancies to coordinator dashboards.
The coordinator-to-mentor assignment relationship may not always be 1:1 or may be stale (coordinator reassigned after a pause was set), causing notifications to be sent to the wrong coordinator or not sent at all.
Mitigation & Contingency
Mitigation: Query the assignment relationship at notification dispatch time rather than caching it at pause creation time. Add a fallback to notify the chapter administrator if no active coordinator assignment exists.
Contingency: Log all undeliverable notification attempts with the originating mentor ID so administrators can manually follow up, and surface undelivered notification counts on the coordinator dashboard.
The CoordinatorPauseRosterScreen may load slowly for coordinators managing large rosters with many concurrent certification expiry queries, degrading usability on low-bandwidth mobile connections.
Mitigation & Contingency
Mitigation: Use a single Supabase RPC that joins mentor status, certification expiry, and assignment data in one query rather than N+1 individual calls. Implement pagination with a configurable page size and skeleton loading states.
Contingency: Add an offline cache of the last-fetched roster state using Riverpod with SharedPreferences, ensuring coordinators can at minimum view stale data when connectivity is poor.