high priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

A CertificateExpiryThreshold enum (or sealed class) defines exactly three upcoming tiers: days60, days30, days7, plus a lapsed tier for already-expired certificates
Each tier exposes a daysUntilExpiry int getter returning 60, 30, 7, or 0 (lapsed), and a notificationType string matching the value used in FCM payloads and notification records
A CertificateExpiryThresholdEvaluator.evaluate(daysUntilExpiry) static method returns the correct CertificateExpiryThreshold for any integer input, or null if the value does not match any configured tier
evaluate(-5) returns CertificateExpiryThreshold.lapsed; evaluate(7) returns days7; evaluate(30) returns days30; evaluate(60) returns days60; evaluate(45) returns null (no tier defined for 45 days)
The constants file is the single source of truth — no other file in the codebase contains the literal values 60, 30, or 7 in the context of expiry thresholds
The evaluator and constants are importable from both the Dart Flutter codebase and the Supabase Edge Function TypeScript context (or the TypeScript equivalent is defined in a shared config file consumed by the Edge Function)
Unit tests cover all four tiers plus at least two out-of-tier values confirming null is returned

Technical Requirements

frameworks
Dart (enum or sealed class pattern)
TypeScript/Deno (for Edge Function config mirror, if needed)
data models
CertificateExpiryThreshold (enum/sealed class)
CertificateExpiryThresholdEvaluator (static utility)
performance requirements
Threshold evaluation is a pure synchronous function with O(1) complexity — no database calls
security requirements
No sensitive data in the constants file — only numeric day values and string identifiers

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use a Dart enum with methods rather than a class with static constants — enums are exhaustively matched in switch expressions which makes the orchestrator code safer (the compiler warns on missing cases). Define the enum as: enum CertificateExpiryThreshold { days60, days30, days7, lapsed } with extension methods for daysUntilExpiry and notificationType. The evaluator's evaluate method should use a sorted list lookup: check if daysUntilExpiry < 0 → lapsed, == 7 → days7, == 30 → days30, == 60 → days60, else null. If the Edge Function needs the same constants, create a shared TypeScript config file at supabase/functions/_shared/expiryThresholds.ts with the same values.

This avoids magic numbers in both the Flutter orchestrator and the Deno edge function, and provides a single place to add a future 14-day tier.

Testing Requirements

Unit tests only. Test every enum variant's daysUntilExpiry and notificationType properties. Test CertificateExpiryThresholdEvaluator.evaluate with: exact boundary values (60, 30, 7, 0, -1), values between tiers (45, 15), and extreme values (365, -365). Confirm null is returned for all non-tier values.

Tests should be pure Dart with no dependencies on Supabase or Firebase. Target 100% branch coverage (trivially achievable for this utility).

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.