critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

Abstract class `NotificationRecordRepository` defines the full interface: createRecord, getRecordsForRecipient, markAsRead, markAllAsRead
Concrete class `SupabaseNotificationRecordRepository` implements all interface methods
`NotificationRecord` is an immutable Dart class with fields: id, recipientId, notificationType, thresholdDays, title, body, isRead, createdAt, referenceId
`NotificationRecord.fromJson` parses all fields including nullable thresholdDays and referenceId without throwing
`NotificationRecord.toJson` produces a Supabase-compatible map (snake_case keys, null fields omitted or explicitly null as required)
`createRecord(NotificationRecord)` performs an insert and returns the created record with server-assigned id and createdAt
`getRecordsForRecipient(String recipientId, {int limit, int offset})` returns paginated results ordered by createdAt DESC
`markAsRead(String notificationId)` updates only the is_read column to true and returns the updated record
`markAllAsRead(String recipientId)` bulk-updates all unread records for the recipient and returns the count updated
All methods throw a typed `NotificationRecordException` (not a generic Exception) on Supabase errors
Riverpod `Provider<NotificationRecordRepository>` is defined
Network timeout errors are caught and rethrown as `NotificationRecordNetworkException` to allow UI distinction between server errors and connectivity issues

Technical Requirements

frameworks
Flutter
Riverpod
supabase_flutter
apis
Supabase PostgREST (table: notification_records)
data models
NotificationRecord
NotificationThreshold (enum)
NotificationRecordException
performance requirements
getRecordsForRecipient must default to limit=20 to prevent large payload fetches on notification inbox open
markAllAsRead must be a single bulk UPDATE, not an N-row UPDATE loop
security requirements
Repository uses the authenticated Supabase client — RLS enforces recipient isolation at DB level
createRecord must not accept a recipient_id from the caller when called from client context — derive it from the current session's auth.uid(); only edge functions pass an explicit recipient_id

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Place files in `lib/features/notifications/data/`. Define `NotificationThreshold` as a Dart enum with values `days60`, `days30`, `days7` and a `toInt()` extension method for serialization — this prevents raw int magic numbers throughout the codebase. For `createRecord`, use Supabase's `.insert(record.toJson()).select().single()` to retrieve the server-generated `id` and `created_at` in a single round-trip. For `markAllAsRead`, use `.update({'is_read': true}).eq('recipient_id', recipientId).eq('is_read', false)` — the Supabase Dart client does not return affected row count from UPDATE by default; use `.select('id')` chained to count the returned IDs if the count is needed.

Handle the `AuthException` case where the session has expired — catch it and rethrow as `NotificationRecordAuthException` so the BLoC can redirect to login.

Testing Requirements

Unit tests with mocked Supabase client covering: (1) createRecord calls insert with correct payload and returns deserialized record; (2) getRecordsForRecipient passes correct order (created_at desc) and limit/offset; (3) markAsRead sends update only to is_read column with correct filter; (4) markAllAsRead sends bulk update filtered by recipient_id and is_read=false; (5) PostgrestException is caught and rethrown as NotificationRecordException; (6) fromJson handles null thresholdDays (non-expiry notification) without throwing; (7) toJson omits null fields correctly. Target 90%+ line coverage on the repository implementation class.

Component
Notification Record Repository
data medium
Epic Risks (3)
high impact medium prob technical

The RLS policy predicate that checks certification_expiry_date and suppression_status on every coordinator list query could cause full table scans at scale, degrading response time for coordinator contact list screens across all chapters.

Mitigation & Contingency

Mitigation: Add a partial index on (certification_expiry_date, suppression_status) filtered to active mentors. Benchmark the policy predicate against a representative data set (500+ mentors) during development using EXPLAIN ANALYZE on Supabase staging.

Contingency: If the index does not resolve the performance issue, introduce a computed boolean column is_publicly_visible that is updated by the mentor_visibility_suppressor service and indexed separately, shifting the predicate cost to write time rather than read time.

medium impact medium prob integration

FCM device tokens become invalid when users reinstall the app or switch devices. If the token management strategy does not handle token refresh reliably, notification delivery will silently fail for a significant portion of the user base without surfacing errors.

Mitigation & Contingency

Mitigation: Implement the FCM token refresh callback in the Flutter client to upsert the latest token to Supabase on every app launch. Store token with a last_refreshed_at timestamp. The FCM sender should handle UNREGISTERED error codes by deleting stale tokens.

Contingency: If token staleness becomes widespread, add a token health check that forces re-registration during the expiry check edge function run by querying mentors whose token was last refreshed more than 30 days ago and triggering a silent push to prompt re-registration.

medium impact low prob integration

The certification expiry and notification record tables may have column naming or constraint conflicts with existing tables in the peer mentor status and certification management features, causing migration failures in shared Supabase environments.

Mitigation & Contingency

Mitigation: Audit existing table schemas for user_roles, certifications, and notification tables before writing migrations. Prefix new columns with expiry_ to avoid collisions. Run migrations against a clean Supabase branch environment before merging.

Contingency: If a conflict is found post-merge, apply ALTER TABLE migrations to rename conflicting columns and issue a hotfix migration. Communicate schema changes to all dependent feature teams via a shared migration changelog.