critical priority medium complexity backend pending backend specialist Tier 4

Acceptance Criteria

A ClaimEvent record is written after every successful state transition: submitted, approved, rejected, and exported
Each event record contains: claim_id, from_status, to_status, actor_id (from JWT), actor_role, occurred_at (UTC), and optional coordinator_comment
ClaimEvent records have no UPDATE or DELETE RLS policy — they are insert-only at the database level
The event write occurs within the same logical operation as the status update — if the event write fails, the transition is rolled back (or the event is retried safely)
actor_id is always the authenticated user's ID from the JWT — never a client-supplied value
actor_role is validated against allowed enum values (peer_mentor, coordinator, admin) before insertion
coordinator_comment is optional and nullable; when present it is sanitised for XSS/injection before storage
Events are queryable by claim_id and ordered by occurred_at ascending for audit trail display
No existing event record can be modified after creation — verified by the absence of UPDATE grants in RLS
Events table schema matches the ClaimEvent data model including all required fields and enum constraints

Technical Requirements

frameworks
Flutter
BLoC
Dart
apis
Supabase PostgreSQL 15 SDK
Supabase Auth JWT claims
data models
claim_event
performance requirements
Event insert must not add more than 100ms latency to the state transition operation
Batch event queries for audit trail display must return within 300ms for up to 50 events per claim
security requirements
RLS must grant INSERT only — no UPDATE or DELETE on claim_events table for any role including coordinator
actor_id must be extracted from supabase.auth.uid() server-side, not accepted from request body
coordinator_comment must be sanitised (strip HTML/script tags) before persistence
Organisation-scoped RLS ensures coordinators can only read/insert events for claims in their own organisation

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Use Supabase's `.insert()` on the claim_events table immediately after the status update `.update()` call. To achieve transactionality without a Supabase Edge Function, wrap both operations in a Supabase RPC (PostgreSQL function) that performs both writes in a single database transaction. This is the safest approach — define a `record_claim_transition(p_claim_id, p_from_status, p_to_status, p_actor_id, p_actor_role, p_comment)` SQL function and call it via `supabase.rpc()`. The RLS on claim_events should be configured as: ENABLE ROW LEVEL SECURITY, then only a single INSERT policy with `USING (organisation_id = auth.jwt() ->> 'organisation_id')`.

Never add an UPDATE policy. The ClaimEventsRepository should expose only `insertEvent()` and `getEventsForClaim()` methods — no update or delete methods should exist on the repository class.

Testing Requirements

Unit tests with flutter_test and mocked ClaimEventsRepository. Test cases: (1) event written with correct from/to states for each of the 4 transitions, (2) event not written if state transition repository call fails (rollback behaviour), (3) actor_id and actor_role correctly populated from auth context, (4) optional comment stored when provided and null when absent, (5) comment sanitisation strips unsafe characters, (6) duplicate event write attempt is handled gracefully (idempotency). Integration test: verify RLS blocks UPDATE and DELETE on real Supabase test environment.

Component
Approval Workflow Service
service high
Epic Risks (3)
high impact high prob technical

The ThresholdEvaluationService is described as shared Dart logic used both client-side and in the Edge Function. Supabase Edge Functions run Deno/TypeScript, not Dart, meaning the threshold logic must be maintained in two languages and can diverge, causing the server to reject legitimate client submissions.

Mitigation & Contingency

Mitigation: Implement the threshold logic as a single TypeScript module in the Edge Function and call it via a thin Dart HTTP client wrapper for client-side preview feedback only. The server is always authoritative; the client version is purely for UX (showing the user whether their claim will auto-approve before they submit).

Contingency: If dual-language maintenance is unavoidable, create a shared golden test file (JSON fixtures with inputs and expected outputs) that is run against both implementations in CI to detect divergence immediately.

medium impact medium prob technical

A peer mentor could double-tap the submit button or a network retry could trigger a duplicate submission, causing the ApprovalWorkflowService to attempt two concurrent state transitions from draft→submitted for the same claim, potentially resulting in two audit events or conflicting statuses.

Mitigation & Contingency

Mitigation: Implement idempotency in the ApprovalWorkflowService using a database-level unique constraint on (claim_id, from_status, to_status) per transition, combined with a UI-level submission lock (disable button after first tap until response returns).

Contingency: Add a deduplication check at the start of every state transition method that returns the existing state if an identical transition is already in progress or completed within the last 10 seconds.

high impact medium prob scope

Claims with multiple expense lines (e.g., mileage + parking) must have their combined total evaluated against the threshold. If individual lines are added asynchronously or the evaluation runs before all lines are persisted, the auto-approval decision may be computed on an incomplete set of expense lines.

Mitigation & Contingency

Mitigation: The Edge Function always fetches all expense lines from the database (not from the client payload) before computing the threshold decision. Define a clear claim submission contract that requires all expense lines to be persisted before the submit action is called.

Contingency: Add a validation step in ApprovalWorkflowService that counts expected vs. persisted expense lines before allowing the transition, returning a validation error if lines are missing.