critical priority medium complexity backend pending backend specialist Tier 4

Acceptance Criteria

Batch size is configurable via ReminderConfigRepository (not hard-coded) and defaults to a safe fallback value (e.g., 50) if config is absent
A scheduler_run_log table (or equivalent Supabase record) is checked before starting the run; if a record with today's ISO date already exists and status = completed, the method returns the previous SchedulerRunResult immediately without processing any assignments
Scheduler idempotency record is created with status = in_progress at the start of the run before any assignments are fetched
Idempotency record is updated to status = completed with the final SchedulerRunResult JSON at the end of a successful run
Idempotency record is updated to status = failed with error details if the run throws an unhandled exception
A structured log entry is emitted at the start of each batch with: batch number, batch size, and cumulative assignments processed so far
A structured log entry is emitted at the end of the full run with the complete SchedulerRunResult fields as key-value pairs
Duplicate cron trigger within the same calendar day returns SchedulerRunResult from the completed record without emitting batch-progress logs (it is a cache-hit path)
All new behaviour is covered by unit tests with mocked dependencies

Technical Requirements

frameworks
Flutter
Dart
Riverpod
apis
Supabase PostgREST REST API
data models
SchedulerRunLog
ReminderConfig
SchedulerRunResult
performance requirements
Idempotency check is a single Supabase query on an indexed date column and must complete in under 100ms
Batch size should be tunable between 10 and 200 without code changes
security requirements
scheduler_run_log table must be accessible only by the service-role key — not by authenticated users
RLS policies on scheduler_run_log must deny all non-service-role access

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Add a SchedulerRunLogRepository abstraction to the domain layer with methods: findRunByDate(DateTime date), createRun(DateTime date), markCompleted(String runId, SchedulerRunResult result), markFailed(String runId, String error). The concrete implementation uses a Supabase table with columns: id (uuid), run_date (date, unique), status (text), result_json (jsonb nullable), error (text nullable), created_at (timestamptz). Use an upsert with on_conflict=run_date to handle any race between two concurrent invocations. For the in_progress case, decide upfront: either treat it as a skip (safe, idempotent) or raise a SchedulerAlreadyRunningException.

Documenting this decision in a code comment is sufficient — avoid over-engineering a distributed lock. The batch progress log should use the same ReminderLogger abstraction introduced in task-008 to ensure consistency.

Testing Requirements

Unit tests using flutter_test with mocktail. Required scenarios: (1) first run of the day — idempotency record created as in_progress, assignments processed, record updated to completed; (2) duplicate run same day — completed record found, previous SchedulerRunResult returned immediately, no assignments fetched; (3) in_progress record found (concurrent run) — method waits or returns early with a typed ConflictResult (decide and document); (4) batch progress logs emitted once per batch with correct batch index; (5) run completion log contains correct SchedulerRunResult values; (6) failed run updates idempotency record to failed status. Verify via mock that AssignmentContactTrackingRepository.fetchOpenAssignments is never called in the duplicate-run scenario.

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.