critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

updateLastContactDate(String assignmentId, DateTime date) updates last_contact_date on the assignments row and returns the updated AssignmentContactSummary
updateLastContactDate throws AssignmentNotFoundException if assignmentId does not exist or is not accessible to the current user
fetchOverdueAssignments(String orgId, int thresholdDays) returns a List<OverdueAssignment> where last_contact_date < now() - thresholdDays days OR last_contact_date IS NULL (never contacted), scoped to orgId
fetchOverdueAssignments returns an empty list (not an exception) when no overdue assignments exist
fetchAssignmentContactSummary(String assignmentId) returns an AssignmentContactSummary with assignmentId, peerId, orgId, lastContactDate (nullable), and daysSinceContact (nullable, computed in Dart)
fetchAssignmentContactSummary throws AssignmentNotFoundException if the row is not accessible
All queries include org_id scoping as defense-in-depth (in addition to RLS)
fetchOverdueAssignments query uses the compound index (org_id, last_contact_date) — verified via EXPLAIN in test environment
daysSinceContact in AssignmentContactSummary is computed client-side as (DateTime.now().difference(lastContactDate).inDays) and is null when lastContactDate is null
Repository is provided via Riverpod and unit tests pass with mocked Supabase client

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Flutter SDK
apis
Supabase PostgREST (assignments table)
data models
assignments
AssignmentContactSummary (domain model)
OverdueAssignment (domain model)
performance requirements
fetchOverdueAssignments uses the compound index on (org_id, last_contact_date) — query must filter by org_id first to leverage the index
Overdue query: `.lte('last_contact_date', DateTime.now().subtract(Duration(days: thresholdDays)).toIso8601String())` combined with `.or('last_contact_date.is.null')` — test this PostgREST OR syntax carefully
Select only required columns (id, peer_mentor_id, org_id, last_contact_date) — do not SELECT * to avoid over-fetching sensitive assignment data
security requirements
updateLastContactDate must validate that the authenticated user owns the assignment (peer mentor role) or is a coordinator in the same org — enforced by RLS, but repository should not bypass this
fetchOverdueAssignments is typically called by coordinator role — if called by peer mentor role, RLS will automatically scope to their own rows
Do not expose last_contact_date of other peer mentors' assignments to peer mentors — rely on RLS from migration task-002
assignmentId is treated as opaque — do not log full assignment content in error messages

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

The PostgREST OR filter for 'overdue OR null' requires careful syntax: `.or('last_contact_date.lte.${threshold.toIso8601String()},last_contact_date.is.null')` — test this explicitly as PostgREST OR with IS NULL is a common gotcha. Define two domain models: `OverdueAssignment` (contains assignmentId, peerId, orgId, lastContactDate, daysSinceContact) and `AssignmentContactSummary` (same fields plus a label/status field). Compute daysSinceContact in Dart, not in SQL, to keep the repository logic simple and testable. For updateLastContactDate, use `.update({'last_contact_date': date.toIso8601String()}).eq('id', assignmentId).select()` — the `.select()` after `.update()` returns the updated row, allowing immediate construction of AssignmentContactSummary without a second round-trip.

thresholdDays in fetchOverdueAssignments should come from ReminderConfigRepository — the calling BLoC/use case should fetch config first and pass thresholdDays in. Do not fetch config inside this repository (separation of concerns). Consider adding a `watchOverdueCount(String orgId, int thresholdDays)` Stream method in a follow-up task for live coordinator dashboard badge updates.

Testing Requirements

Unit tests with mocked SupabaseClient (flutter_test): (1) updateLastContactDate: mock returns updated row, assert AssignmentContactSummary with correct lastContactDate; (2) updateLastContactDate: mock returns empty (0 rows), assert AssignmentNotFoundException; (3) fetchOverdueAssignments with thresholdDays=10: assert query filters by org_id AND (last_contact_date <= threshold OR last_contact_date IS NULL); (4) fetchOverdueAssignments returns empty list when mock returns []; (5) fetchOverdueAssignments returns correct OverdueAssignment list with daysSinceContact computed correctly; (6) fetchAssignmentContactSummary: mock returns row with null last_contact_date, assert daysSinceContact is null; (7) fetchAssignmentContactSummary: mock returns row with last_contact_date 5 days ago, assert daysSinceContact=5; (8) fetchAssignmentContactSummary: mock returns empty, assert AssignmentNotFoundException; (9) Riverpod Provider resolves in ProviderContainer. Integration test against test Supabase project: assert overdue query returns only assignments older than threshold, never contacts from other orgs.

Epic Risks (3)
high impact medium prob integration

Adding last_contact_date to the assignments table may conflict with existing RLS policies or trigger-based logic that monitors the assignments table. If the migration is not carefully reviewed, existing assignment management features could break in production.

Mitigation & Contingency

Mitigation: Review all existing triggers, policies, and foreign key constraints on the assignments table before writing the migration. Run the migration against a staging Supabase instance with production-like data and execute the full existing test suite before merging.

Contingency: Roll back the migration using Supabase's versioned migration history. Apply the schema change as an additive-only migration (nullable column with default) to ensure zero downtime and reversibility.

medium impact medium prob dependency

The PushNotificationService wraps an existing FCM integration whose internal API contract may have changed or may not expose the payload formatting required for deep-link CTAs. Misalignment discovered late delays the dispatch service epic.

Mitigation & Contingency

Mitigation: Before implementing the wrapper, read the existing push notification integration code and confirm the method signatures, payload structure, and token management model. Agree on a stable interface contract in a shared Dart abstract class.

Contingency: If the existing service is incompatible, implement a thin adapter layer that translates reminder payloads to the existing service's format, isolating the reminder feature from upstream changes.

high impact low prob security

Incorrect RLS policies on notification_log could allow coordinators to read reminder records belonging to peer mentors in other chapters, exposing sensitive assignment information across organisational boundaries.

Mitigation & Contingency

Mitigation: Write explicit RLS policies with integration tests that assert cross-chapter queries return zero rows. Use Supabase's built-in auth.uid() and join through the org membership tables to scope all queries.

Contingency: If a policy gap is discovered post-merge, immediately disable the affected table's SELECT policy, deploy a corrected policy, and audit recent queries in Supabase logs for any cross-boundary reads.