high priority low complexity backend pending backend specialist Tier 3

Acceptance Criteria

markAsExported() transitions a claim from 'approved' to 'exported' status successfully when called with a valid actor who has coordinator or admin role
markAsExported() throws a domain exception (InvalidStateTransitionException) when called on a claim that is not in 'approved' state (e.g., pending, rejected, already exported)
markAsExported() throws a DomainException with code CLAIM_ALREADY_EXPORTED when called on a claim already in 'exported' state, preventing double-export
The export actor user ID and UTC timestamp are persisted alongside the status update in the expense claim record
The status update is atomic — no partial updates are visible if any part of the operation fails
RLS in Supabase enforces that only coordinator/admin roles for the same organisation can call the export transition
Calling markAsExported() on a claim belonging to a different organisation returns an authorisation error, never a state error
The returned claim object after export reflects the updated status, actor, and timestamp fields

Technical Requirements

frameworks
Flutter
BLoC
Riverpod
Dart
apis
Supabase PostgreSQL 15 REST/SDK
Supabase Auth JWT validation
data models
claim_event
assignment
performance requirements
State transition must complete within 500ms under normal network conditions
Single database round-trip for status update — no polling or retry loops on the happy path
security requirements
RLS policy must restrict UPDATE to coordinator/admin roles within the same organisation_id
Service role key must never be used client-side — transition must be callable only from authenticated Supabase client with valid JWT
Actor user ID must be derived from the verified JWT claims, not from a client-supplied parameter
All inputs validated server-side before any database write

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Implement as a pure domain method on ApprovalWorkflowService using the FSM pattern established in previous tasks. The guard check should be a single synchronous condition evaluated before any async repository call — fail fast. Use a sealed class or enum-based result type for the return value so callers cannot ignore the error branch. The export actor timestamp must be captured as DateTime.now().toUtc() inside the service, not passed in from outside, to prevent timestamp forgery.

Wire the status update as a single Supabase `.update()` call with a `.eq('status', 'approved')` conditional filter — this provides optimistic locking at the database level and prevents TOCTOU races without needing explicit transactions.

Testing Requirements

Unit tests using flutter_test with mocked ExpenseClaimStatusRepository and ClaimEventsRepository. Test cases: (1) happy-path export from approved state, (2) exception thrown when claim is in pending state, (3) exception thrown when claim is in rejected state, (4) CLAIM_ALREADY_EXPORTED exception on double-export, (5) actor ID correctly sourced from auth context not payload, (6) atomic failure leaves claim in approved state. All tests must pass with 100% branch coverage on markAsExported().

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.