critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

PromptDeliveryRecord model class exists with fields: logId (String), peerMentorId (String), chapterId (String), scenarioRuleId (String), deliveredAt (DateTime), deliveryChannel (DeliveryChannel enum: fcm/apns), idempotencyKey (String), status (DeliveryStatus enum: delivered/failed/skipped), with fromJson/toJson
PromptHistoryRepository class is implemented with methods: hasBeenDelivered(String idempotencyKey) → Future<bool>, recordDelivery(PromptDeliveryRecord record) → Future<void>, getDeliveryHistoryForMentor(String mentorId, String chapterId) → Future<List<PromptDeliveryRecord>>, getDeliveryHistoryForChapter(String chapterId) → Future<List<PromptDeliveryRecord>>
hasBeenDelivered performs a COUNT or SELECT query on idempotency_key and returns true if exactly one record exists
recordDelivery inserts via service_role pathway (Edge Function or SECURITY DEFINER function) — does NOT use authenticated client insert (blocked by RLS)
getDeliveryHistoryForMentor filters by both mentorId and chapterId and is ordered by deliveredAt descending
getDeliveryHistoryForChapter filters by chapterId only and is ordered by deliveredAt descending
Repository is registered as a Riverpod provider
All methods throw typed exceptions on error (PromptHistoryRepositoryException)
hasBeenDelivered is idempotency-safe: calling it concurrently returns the same result
Unit tests cover all four methods with mocked dependencies

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Flutter SDK
apis
Supabase PostgREST API (follow_up_notification_log table, SELECT)
Supabase Edge Function or SECURITY DEFINER RPC for INSERT (service_role pathway)
Supabase Auth (JWT for RLS on SELECT)
data models
PromptDeliveryRecord
follow_up_notification_log (DB table)
DeliveryChannel (enum)
DeliveryStatus (enum)
performance requirements
hasBeenDelivered must use a SELECT with .limit(1) — not a full table scan
getDeliveryHistoryForMentor and getDeliveryHistoryForChapter should support pagination (limit/offset parameters) for mentors with large delivery histories
Queries must leverage existing indexes on (peer_mentor_id, chapter_id)
security requirements
recordDelivery must NOT use the authenticated mobile client to insert — must route through a Supabase Edge Function or SECURITY DEFINER Postgres function holding service_role privileges
The mobile client must never hold or transmit the service_role key
idempotency_key must be validated as non-empty before calling recordDelivery
hasBeenDelivered must not expose whether other mentors' records exist — only queries own idempotency keys

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

The critical design decision here is the insert pathway for recordDelivery. Since RLS blocks authenticated client inserts, two options exist: (a) Create a Supabase Edge Function `record_prompt_delivery` that accepts the PromptDeliveryRecord payload, validates the caller's JWT, and inserts using the service_role client internally. This is the recommended approach as it keeps service_role credentials server-side. (b) Create a SECURITY DEFINER Postgres function accessible via RPC that validates the caller matches the peer_mentor_id in the record before inserting.

Option (a) is preferred. Place the Edge Function in `supabase/functions/record-prompt-delivery/`. The mobile repository calls `supabase.functions.invoke('record-prompt-delivery', body: record.toJson())`. Coordinate with task-004 implementer to confirm which pathway was decided.

For the model, use Dart enums with a fromString factory for DeliveryChannel and DeliveryStatus to handle JSON deserialization robustly. Place the repository in `lib/features/scenario_rules/data/repositories/prompt_history_repository.dart` alongside the ScenarioRuleRepository.

Testing Requirements

Unit tests with flutter_test: (1) hasBeenDelivered returns true when mock returns a non-empty result for the given idempotency_key; (2) hasBeenDelivered returns false when mock returns empty result; (3) recordDelivery calls the correct Edge Function or RPC with the serialized PromptDeliveryRecord; (4) recordDelivery throws PromptHistoryRepositoryException when the insert fails; (5) getDeliveryHistoryForMentor returns a correctly deserialized list sorted by deliveredAt; (6) getDeliveryHistoryForChapter returns all chapter records; (7) fromJson handles all DeliveryChannel and DeliveryStatus enum values correctly; (8) fromJson with unknown enum value throws a descriptive error rather than crashing silently. Integration tests (local Supabase): (9) insert a record via service_role, then call hasBeenDelivered — assert true; (10) attempt direct insert via authenticated client — assert permission denied; (11) coordinator reads chapter history — assert all entries visible; (12) peer mentor reads own history via getDeliveryHistoryForMentor — assert only own entries.

Component
Prompt History Repository
data medium
Epic Risks (2)
high impact medium prob security

Supabase RLS policies for chapter-scoped rule access may interact unexpectedly with service-role keys used by the Edge Function, potentially blocking backend reads or leaking cross-chapter data.

Mitigation & Contingency

Mitigation: Write and review RLS policies in isolation with automated policy tests before merging; define a dedicated service-role bypass policy scoped to the edge function's Postgres role.

Contingency: If RLS blocks the edge function, temporarily use a bypass policy with audit logging while a permanent fix is implemented; escalate to a Supabase security review.

medium impact high prob integration

FCM device tokens become invalid when users reinstall the app or revoke permissions; stale tokens cause silent delivery failures that are hard to detect without explicit error handling.

Mitigation & Contingency

Mitigation: Implement token invalidation handling in PushNotificationDispatcher that removes stale tokens from the database on FCM 404/410 responses; log all delivery failures with structured output.

Contingency: If token hygiene proves unreliable, add a periodic token refresh job that re-registers all active users' tokens via the FCM registration endpoint.