high priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

deduplicateAgainstHistory(candidates, cooldownHours) method exists and accepts List<ScenarioPrompt> and optional int cooldownHours (default 72)
Method queries Prompt History Repository for records matching (peer_mentor_id, scenario_id) with dispatched_at >= (now - cooldownHours)
Any candidate whose (peer_mentor_id, scenario_id) pair exists in the cooldown window is removed from the returned list
Candidates not present in history (net-new) are returned unchanged
If candidates list is empty, the history query is skipped entirely and empty list is returned immediately
Cooldown window is configurable — passing cooldownHours=0 disables deduplication (returns all candidates, useful for testing)
Supabase query uses an IN clause on (peer_mentor_id, scenario_id) pairs to batch the lookup — no N+1 queries
Unit test: 3 candidates, 1 already in history within cooldown — assert 2 returned
Unit test: all candidates in history — assert empty list returned
Unit test: cooldownHours=0 — assert all candidates pass through regardless of history

Technical Requirements

frameworks
Flutter
Riverpod
supabase_flutter
apis
Supabase PostgreSQL 15 — scenario_prompt_history table (or equivalent Prompt History table)
data models
activity
performance requirements
Single batched Supabase query using .in_() filter — not one query per candidate
History lookup must complete under 500ms for up to 100 candidate pairs
Use .select('peer_mentor_id, scenario_id') with minimal column projection
security requirements
Prompt History table must have RLS — users can only read their own history records
Query scoped to authenticated user's organization via JWT claims — no cross-org history leakage
peer_mentor_id values must be validated as UUIDs before query construction

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Define a PromptHistoryRepository interface with: Future> getRecentHistory(List candidates, int cooldownHours). Inside SchedulerService.deduplicateAgainstHistory(), guard with if (candidates.isEmpty) return []. Build the cutoff timestamp as DateTime.now().toUtc().subtract(Duration(hours: cooldownHours)). For the Supabase query, extract unique peer_mentor_ids and scenario_ids and use two separate .in_() filters (Supabase PostgREST does not support composite IN).

Then do client-side Set intersection to build the 'already sent' set keyed on '$peerId:$scenarioId'. Filter candidates using candidates.where((c) => !alreadySentKeys.contains('${c.peerId}:${c.scenarioId}')).toList(). Keep repository injectable for unit testing.

Testing Requirements

Unit tests with flutter_test using a mocked PromptHistoryRepository. Test cases: (1) 3 candidates, mock returns 1 matching history record — assert 2 net-new candidates returned; (2) mock returns empty history — assert all 3 candidates returned; (3) mock returns history for all 3 — assert empty list; (4) empty candidates input — assert repository query method never called; (5) cooldownHours=0 — assert repository query skipped, all candidates returned. Integration test: seed prompt_history table with a record for (peer1, scenario-A) at T-48h, pass candidate for same pair with cooldownHours=72 — assert it is filtered. Pass candidate for same pair with cooldownHours=24 — assert it passes through.

Component
Scenario Prompt Scheduler Service
service high
Epic Risks (2)
high impact medium prob technical

If the scheduler runs concurrently (e.g., two overlapping cron invocations due to edge function retry), duplicate prompts could be dispatched before the first run's history records are committed, breaking the deduplication guarantee.

Mitigation & Contingency

Mitigation: Use a Postgres advisory lock or unique constraint on (user_id, scenario_id, activity_ref) in the prompt history table to make concurrent writes idempotent; design the scheduler to check history inside a transaction.

Contingency: If concurrency issues persist in production, add a distributed lock via Supabase Edge Function concurrency limit (max_instances=1) for the evaluation function as a hard guard.

medium impact medium prob scope

Coordinators may find scenario configuration unclear if trigger conditions are expressed as raw JSON or technical terminology, leading to misconfiguration and irrelevant prompts being sent to peer mentors.

Mitigation & Contingency

Mitigation: Design the ScenarioConfigurationScreen to display human-readable descriptions of each template's trigger condition (e.g., 'Send 3 days after first contact if wellbeing concern was flagged') rather than raw rule properties; validate with an HLF coordinator in a design review before implementation.

Contingency: If coordinators still misconfigure rules after launch, add a preview mode that shows a simulated prompt based on a test activity before the rule is enabled.