critical priority medium complexity integration pending backend specialist Tier 2

Acceptance Criteria

A callReminderService(payload: ReminderPayload) function is exported from supabase/functions/certification-expiry-cron/services.ts
A callManagementService(payload: ManagementPayload) function is exported from the same services.ts module
For each certification in the expiring30, expiring14, and expiring7 windows, callReminderService is invoked with a payload containing { certification_id, peer_mentor_id, organization_id, expires_at, days_until_expiry }
For each certification in the expiredToday window, callManagementService is invoked with a payload containing { certification_id, peer_mentor_id, organization_id, expires_at }
Service calls use Supabase RPC (supabase.rpc()) or direct Edge Function invocation (supabase.functions.invoke()) — not raw fetch to production service URLs
Each invocation result is logged at INFO level: certification_id, service called, HTTP status code, and elapsed time in ms
If a service call returns a non-2xx HTTP status, the error is logged at ERROR level with the full response body, but processing continues for remaining certifications in the batch
If a service call throws a network-level exception, it is caught, logged, and the batch continues — no single failure aborts the entire run
The orchestrateExpiryCheck() function in index.ts calls queryExpiringCertifications and then fans out to the service calls in the correct mapping (reminder for 30/14/7, management for expired)
A Deno unit test in services_test.ts mocks the Supabase client and asserts callReminderService is called once per certification in each reminder window with the correct payload shape
A Deno unit test asserts callManagementService is called once per certification in the expiredToday window with the correct payload shape

Technical Requirements

frameworks
Supabase Edge Functions (Deno)
supabase-js v2
apis
CertificationReminderService (internal Supabase RPC or Edge Function)
CertificationManagementService (internal Supabase RPC or Edge Function)
Supabase Edge Functions invocation API
data models
certification
performance requirements
Service calls for all certifications in a window should be dispatched in parallel using Promise.allSettled — not sequentially
Total service call phase must complete within 10 seconds for up to 500 certifications per window
security requirements
Service calls are server-to-server (Edge Function to Edge Function) using the service role key — not exposed to mobile clients
Payload must not include PII beyond the minimum required IDs and date fields
Function input validated before forwarding to downstream services — reject malformed payloads before any service call
Supabase service role key used for internal function invocation — never passed in payload to downstream services

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Implement service call fans-out using Promise.allSettled(certifications.map(cert => callReminderService(cert))) to process all certifications in a window in parallel. This is critical for performance at scale. Define typed payload interfaces (ReminderPayload, ManagementPayload) at the top of services.ts. Use a wrapper function wrapServiceCall(fn, payload) that handles try/catch and logging uniformly so each service call site stays clean.

For logging, structure log lines as JSON objects rather than plain strings — this enables log querying in Supabase Dashboard and external log aggregators. Example: console.log(JSON.stringify({ level: 'INFO', service: 'reminder', certification_id: cert.id, status: result.status, elapsed_ms: elapsed })). Ensure the cron function has the correct permissions to invoke other Edge Functions within the same Supabase project via the service role.

Testing Requirements

Write Deno unit tests in supabase/functions/certification-expiry-cron/services_test.ts. Use a stub Supabase client that captures rpc() or functions.invoke() calls. For each test, seed a mock ExpiryWindowResult with known certification records and assert the correct service is called with the correct payload for each record. Test the error-handling path: inject a stub that returns a 500 response for one invocation and assert that the remaining certifications in the batch are still processed.

Test the exception path: inject a stub that throws a network error for one invocation and assert the batch continues. Write an integration test using a local Supabase instance with real RPC functions stubbed to return success and assert the orchestrateExpiryCheck() function completes without throwing.

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.