Build audience-specific notification payload builders
epic-certification-management-core-logic-task-010 — Implement separate payload builder functions inside CertificationReminderService for two audiences: peer mentor (personal reminder with days remaining and course link) and coordinator (team oversight notification listing all expiring mentors). Each builder must produce a platform-compatible FCM payload with correct title, body, data fields, and deep-link target for the notification centre.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 4 - 323 tasks
Can start after Tier 3 completes
Implementation Notes
Define FcmPayload as an immutable Dart class with a toJson() method that produces the FCM v1 message body structure. The FCM v1 format requires a 'message' wrapper with 'notification' (title/body) and 'data' (string key-value pairs) — ensure all data map values are explicitly cast to String since FCM rejects non-string data values. For the coordinator batching, use a groupBy utility (from the collection package) to group candidates by coordinatorId in a single pass before building payloads. Implement a _formatMentorNameShort(String firstName, String lastName) private helper that produces 'Ola N.' format — centralise PII truncation logic here.
The 4KB limit check should be implemented as an assert in debug mode and a logged warning (not throw) in release mode to avoid crashing the reminder pipeline over a marginal overage. Use separate builder methods (not a generic builder with an audience flag) for clarity — the two payloads have structurally different inputs.
Testing Requirements
Unit tests (flutter_test): (1) buildMentorReminderPayload with a 7-day candidate; assert body contains '7 days', deep_link routes to /certification/{certId}, data map contains threshold_days=7 and cert_type. (2) buildMentorReminderPayload with 14-day and 30-day candidates; assert correct day counts and threshold values. (3) buildCoordinatorReminderPayload with 60 candidates for one coordinator; assert body contains '… and X more' and total payload < 4KB. (4) Assert PII check: no '@' (email) and no digit sequences >5 chars (personnummer proxy) in any payload field.
(5) jsonEncode/jsonDecode round-trip test for both payload types. (6) buildCoordinatorReminderPayload with candidates for 3 different coordinators; assert Map contains 3 keys with correct grouping. (7) Empty candidate list to coordinator builder returns empty Map. Coverage target: 90% line coverage.
The auto-pause workflow requires CertificationManagementService to call PauseManagementService and HLFDynamicsSyncService in the same logical transaction. If PauseManagementService succeeds but the Dynamics webhook fails, the mentor is paused locally but remains visible on the HLF portal.
Mitigation & Contingency
Mitigation: Implement a saga pattern: write a pending sync event to the database before calling Dynamics, and have a background retry job consume pending events. This guarantees eventual consistency even if the webhook fails transiently.
Contingency: If the Dynamics sync fails after auto-pause, surface an explicit coordinator alert in the dashboard indicating 'Dynamics sync pending — mentor may still be visible on portal'. Allow manual retry from coordinator UI.
If the nightly cron job runs concurrently (e.g., due to infra retry), CertificationReminderService could dispatch duplicate notifications to mentors before the cert_notification_log insert is visible to the second invocation.
Mitigation & Contingency
Mitigation: Use Supabase's upsert with a unique constraint on (mentor_id, threshold_days, cert_id) in cert_notification_log. The second concurrent insert will fail gracefully and the duplicate dispatch will be skipped.
Contingency: If duplicate notifications do reach mentors, add a post-dispatch dedup check and include a 'you may receive this notification again' disclaimer until the constraint is deployed.