critical priority low complexity backend pending backend specialist Tier 2

Acceptance Criteria

SchedulerRunResult is a Dart class (or freezed data class) with four non-nullable int fields: totalEvaluated, remindersDispatched, escalationsDispatched, skipped
SchedulerRunResult provides a copyWith method and equality based on field values (use freezed or implement == and hashCode manually)
SchedulerRunResult.empty() factory constructor returns a zero-value instance for accumulator initialisation
Abstract ReminderSchedulerService interface declares exactly one method: Future<SchedulerRunResult> runDailyEvaluation()
Interface is placed in the domain layer (lib/domain/services/) and has no imports from infrastructure or data layers
SchedulerRunResult is placed in lib/domain/models/ alongside other domain types
Both files are exported from the domain layer barrel file (lib/domain/domain.dart or equivalent)
Code compiles with zero errors and zero warnings under npm run build equivalent (flutter pub get && dart analyze)

Technical Requirements

frameworks
Flutter
Dart
freezed (optional but recommended)
data models
SchedulerRunResult

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

If the project already uses freezed for other domain models, generate SchedulerRunResult with @freezed to get equality, copyWith, and toString for free — this also makes it trivially serialisable for logging. If freezed is not in use, implement a plain Dart class with final fields, a const constructor, copyWith, and override == and hashCode using all four count fields. Keep the abstract interface minimal — a single runDailyEvaluation() method. Resist adding overloads or optional parameters at this stage; the concrete implementation task (task-011) will reveal any needed flexibility.

Place the interface in the domain layer with no framework imports so it remains portable and independently testable.

Testing Requirements

Minimal unit tests using flutter_test: (1) SchedulerRunResult.empty() returns all-zero counts; (2) copyWith correctly overrides individual fields; (3) two SchedulerRunResult instances with identical values are equal. No mocks needed. Tests should live in test/domain/models/scheduler_run_result_test.dart.

Component
Reminder Scheduler Service
service high
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.