critical priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

A `PeerMentorReminderPayload` value class is defined with fields: recipientUserId (String), assignmentId (String), daysSinceContact (int), orgName (String)
A `CoordinatorEscalationPayload` value class is defined with fields: recipientUserId (String), assignmentId (String), peerMentorName (String), daysSinceContact (int), orgName (String)
An `InAppNotificationRecord` model is defined with fields: id (String), recipientUserId (String), assignmentId (String), notificationType (enum: reminder | escalation), createdAt (DateTime), isRead (bool)
An abstract interface `ReminderDispatchService` is defined with: `Future<void> dispatchReminder(PeerMentorReminderPayload payload)` and `Future<void> dispatchEscalation(CoordinatorEscalationPayload payload)`
All types are exported from a single barrel file under the domain layer
No push-notification SDK specifics leak into the domain types — payloads are pure Dart value objects
All value types implement `==` and `hashCode` for testability
`InAppNotificationRecord` has a `copyWith()` method for immutable updates (e.g., marking as read)

Technical Requirements

frameworks
Flutter
Dart
data models
InAppNotificationRecord
PeerMentorReminderPayload
CoordinatorEscalationPayload
performance requirements
All types are immutable — no setters or mutable fields
Value objects use const constructors where possible
security requirements
Payload types must not contain full contact records or sensitive health information — only opaque IDs and display-safe strings
No authentication tokens or secrets may appear in payload fields
peerMentorName in CoordinatorEscalationPayload should be treated as display-name only, not a unique identifier

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Place all types under `lib/domain/reminders/dispatch/` to keep evaluation and dispatch concerns separated within the reminders domain. Use an enum for `notificationType` (not a string constant) to enable exhaustive switch in notification rendering code. The `InAppNotificationRecord.id` should be a UUID string generated at creation time — document this expectation but do not enforce UUID format in the domain type. `PeerMentorReminderPayload` and `CoordinatorEscalationPayload` are intentionally separate types (not a single generic payload) because their fields and semantics differ — do not collapse them.

This separation also ensures that future localization of push notification content can be handled independently per audience.

Testing Requirements

Compile-time verification that the interface is correctly defined. Smoke tests instantiating each payload type and asserting field values. Verify `==` and `hashCode` correctness on value types using standard equality assertions. Verify `copyWith()` on `InAppNotificationRecord` produces a new object with the updated field only.

No mocking required at this stage.

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.