high priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

Builder produces a distinct title and body string for each of the four threshold tiers (60-day, 30-day, 7-day, lapsed) — all four are different and human-readable in Norwegian (Bokmål)
Each payload includes mentor display name, certificate type label, and expiry date formatted as 'DD. MMMM YYYY' in Norwegian locale
Each payload includes a deep-link URI in the 'data' payload field pointing to the certificate renewal screen, e.g. '/certificate/renew/{certificateId}'
The lapsed-tier payload uses urgency language and omits the expiry date phrase 'expires on' — it uses 'expired on' past tense
Builder never mutates the input mentor record; it returns a new immutable FCMMessagePayload value object
Deep-link URI is constructed only from validated certificate ID — builder throws ArgumentError if certificateId is null or empty
Unit tests assert exact title and body strings for each tier, correct date formatting, correct deep-link construction, and ArgumentError on invalid certificateId

Technical Requirements

frameworks
Flutter (Dart)
apis
Firebase Cloud Messaging HTTP v1 API (notification and data payload fields)
data models
CertificateExpiryNotification
NotificationThresholdTier
FCMMessagePayload
MentorRecord
performance requirements
Payload construction is synchronous and must complete in under 1ms per record — no async calls allowed in builder
security requirements
Mentor name included in push payload is display name only — never include national ID, address, or diagnosis in FCM payload
Deep-link URI must not contain authentication tokens or session data

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Implement as a stateless Dart class with a single build(MentorRecord record, NotificationThresholdTier tier) method returning FCMMessagePayload. Define all message template strings as private constants at the top of the class for easy localisation review. Use the intl package (already likely present for date formatting) with a fixed 'nb_NO' locale — do not rely on device locale for server-side payload construction. FCMMessagePayload should be a Dart sealed class or a simple data class with: title (String), body (String), data (Map).

The deep-link scheme should match the Flutter app's registered URI scheme (confirm with frontend team before finalising). Keep lapsed-tier wording distinct enough to convey urgency without being alarmist — coordinate copy with product owner.

Testing Requirements

Unit tests (flutter_test): one test per threshold tier asserting exact title text, body text, date format, and deep-link value. Test date formatting with a fixed DateTime (e.g. 2026-06-15) to avoid locale flakiness. Test ArgumentError thrown when certificateId is empty string and when null.

Test that mentor name and certificate type appear verbatim in body. No integration tests required for this builder — it is pure logic. Coverage target: 100% of builder methods.

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.