critical priority medium complexity backend pending backend specialist Tier 6

Acceptance Criteria

acceptClaim(claimId, comment: String?) updates the claim status to 'approved' in Supabase and records the coordinator's userId, decision timestamp, and optional comment
rejectClaim(claimId, comment: String) throws a ValidationException if comment is empty or whitespace-only, before any network call is made
rejectClaim with a non-empty comment updates the claim status to 'rejected' in Supabase and records the coordinator userId, decision timestamp, and the comment
Both methods write a ClaimEvent audit record (claim_events table) with actor_id, actor_role='coordinator', from_status='pending-attestation', to_status='approved'/'rejected', and comment
After a successful decision, the affected claim is optimistically removed from the pending queue held by the AsyncNotifier without requiring a full refetch
Notification dispatch is triggered for the claimant after a successful decision (fire-and-forget via Supabase Edge Function — do not await notification result for the UI response)
If the Supabase update fails, the claim remains in the queue, the error is surfaced to the caller as an exception, and no ClaimEvent is written
Both methods are idempotent-safe: attempting to approve/reject an already-decided claim returns a specific AlreadyDecidedException, not a generic error
Only the coordinator who owns the chapter scope (from JWT) can call these methods — a RLS violation triggers an AuthorizationException

Technical Requirements

frameworks
Flutter
Riverpod
Dart
apis
Supabase PostgreSQL 15 REST/PostgREST
Supabase Edge Functions (Deno) for notification dispatch
Supabase Auth (coordinator identity from JWT)
data models
claim_event
performance requirements
Decision update + ClaimEvent insert should complete in a single database transaction to guarantee atomicity
Optimistic queue update applied immediately so the coordinator sees the claim removed within 100ms of confirming the decision
Notification Edge Function called asynchronously — must not block the decision response
security requirements
Coordinator identity sourced from server-side JWT, never from client-supplied actorId parameter
RLS policy on expense_claims enforces that UPDATE is only allowed for claims in the coordinator's chapter
Comment text sanitized of any HTML/script before storage (strip tags client-side before sending)
ClaimEvent records are append-only — no UPDATE or DELETE allowed on claim_events table via RLS

Execution Context

Execution Tier
Tier 6

Tier 6 - 158 tasks

Can start after Tier 5 completes

Implementation Notes

Wrap the claim UPDATE and ClaimEvent INSERT in a Supabase RPC (Postgres function) to ensure atomicity — do not do two separate REST calls for this because a partial write (claim updated, event not written) would corrupt the audit trail. The RPC should accept (p_claim_id uuid, p_decision text, p_comment text, p_actor_id uuid) and return the updated claim row. Validate comment emptiness in Dart before calling the RPC to give immediate UI feedback. For notification dispatch, call an Edge Function by name using supabase.functions.invoke('dispatch-claim-decision-notification', body: {...}) without awaiting — catch and log any error silently.

After a successful decision, use ref.read(attestationQueueProvider.notifier).removeClaim(claimId) for the optimistic update rather than invalidating the entire provider.

Testing Requirements

Unit tests with fake ExpenseRepository and fake NotificationDispatcher: (1) acceptClaim happy path — status updated, ClaimEvent written, notification dispatched, claim removed from notifier state; (2) rejectClaim with empty comment — throws ValidationException, no network calls made; (3) rejectClaim with valid comment — full success path; (4) Supabase update throws — AlreadyDecidedException returned when claim status is not pending-attestation; (5) network failure during update — no ClaimEvent written, original queue state preserved. Use flutter_test with mockito or manual fakes. Integration test: verify claim_events row is inserted with correct from/to status values.

Component
Expense Attestation Service
service medium
Epic Risks (3)
high impact medium prob scope

Mutual exclusion rules are stored in the expense type catalogue's exclusive_groups field. If the catalogue schema or group definitions differ between HLF and Blindeforbundet, the validation service must handle multiple group configurations without hardcoding organisation-specific logic.

Mitigation & Contingency

Mitigation: Design the validation service to be purely data-driven: it reads exclusive_groups from the cached catalogue and enforces whichever groups are defined, with no hardcoded organisation names. Write parameterised unit tests covering at least 4 different catalogue configurations to verify generality.

Contingency: If an organisation requires non-standard exclusion semantics (e.g. partial exclusion within a group), introduce an exclusion_type field to the catalogue schema and extend the service, treating it as a catalogue configuration change rather than a code fork.

medium impact high prob technical

The attestation service subscribes to Supabase Realtime for live queue updates. On mobile, Realtime WebSocket connections can be dropped during network transitions, causing the coordinator queue to become stale without the user being aware.

Mitigation & Contingency

Mitigation: Implement connection lifecycle management: reconnect on network-change events, show a 'reconnecting' indicator when the subscription is broken, and perform a full queue refresh on reconnect rather than relying solely on delta events.

Contingency: Add a manual pull-to-refresh gesture on the attestation queue screen as a guaranteed fallback. If Realtime proves unreliable in production, switch to periodic polling (30-second interval) as a degraded but functional mode.

medium impact medium prob integration

If a peer mentor submits a draft while offline and then submits the same claim again after connectivity is restored (thinking the first attempt failed), duplicate claims may be persisted in Supabase.

Mitigation & Contingency

Mitigation: Assign a client-generated idempotency key (UUID) to each draft at creation time. The submission service sends this key as an upsert key to Supabase, preventing duplicate inserts. The draft is marked 'submitted' locally after first successful upload.

Contingency: Implement a server-side duplicate detection trigger on the expense_claims table checking (activity_id, claimant_id, created_date) within a 24-hour window and returning the existing record ID rather than inserting a duplicate.