medium priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

The `buildPrompt` method now returns a fully resolved, non-placeholder `enrollmentUrl` that is a valid deep-link URI (e.g. `likeperson://course-admin?mentorId=...&certType=...&sig=...`)
The URL includes query parameters: `mentorId` (UUID), `certType` (enum string), `exp` (Unix expiry timestamp — 48 h from generation), and `sig` (HMAC-SHA256 signature over `mentorId|certType|exp` using server-side signing secret)
Signature verification logic is implemented in a separate exported `verifyEnrollmentUrl(url: string): boolean` function that can be used by the app's deep-link handler to reject tampered URLs
The signing secret is read from the Deno environment variable `ENROLLMENT_URL_SIGNING_SECRET` — an empty or missing secret causes the function to throw at startup, not silently generate unsigned URLs
Generated URLs do not include any PII beyond the opaque UUID `mentorId` — no names, emails, or personnummer appear in the URL
Expired URLs (`exp` in the past) return `false` from `verifyEnrollmentUrl` without throwing
Unit tests cover: valid URL structure, correct parameter encoding, signature correctness, expired URL rejection, and tampered-parameter rejection

Technical Requirements

frameworks
Deno (Supabase Edge Function runtime)
Web Crypto API (Deno built-in, for HMAC-SHA256)
apis
Deno `crypto.subtle` for HMAC-SHA256 signing — no external crypto library needed
data models
certification (cert_type enum)
certification (peer_mentor_id — source of mentorId parameter)
performance requirements
URL generation (including HMAC signing) must complete in under 10 ms — pure CPU operation, no I/O
No additional database queries beyond those already made in task-011 mentor identity fetch
security requirements
HMAC-SHA256 with a minimum 256-bit secret stored in Supabase Edge Function environment secrets vault — never hardcoded
URL expiry enforced server-side at deep-link handling time — client-side expiry is advisory only
No PII in URL query parameters — `mentorId` UUID is opaque to external observers
ENROLLMENT_URL_SIGNING_SECRET must differ per environment (dev/staging/prod) to prevent cross-environment replay

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Use `crypto.subtle.importKey` and `crypto.subtle.sign` from Deno's Web Crypto API to avoid introducing third-party dependencies. The signature should be computed as `HMAC-SHA256(base64url(mentorId) + '|' + certType + '|' + exp.toString())` — document the exact canonical form in a comment so the Flutter deep-link handler can replicate it. Store the URL builder and verifier in a co-located `url-builder.ts` file within the shared module rather than inline in the service class, to allow independent testing and future reuse. The 48-hour expiry window is appropriate since enrollment prompts are tied to daily cron runs; document this assumption in a comment near the constant.

Testing Requirements

Unit tests using Deno's built-in test runner. Test file at `functions/_shared/course-enrollment-prompt-service.url.test.ts`. Required scenarios: (1) generated URL parses as valid URI with all required parameters present; (2) `verifyEnrollmentUrl` returns `true` for freshly generated URL; (3) altering any query parameter causes `verifyEnrollmentUrl` to return `false`; (4) URL with `exp` in the past returns `false` from verify; (5) missing `ENROLLMENT_URL_SIGNING_SECRET` env var throws at module initialisation. All tests must run without a live Supabase connection — inject the signing secret via test environment variables.

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.