Add Idempotency Guards to Cron
epic-certification-management-automation-task-004 — Implement idempotency protection so the cron function does not send duplicate reminders or trigger duplicate auto-pauses if it is invoked more than once within a 24-hour window. Use a processed-runs log table or a date-keyed lock record in Supabase to track each action already taken for a given certification and run date.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Create the migration file first and run supabase db push locally to verify the table and constraint are created correctly before writing the idempotency module. In idempotency.ts, use a single .select('id').eq('certification_id', id).eq('action_type', action).eq('run_date', date).maybeSingle() query for the check — maybeSingle() returns null rather than throwing when no row is found, making the check cleaner. For markAsProcessed, use .insert({ certification_id, action_type, run_date }).select() and check for a 23505 PostgreSQL error code (unique_violation) in the error response to distinguish a race-condition duplicate from a genuine insert failure. Run the idempotency check and mark in the same logical unit as the service call: check → call service → mark.
Do not mark before the service call succeeds, as a failed service call that is marked as processed would never be retried. Consider adding a status field to the log table ('success' | 'failed') to support future retry logic.
Testing Requirements
Write Deno unit tests in supabase/functions/certification-expiry-cron/idempotency_test.ts. Mock the Supabase client to return an existing row for isAlreadyProcessed and assert it returns true. Mock to return empty rows and assert it returns false. For markAsProcessed, mock a successful insert and assert no error is thrown.
Mock a 409/unique-constraint error response and assert the error is swallowed without re-throwing. Write an integration test using a local Supabase instance: invoke the cron function twice on the same date and assert the certification_expiry_cron_log table contains exactly one row per (certification_id, action_type, run_date) combination after both invocations. Assert that the second invocation's response body contains processed: 0 and skipped: N where N equals the number of certifications in the windows.
Supabase Edge Functions can have cold-start latency that causes the nightly cron to time out when processing large cohorts of expiring certifications, resulting in partial reminder dispatches.
Mitigation & Contingency
Mitigation: Batch the cron processing in chunks of 50 mentors per iteration. Use pagination with a cursor to resume processing if the function is re-invoked. Keep total invocation time well under the Edge Function timeout limit.
Contingency: If timeouts occur in production, split the cron into two separate functions: one for reminders and one for auto-pauses, each with its own schedule offset to reduce peak load.
Certification BLoC covers three distinct workflows (view, renew, enrol) which may lead to an overly complex state machine that is hard to test and maintain, particularly when error states from multiple concurrent operations need to be differentiated in the UI.
Mitigation & Contingency
Mitigation: Use separate sealed state classes per workflow (CertificationViewState, RenewalState, EnrolmentState) composed into a single BLoC state wrapper. Follow the existing BLoC patterns established in the codebase for consistency.
Contingency: If the BLoC grows too complex, split into two BLoCs: CertificationBLoC (view/load) and CertificationActionBLoC (mutations), connected via a shared stream.