critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

Concrete class `ReminderDispatchServiceImpl` implements `ReminderDispatchService` and is registered via Riverpod
`dispatchReminder()` composes a push notification with title 'Reminder: follow up on assignment' and a body referencing daysSinceContact, then delegates to `PushNotificationService.send()`
`dispatchEscalation()` composes a push notification addressed to the coordinator with peerMentorName and daysSinceContact in the body, then delegates to `PushNotificationService.send()`
Both methods persist an `InAppNotificationRecord` via `InAppNotificationRepository.save()` before returning
Both methods write a `reminder_sent_at` timestamp to the assignment record via `AssignmentContactTrackingRepository.recordReminderSent()` after successful dispatch
If push delivery fails, the in-app notification is still persisted and the error is logged but does not throw — push is best-effort
If in-app persistence fails, the error is thrown — in-app delivery is required
Duplicate dispatch within the same reminder window is prevented: if `reminder_sent_at` is already set within the current window, the method returns early without sending
All async operations use structured error handling with typed exceptions
The service is stateless — no mutable instance fields

Technical Requirements

frameworks
Flutter
Riverpod
Dart async/await
apis
PushNotificationService (internal — wraps FCM or Supabase push)
InAppNotificationRepository (internal — Supabase-backed)
AssignmentContactTrackingRepository (internal — records reminder_sent_at)
data models
PeerMentorReminderPayload
CoordinatorEscalationPayload
InAppNotificationRecord
Assignment
performance requirements
Push delivery and in-app persistence should be awaited sequentially (push first, then persist) to maintain audit order
reminder_sent_at write must complete before the method returns to guarantee idempotency
Total dispatch latency budget: under 2 seconds under normal Supabase + push service conditions
security requirements
Push notification content must not include sensitive personal data — only name, days count, and a deep link to the assignment
Deep links in push payloads must use app-scheme URIs that require authentication to resolve (no unauthenticated data exposure)
Row-level security on InAppNotificationRepository must scope inserts to the authenticated org
reminder_sent_at timestamp must be server-generated (Supabase `now()`) not client-provided, to prevent manipulation

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Structure the implementation as two private helpers `_sendPush()` and _`persistInApp()` called from each public method — this avoids duplication between reminder and escalation paths. For the push-is-best-effort pattern, wrap `PushNotificationService.send()` in a `try/catch` that logs the error via the logger abstraction and continues. The duplicate-window check should query `AssignmentContactTrackingRepository.getReminderSentAt(assignmentId)` and compare against the current window boundary (e.g., `remind_sent_at > now - reminderWindowDays`). Use Supabase's server-side `now()` function for the `reminder_sent_at` write by passing `DateTime.now().toUtc()` from the client — document that slight clock skew is acceptable here.

Notification message strings should be defined as constants in a `ReminderMessages` class to support future localization without touching service logic.

Testing Requirements

Unit tests with mocked dependencies covering: (1) successful reminder dispatch calls PushNotificationService and persists InAppNotificationRecord, (2) successful escalation dispatch calls PushNotificationService with coordinator payload and persists record, (3) push failure is swallowed and in-app record is still saved, (4) in-app persistence failure propagates as exception, (5) duplicate prevention: if reminder_sent_at is within window, neither push nor persistence is called. Integration test (optional, flagged with @Tags(['integration'])): verify end-to-end dispatch against a Supabase test instance with a test push token. Use flutter_test with Mockito fakes. Aim for 100% branch coverage on the dispatch guard (duplicate prevention) logic.

Component
Reminder Dispatch Service
service medium
Epic Risks (3)
medium impact high prob scope

The idempotency window (how long after a reminder is sent before another can be sent for the same assignment) is not explicitly specified. An incorrect window — too short, duplicate reminders appear; too long, a resolved and re-opened situation is not re-notified. This ambiguity could result in user-visible bugs post-launch.

Mitigation & Contingency

Mitigation: Before implementation, define the idempotency window explicitly with stakeholders: a reminder is suppressed if a same-type notification record exists with sent_at within the last (reminder_days - 1) days. Document this rule as a named constant in the service with a comment referencing the decision.

Contingency: If the window is wrong in production, it is a single constant change with a hotfix deployment. The notification_log table allows re-processing without data migration.

high impact medium prob technical

For organisations with thousands of open assignments (e.g., NHF with 1,400 chapters), the daily scheduler query over all open assignments could time out or consume excessive Supabase compute units, especially if the contact tracking query lacks proper indexing.

Mitigation & Contingency

Mitigation: Add a composite index on assignments(status, last_contact_date) before running performance tests. Use cursor-based pagination in the scheduler (query 500 rows at a time). Run a load test with 10,000 synthetic assignments as described in the feature documentation before merging.

Contingency: If the query is too slow for synchronous execution, move the evaluation to the Edge Function (cron trigger epic) and use Supabase's built-in parallelism. The service interface does not change, only the execution context.

medium impact medium prob integration

If the push notification service fails (FCM outage, invalid device token) during dispatch, the in-app notification may already be persisted but the push is silently lost. Inconsistent state makes it impossible to report accurate delivery status.

Mitigation & Contingency

Mitigation: Implement push dispatch and in-app persistence as separate operations with independent error handling. Record delivery_status as 'pending', 'delivered', or 'failed' on the notification_log row. Retry failed push deliveries up to 3 times with exponential backoff.

Contingency: If FCM is consistently unavailable, the in-app notification is still visible to the user, providing a degraded but functional fallback. Alert on consecutive push failures via the cron trigger's error logging.