high priority medium complexity backend pending backend specialist Tier 6

Acceptance Criteria

CertificationManagementService exposes an async initiateCourseEnrolment(certificationId: String, mentorId: String) method that returns an EnrolmentInitiationResult sealed class (success, portalUnreachable, alreadyEnrolled, noRenewalCourseFound)
Method correctly resolves the renewal course URL/deep-link from a course mapping keyed by cert_type; mapping sourced from Supabase or a local config constant
Successful initiation opens the HLF course portal via url_launcher with external application link mode; fallback to in-app WebView is not accepted
Every call to initiateCourseEnrolment writes a record to the enrolment_initiation_log table (mentor_id, cert_type, initiated_at, portal_reachable: bool, queued: bool) via CertificationRepository
When the portal returns a non-200 or throws a connection timeout (>10 seconds), the method queues the request in the local pending_enrolment_queue Supabase table and returns portalUnreachable
Queued requests include mentor_id, cert_type, created_at, and retry_count (initial value 0); retry_count is incremented on each failed retry attempt
On the next successful app sync cycle, all queued items with retry_count < 5 are retried; items exceeding 5 retries are marked as expired and surfaced to the coordinator via a notification
If a mentor is already enrolled (enrolment_initiation_log contains a record within the last 30 days for the same cert_type), the method returns alreadyEnrolled without opening the portal
No certification ID, mentor PII, or personnummer is embedded in the constructed deep-link URL
All database writes are wrapped in a try/catch; failures are logged via the app logger and do not crash the calling context

Technical Requirements

frameworks
Flutter (url_launcher package for portal deep-link)
BLoC for state management in calling UI layer
Riverpod for service injection
apis
Supabase PostgreSQL — enrolment_initiation_log table (insert, select)
Supabase PostgreSQL — pending_enrolment_queue table (insert, update, select)
HLF course portal — external deep-link or REST endpoint for enrolment redirect
Supabase Edge Functions (Deno) — optional: trigger retry loop server-side on sync
data models
certification (cert_type, peer_mentor_id, issued_at, expires_at)
enrolment_initiation_log (mentor_id, cert_type, initiated_at, portal_reachable, queued)
pending_enrolment_queue (mentor_id, cert_type, created_at, retry_count, last_attempt_at)
performance requirements
Portal reachability check must time out within 10 seconds to avoid blocking the UI thread
Local queue write must complete within 500ms so the user receives immediate feedback
Retry loop must process all queued items within a single sync cycle without blocking UI
security requirements
Deep-link URL must not contain PII (no personnummer, no full name, no email)
Supabase RLS must restrict enrolment_initiation_log reads/writes to the authenticated user's own records and their coordinator
pending_enrolment_queue writes use the service role key only via Edge Functions — never from the mobile client directly
url_launcher must validate the portal URL scheme against an allowlist before launching to prevent open-redirect injection

Execution Context

Execution Tier
Tier 6

Tier 6 - 158 tasks

Can start after Tier 5 completes

Implementation Notes

Use a CoursePortalResolver helper class that maps cert_type enum values to portal URLs, making the mapping easy to update without touching service logic. Implement a PendingEnrolmentRetryCoordinator (or extend the existing sync service) that queries pending_enrolment_queue on app foreground resume. Use Dart's http package with a custom timeout interceptor rather than Supabase client for the portal reachability check, since the portal is an external system. Sealed classes for EnrolmentInitiationResult will allow exhaustive handling in BLoC event listeners.

Avoid storing the portal URL in the certification table — keep it in a separate course_type_config table or a Dart constant to prevent stale deep-links. The log table should use a composite unique index on (mentor_id, cert_type, DATE(initiated_at)) to prevent accidental duplicate logs on rapid re-taps.

Testing Requirements

Unit tests (flutter_test): (1) Mock CertificationRepository and url_launcher; assert initiateCourseEnrolment returns success and calls repository.logEnrolmentInitiation with correct args when portal responds 200. (2) Simulate connection timeout; assert portalUnreachable is returned and repository.queueEnrolment is called with retry_count=0. (3) Seed enrolment_initiation_log mock with a record within 30 days; assert alreadyEnrolled returned without opening portal. (4) Assert that retry logic increments retry_count and marks expired after 5 attempts.

Integration tests: Run against local Supabase instance; verify RLS prevents one mentor reading another's enrolment log; verify queue record is persisted and retrieved correctly across separate service instantiations. Coverage target: 90% branch coverage on initiateCourseEnrolment method.

Component
Certification Management Service
service high
Epic Risks (2)
high impact medium prob technical

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.

medium impact low prob technical

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.