medium priority medium complexity integration pending integration specialist Tier 4

Acceptance Criteria

The expiry notification orchestrator calls `CourseEnrollmentPromptService.buildPrompt(mentorId, certType)` for every mentor record in both the `lapsed` and `7-day-warning` tiers before constructing notification payloads
The FCM push notification payload's `data` object includes a `enrollmentUrl` key containing the signed deep-link URL returned by the prompt service
The in-app notification record persisted to the database includes the `enrollmentUrl` in its `action_url` or equivalent JSON metadata field
Mentors in the `30-day` and `60-day` warning tiers do NOT receive enrollment deep links in their payloads — only lapsed and 7-day records include them
A failure in `buildPrompt` for a single mentor (e.g. mentor not found) logs a warning and skips the enrollment URL for that mentor without aborting the orchestrator run for other mentors
Integration test: mocking the prompt service to return a fixed URL verifies that the FCM payload and database record both contain the expected URL
No additional database round-trips are introduced — the mentor identity already fetched by the orchestrator is passed to the prompt service to avoid duplicate queries

Technical Requirements

frameworks
Deno (Supabase Edge Function runtime)
Supabase Edge Functions TypeScript
apis
Firebase Cloud Messaging (FCM) API v1 — for push payload construction
Supabase service-role client — for persisting in-app notification records
data models
certification (cert_type, peer_mentor_id, expiry date for tier determination)
device_token (FCM token per mentor for push targeting)
performance requirements
Prompt service calls must be batched or parallelised using `Promise.all` across mentor records in a tier — no sequential await loop
Total added latency from enrollment URL generation must remain under 50 ms per batch of 20 mentors
security requirements
FCM server key used only inside Edge Function environment — never returned to the mobile client
Signed enrollment URLs included in FCM `data` payload (not `notification` body) to prevent URL leakage in notification centre previews on locked screens
In-app notification records storing `enrollmentUrl` are subject to RLS — only the target mentor can read their own notification records

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Inject `CourseEnrollmentPromptService` into the orchestrator via constructor parameter (not a module-level import) to keep the integration testable without a live Supabase connection. Use `Promise.allSettled` rather than `Promise.all` when generating enrollment URLs for a batch of mentors — this ensures one failure does not reject the entire batch. Wrap each `buildPrompt` call in a try/catch; on error, log `{ mentorId, error: e.message }` at `warn` level and continue with `enrollmentUrl: null`. The FCM payload builder should conditionally include `enrollmentUrl` only when non-null, keeping payloads lean for tiers that do not need it.

Testing Requirements

Integration tests using Deno test runner with dependency injection for both FCM client and Supabase client. Required scenarios: (1) lapsed-tier mentor receives FCM payload with `enrollmentUrl` present; (2) 7-day-tier mentor receives FCM payload with `enrollmentUrl` present; (3) 30-day and 60-day tier mentors receive FCM payloads WITHOUT `enrollmentUrl`; (4) prompt service failure for one mentor in a batch does not prevent other mentors from receiving notifications; (5) in-app notification record in database contains `enrollmentUrl` for lapsed and 7-day tiers. Mock both `CourseEnrollmentPromptService` and FCM client — no live network calls in tests.

Component
Course Enrollment Prompt Service
service low
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.