Build CertificationManagementService lifecycle orchestrator
epic-certification-management-core-logic-task-003 — Implement CertificationManagementService as the single lifecycle orchestrator for certifications. Provide methods: createCertification, recordRenewal, computeExpiryState, initiateCourseEnrolment. Wire the service to CertificationRepository for persistence, and inject PauseManagementService and HLFDynamicsSyncService as collaborators. All state transitions must be transactional.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Place in `lib/domain/services/certification_management_service.dart`. The service is a pure domain-layer class — no Flutter or Supabase imports. Collaborator orchestration order in `recordRenewal` should be: (1) persist renewal to repository, (2) recompute expiry state, (3) if now valid, call Dynamics sync to restore visibility, (4) if still expired, call PauseManagementService. Never reverse this order or make Dynamics sync a precondition for local persistence.
For transactional semantics without a distributed transaction manager, use a compensating-action pattern: if Dynamics sync fails after a successful repository write, queue the sync for retry rather than rolling back the local write. Implement `initiateCourseEnrolment` as a thin delegation to a `CourseEnrolmentService` or a Supabase Edge Function — do not hardcode enrolment logic here. Register the service as a Riverpod `Provider` with all dependencies injected, keeping it lazily initialized.
Testing Requirements
Write unit tests using `mocktail` for all four public methods. For each method test: happy path with all collaborators returning success, repository failure path (assert exception propagation), pause service failure path (assert local state is not corrupted), and Dynamics sync failure path (assert local update is preserved and error is surfaced). Test `computeExpiryState` with boundary values: exactly at expiry date, one day before expiry, 30 days before expiry, 31 days before expiry. Verify that `recordRenewal` calls collaborators in the correct sequence.
Aim for 95% branch coverage on the service class. Integration test: execute `createCertification` followed by `recordRenewal` against local Supabase and assert database state is consistent.
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.