critical priority low complexity backend pending backend specialist Tier 0

Acceptance Criteria

A sealed class `ReminderEvaluationResult` is defined in Dart with exactly three subclasses/variants: `ReminderEvaluationResultNone`, `ReminderEvaluationResultRemind`, and `ReminderEvaluationResultEscalate`
Each variant is a final class extending `ReminderEvaluationResult` and carries no extra mutable state unless explicitly required by downstream consumers
An abstract interface class `ReminderEvaluationService` is defined with a single method `Future<ReminderEvaluationResult> evaluate({required String assignmentId, required OrgReminderThresholds thresholds})`
An `OrgReminderThresholds` value object is defined with `remindAfterDays` (int) and `escalateAfterDays` (int) fields, both non-nullable and positive
All types are exported from a single barrel file (e.g., `reminder_evaluation.dart`) under the domain layer
No concrete implementation logic exists in this file — pure domain contract only
All types compile without error under `dart analyze` with no warnings
Equality and `toString` are implemented on `OrgReminderThresholds` for testing convenience

Technical Requirements

frameworks
Flutter
Dart sealed classes (Dart 3+)
data models
Assignment
OrgReminderThresholds
performance requirements
Types must be immutable value objects — no mutable fields
Sealed class exhaustiveness enables compiler-enforced switch coverage with zero runtime overhead
security requirements
No PII stored in domain types — assignment ID is an opaque reference only
OrgReminderThresholds must not expose raw org config beyond threshold integers

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use Dart 3 sealed classes (`sealed class ReminderEvaluationResult {}`) so the compiler enforces exhaustive pattern matching in all consumers — this eliminates silent unhandled cases at call sites. Place all types under `lib/domain/reminders/` following the project's existing domain layer conventions. Do not mix repository interfaces into this file; keep it a pure domain model file. `OrgReminderThresholds` should use `const` constructors to allow compile-time constant usage in tests.

Ensure `escalateAfterDays >= remindAfterDays` is documented as a precondition (enforce via assertion in debug mode).

Testing Requirements

Unit tests are not required for pure type definitions, but a compile-time test file should assert that exhaustive switch on `ReminderEvaluationResult` covers all three variants without a default case. Verify that `dart analyze` passes with zero issues. Include a brief smoke test instantiating each variant and asserting type identity.

Component
Reminder Evaluation 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.