critical priority medium complexity database pending database specialist Tier 0

Acceptance Criteria

getMentorsExpiringWithin(organisationId, thresholdDays) returns a List<MentorCertification> where every record's expiry_date falls within [today, today + thresholdDays] (inclusive, evaluated at midnight UTC)
getLapsedMentors(organisationId) returns all records where expiry_date < today at midnight UTC
Both methods accept an optional pagination parameter (page, pageSize) and return a consistent page of results; calling with page=1, pageSize=20 on a 45-record set returns exactly 20 records
Both methods accept organisationId and only return certifications belonging to mentors in that organisation — cross-organisation data leakage is impossible at the query level
Queries complete in under 200ms for a dataset of 10,000 certification rows, confirmed by an index on (organisation_id, expiry_date)
The repository is defined as an abstract Dart class (interface) with a concrete Supabase implementation, following the dependency-inversion pattern used elsewhere in the codebase
A null or empty result set returns an empty list, never null or an exception
All Supabase query errors are caught and re-thrown as typed domain exceptions (CertificationQueryException) with the original error attached

Technical Requirements

frameworks
supabase_flutter
Dart async/await
apis
Supabase PostgREST API (certifications table)
Supabase RLS policies (expiry-specific policy from data-foundation epic)
data models
MentorCertification (mentor_id, organisation_id, certificate_type, expiry_date, issued_date, status)
CertificationQueryException (domain error wrapper)
performance requirements
Query latency under 200ms at p95 for 10,000 rows
Composite index on (organisation_id, expiry_date) must exist before deploying
Pagination prevents unbounded result sets; default pageSize must not exceed 100
security requirements
organisationId filter must be applied at the database level (PostgREST .eq filter), not client-side, to prevent data leakage
RLS policy from the data-foundation epic must be active; repository must not use service role key in the Flutter client
No raw SQL construction from user-supplied strings — use PostgREST filter chaining only

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Define the abstract class CertificationExpiryRepository in a separate file from the Supabase implementation (SupabaseCertificationExpiryRepository) so the orchestrator edge function can depend on the interface. Use Dart's DateTime.now().toUtc() and compute the threshold date as DateTime(now.year, now.month, now.day).add(Duration(days: thresholdDays)) to avoid time-of-day drift. For the PostgREST query, use .gte('expiry_date', todayIso).lte('expiry_date', thresholdDateIso) for upcoming windows and .lt('expiry_date', todayIso) for lapsed. The composite index should be created in a Supabase migration file: CREATE INDEX IF NOT EXISTS idx_certifications_org_expiry ON certifications (organisation_id, expiry_date).

Register the repository with Riverpod as a Provider so the edge function invoker and any Flutter UI components can consume it through dependency injection.

Testing Requirements

Unit tests: mock the Supabase client to verify correct filter chains are built for each threshold (60/30/7/lapsed), pagination parameters are forwarded correctly, and Supabase errors are wrapped in CertificationQueryException. Integration tests: run against a Supabase test project with seeded certification data covering all threshold windows, lapsed, and cross-organisation rows. Assert that cross-organisation records are never returned. Assert empty list for organisations with no expiring certificates.

Assert pagination boundary conditions (last page may have fewer than pageSize records). Minimum test coverage: 90% of repository method branches.

Component
Certificate Expiry Check Edge Function
infrastructure medium
Epic Risks (4)
high impact medium prob technical

If the daily edge function runs more than once in a 24-hour window due to a Supabase scheduling anomaly or manual re-trigger, the orchestrator could dispatch duplicate push notifications to the same mentor and coordinator for the same threshold, eroding user trust.

Mitigation & Contingency

Mitigation: Implement idempotency at the notification record level using a unique constraint on (mentor_id, threshold_days, certification_id). The orchestrator checks for an existing record before dispatching. Use a database-level upsert with ON CONFLICT DO NOTHING.

Contingency: If duplicate notifications are reported in production, add a rate-limiting guard in the edge function that aborts if a notification for the same mentor and threshold was created within the last 20 hours, and add an alerting rule to Supabase logs for duplicate dispatch attempts.

medium impact medium prob scope

The mentor visibility suppressor relies on the daily edge function to detect expiry and update suppression_status. A mentor whose certificate expires at midnight may remain visible for up to 24 hours if the cron runs at a fixed time, violating HLF's requirement that expired mentors disappear promptly.

Mitigation & Contingency

Mitigation: Schedule the edge function to run at 00:05 UTC to minimise lag after midnight transitions. Additionally, the RLS policy can include a direct date comparison (certification_expiry_date < now()) as a secondary predicate that does not rely on suppression_status, providing real-time enforcement at the database level.

Contingency: If the cron lag is unacceptable after launch, implement a Supabase database trigger on the certifications table that fires on UPDATE of expiry_date and calls the suppressor immediately, reducing lag to near-zero for renewal and expiry events.

medium impact low prob integration

The orchestrator needs to resolve the coordinator assigned to a specific peer mentor to dispatch coordinator-side notifications. If the assignment relationship is not normalised or is missing for some mentors, coordinator notifications will silently fail.

Mitigation & Contingency

Mitigation: Query the coordinator assignment from the existing assignments or user_roles table before dispatch. Log a structured warning (missing_coordinator_assignment: mentor_id) when no coordinator is found. Add a data quality check in the edge function that reports mentors without coordinators.

Contingency: If coordinator assignments are missing at scale, fall back to notifying the chapter-level admin role for the mentor's chapter, and surface a data quality report to the admin dashboard showing mentors without assigned coordinators.

medium impact low prob dependency

The course enrollment prompt service generates deep-link URLs targeting the course administration feature. If the course administration feature changes its deep-link schema or the Dynamics portal URL structure changes, enrollment prompts will navigate to broken destinations.

Mitigation & Contingency

Mitigation: Define the deep-link contract between the certificate expiry feature and the course administration feature as a shared constant in a cross-feature navigation config. Version the deep-link schema and validate the generated URL format in unit tests.

Contingency: If the deep-link breaks in production, the course enrollment prompt service should gracefully fall back to opening the course administration feature root screen with a query parameter indicating the notification context, allowing the user to manually locate the correct course.