Implement notification deduplication against cert_notification_log
epic-certification-management-core-logic-task-009 — Within CertificationReminderService, implement deduplication logic that checks cert_notification_log before dispatching any reminder. A reminder for a given (mentor_id, expiry_date, threshold_days) tuple must only be sent once. Query the log table via CertificationRepository and filter out already-notified candidates before building payloads.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Build the in-memory lookup as a Set
The separation of concerns here is critical: this method only reads and filters; it never writes. Writing to the log is task-011's responsibility, which prevents double-write races if the service is later parallelised.
Testing Requirements
Unit tests (flutter_test): (1) Seed mock log with a delivered entry for (mentor-A, expiry-date-X, 7 days); assert mentor-A excluded from result. (2) Seed mock log with a failed entry for the same tuple; assert mentor-A IS included (retry allowed). (3) Seed log with an entry older than 90 days; assert candidate is NOT excluded (stale log). (4) Assert two certifications of different cert_types for the same mentor are deduped independently.
(5) Mock CertificationRepository.getNotificationLog throwing an exception; assert DeduplicationQueryException is thrown and no candidates pass through. (6) Assert batched query is called once regardless of input list size (mock call count assertion). (7) Empty input list returns empty list without database call. Coverage target: 95% branch 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.