critical priority medium complexity database pending database specialist Tier 1

Acceptance Criteria

insertOrIgnore(record) inserts a new NotificationRecord when no row exists for (mentor_id, notification_type, threshold_days, date_window) and returns the inserted record with a populated id and created_at
insertOrIgnore(record) returns the existing record unchanged (no update) when a duplicate key combination already exists — it never throws a unique constraint violation
updateAcknowledgement(recordId, acknowledgedAt) sets acknowledged_at on the target row and updates updated_at; returns the updated record
getUnacknowledgedForMentor(mentorId) returns all records for that mentor where acknowledged_at IS NULL, ordered by created_at ASC
getByKey(mentorId, notificationType, thresholdDays, dateWindow) returns the matching record or null — never throws on a miss
All timestamps (created_at, updated_at, acknowledged_at) are stored and returned as UTC ISO-8601 strings
The repository is defined as an abstract Dart class with a concrete Supabase implementation
All Supabase errors are caught and re-thrown as typed NotificationRecordException domain errors

Technical Requirements

frameworks
supabase_flutter
Dart async/await
apis
Supabase PostgREST API (notification_records table)
Supabase upsert with ignoreDuplicates option
data models
NotificationRecord (id, mentor_id, notification_type, threshold_days, date_window, acknowledged_at, created_at, updated_at)
NotificationRecordException (domain error wrapper)
performance requirements
Unique index on (mentor_id, notification_type, threshold_days, date_window) to enforce idempotency at the database level
getUnacknowledgedForMentor must complete in under 100ms for a mentor with up to 100 unacknowledged records
security requirements
mentor_id filter must be enforced at the database level via RLS — a mentor may only read their own notification records
The orchestrator (service role context) must be able to insert records for any mentor; the Flutter client (mentor context) may only read and acknowledge their own records
No unauthenticated access to notification_records table

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use Supabase's .upsert(data, onConflict: 'mentor_id,notification_type,threshold_days,date_window', ignoreDuplicates: true) to achieve idempotency without application-level locking. The date_window field should be a date string (YYYY-MM-DD) representing the calendar date the notification belongs to, not a timestamp — this prevents the same-day cron run from creating duplicates if it runs multiple times. The unique constraint in the migration should be: ALTER TABLE notification_records ADD CONSTRAINT uq_notification_per_mentor_day UNIQUE (mentor_id, notification_type, threshold_days, date_window). Define the abstract class NotificationRecordRepository separate from SupabaseNotificationRecordRepository and register both with Riverpod.

Use Riverpod's overrideWithValue in tests to inject the mock implementation without modifying production code.

Testing Requirements

Unit tests: mock the Supabase client to verify upsert is called with ignoreDuplicates: true, update targets the correct row id, and errors are wrapped in NotificationRecordException. Integration tests against a Supabase test project: seed duplicate scenarios and verify insertOrIgnore never creates a second row; verify acknowledgement sets acknowledged_at correctly; verify getUnacknowledgedForMentor excludes acknowledged records and respects RLS (a mentor cannot read another mentor's records). Assert correct ordering of results. Minimum 90% branch coverage.

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.