Implement Supabase-backed CertificationRepository
epic-certification-management-core-logic-task-002 — Implement the CertificationRepository using the Supabase Dart client. Cover CRUD operations for certification records: create, read by mentor ID, update renewal date, update expiry status, and paginated queries for certs expiring within N days. Apply RLS policies to scope queries to the authenticated user's organisation.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Place the concrete implementation in `lib/data/repositories/supabase_certification_repository.dart`. Inject `SupabaseClient` via constructor for testability. Use `.from('certification')` query builder throughout — never raw SQL. For `saveRenewal`, use `.upsert()` with `onConflict: 'id'` to handle both create and renewal update in one call.
For `getCertificationsExpiringWithin`, construct the upper bound timestamp server-side using PostgreSQL interval syntax via `.lte('expires_at', upperBound.toIso8601String())` rather than relying on Dart-side date arithmetic to avoid clock skew. Map `PostgrestException` codes to specific `CertificationRepositoryException` subclasses: code `PGRST116` (row not found) → `CertificationNotFoundException`, auth errors → `CertificationPermissionDeniedException`. Register this repository as a singleton in the Riverpod provider tree so the Supabase client instance is not recreated per call.
Testing Requirements
Write unit tests using `mocktail` to mock the Supabase client and verify that each repository method constructs the correct query chain (table name, filters, columns). Write integration tests against a local Supabase instance (via Docker or supabase CLI) seeded with test certification rows for two different organisations; assert that queries for org A never return rows for org B (RLS boundary test). Test error paths: simulate a `PostgrestException` and assert that a typed `CertificationRepositoryException` is thrown. Test pagination: verify that requesting page 2 with page size 5 returns the correct slice.
Aim for 90% branch coverage on the concrete implementation class.
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.