high priority medium complexity testing pending backend specialist Tier 5

Acceptance Criteria

A test script exists at `tests/integration/certification_expiry_cron_test.ts` (Deno) or equivalent, runnable with `supabase functions serve` + a local Supabase instance started via `supabase start`
The test seeds the database with at least four certification records: (1) expires in 30 days, (2) expires in 14 days, (3) expired yesterday, (4) already paused (should be skipped)
After manually invoking the cron function via HTTP POST to the local Edge Function URL, assertions verify: certification (3) has status='paused' in the database, idempotency record exists for certification (3), certifications (1) and (2) received the correct notification-only action (not paused), certification (4) was not touched (no duplicate idempotency record)
Re-running the cron a second time without changing the database produces zero new idempotency records and zero status changes — full idempotency is verified via a record count assertion
The cron_audit_log table contains exactly one row per run with correct missed_pause_count (0 for a clean run)
If the seed contains a certification that should have been paused but was not (simulated missed transition from a prior run), the audit log row for the re-run shows missed_pause_count=1 and the correct certification ID
The test script tears down all seeded rows in a `finally` block so subsequent test runs start from a clean state
Test completes in under 60 seconds on a local developer machine with Supabase CLI running

Technical Requirements

frameworks
Supabase CLI (supabase start, supabase functions serve)
Deno (for test script)
Supabase JS client (for seeding and assertions)
apis
CertificationExpiryCron Edge Function (HTTP POST)
Supabase PostgREST (for seed and assertion queries)
cron_audit_log table
data models
certifications
certification_idempotency_log
cron_audit_log
performance requirements
Seed and teardown operations must each complete in under 5 seconds
The cron function invocation must complete within 30 seconds on local Supabase — use a generous timeout in the HTTP client
security requirements
Test must use the local Supabase `service_role` key from `supabase/config.toml` — never a production key
All seeded records must use clearly fake UUIDs (e.g. `00000000-0000-0000-0000-000000000001`) to prevent accidental collision with real data

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Use the Supabase JS client with the local URL (`http://localhost:54321`) and service_role key from environment variable `SUPABASE_SERVICE_ROLE_KEY` loaded from `.env.test`. Trigger the Edge Function via `fetch('http://localhost:54321/functions/v1/certification-expiry-cron', { method: 'POST', headers: { Authorization: 'Bearer ${serviceKey}' } })` — no Supabase client wrapper for the invocation, raw HTTP is clearer for integration tests. For seeding, insert directly into the `certifications` table using the admin client, setting `expiry_date` to `new Date(Date.now() - 86400000).toISOString()` for an expired record. Assert DB state with a `select` query after each cron invocation rather than inspecting logs — logs are observable side effects, DB state is the source of truth.

For the idempotency assertion, count rows in `certification_idempotency_log` where `certification_id IN (test IDs)` before and after the second run and assert the counts are equal. Keep the test script self-contained with no external test framework dependencies beyond Deno's built-in `assert` module to minimise setup friction.

Testing Requirements

This task is an integration test, not a unit test. It requires a running local Supabase instance (`supabase start`). The test runner is Deno (`deno test --allow-net --allow-env`). Structure: (1) `beforeAll`: run migrations, seed test data via Supabase admin client.

(2) Test case A: invoke cron, assert DB state post-run. (3) Test case B: invoke cron again (idempotency), assert no changes. (4) Test case C: manually insert a missed-pause scenario (set expiry_date in past, no idempotency record, status='active'), invoke cron, assert audit log captures the miss. (5) `afterAll`: delete all rows with the test UUIDs.

Add this test to the CI pipeline in a separate job that starts Supabase CLI as a service container. Document the local setup steps in a `tests/integration/README.md` so any developer can run it independently.

Component
Certification Expiry Nightly Cron Job
infrastructure medium
Epic Risks (2)
medium impact low prob technical

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.

low impact medium prob technical

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.