Implement course enrollment prompt service — base structure
epic-certificate-expiry-notifications-orchestration-services-task-011 — Scaffold the course enrollment prompt service with its public interface: a method that accepts a mentor ID and certification type and returns a fully constructed course enrollment prompt payload. Define the prompt data model including mentor identity, certification type, pre-populated enrollment URL, and deep-link scheme used by the notification system.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Implement as a plain TypeScript class (not a singleton) so the Supabase client can be injected in tests. Place the module at `supabase/functions/_shared/course-enrollment-prompt-service.ts` so it can be imported by multiple edge functions without duplication. Define `EnrollmentPromptPayload` and the two error classes in a co-located `types.ts` file. The `enrollmentUrl` field should initially be populated with a clearly marked placeholder string (`TODO:task-012`) so downstream consumers that depend on task-011 can integrate without waiting for URL generation logic.
The deep-link prefix should be read from a Deno environment variable (`APP_DEEP_LINK_SCHEME`) so it is configurable per environment.
Testing Requirements
Unit tests using Deno's built-in test runner (`Deno.test`). Test file at `functions/course-enrollment-prompt-service/service.test.ts`. Required scenarios: (1) happy-path returns fully populated `EnrollmentPromptPayload` with correct shape; (2) unknown `mentorId` throws `MentorNotFoundError` with the provided ID in the message; (3) unrecognised `certType` string throws `UnknownCertTypeError`; (4) `issuedAt` field is a valid ISO 8601 string. Mock the Supabase client via dependency injection to avoid live DB calls.
Target 100% branch coverage on the service module.
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.
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.
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.
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.