critical priority medium complexity backend pending backend specialist Tier 4

Acceptance Criteria

MileageClaimRepository.save(MileageClaim claim) is called after ClaimStatus is resolved, passing the fully assembled and status-annotated claim.
On successful persistence, submitClaim() returns `SubmissionSuccess` with the `claimId` returned by the repository and the `resolvedStatus` from the evaluator.
On any exception thrown by MileageClaimRepository.save(), submitClaim() catches it and returns `SubmissionFailure` with a descriptive error string — the exception does not propagate to the caller.
No partial state is silently swallowed: if save() throws, the caller receives SubmissionFailure (not SubmissionSuccess with a null claimId, and not a silent no-op).
The repository is implemented against Supabase — the save operation inserts a new row into the `mileage_claims` table with all required fields including `resolved_status`, `distance_km`, `route`, `has_additional_expenses`, `submitter_id`, `submitted_at`, and the serialized `org_rate_config_snapshot`.
The returned `claimId` is the Supabase-generated UUID for the inserted row, not a client-generated ID.
Integration test (using a test Supabase instance or mock) verifies that a saved claim can be retrieved with the same field values.
Unit tests mock MileageClaimRepository and verify: save() called exactly once per submitClaim() invocation; SubmissionSuccess carries the correct claimId; a repository exception yields SubmissionFailure.
`flutter analyze` reports zero issues on all files modified in tasks 001–005.

Technical Requirements

frameworks
Flutter
Dart
Riverpod
apis
Supabase PostgREST insert on `mileage_claims` table
MileageClaimRepository.save(MileageClaim) → Future<String> (returns claimId)
data models
MileageClaim
ClaimStatus
SubmissionOutcome
OrgRateConfig
performance requirements
Persistence must be a single atomic insert — no multi-step insert that could partially succeed.
Repository save() must complete within a reasonable timeout (suggest 10 seconds); callers should not block indefinitely.
security requirements
Row-level security (RLS) on the Supabase `mileage_claims` table must ensure users can only insert rows where `submitter_id` matches their authenticated `auth.uid()`.
org_rate_config_snapshot must be stored as a JSONB column — never as a flat string to prevent injection.
Do not expose the raw Supabase error message to end users — map it to a generic 'Submission failed, please try again' message in SubmissionFailure.

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

MileageClaimRepository.save() should return `Future` where the String is the Supabase-generated UUID. Implement the repository using the Supabase Flutter client: `await supabase.from('mileage_claims').insert(claim.toJson()).select('id').single()`. The `toJson()` method on MileageClaim must serialize `orgRateConfigSnapshot` as a nested JSON object (JSONB in Postgres) and `expenseFlags` as an array of strings. For rollback semantics: Supabase PostgREST insert is atomic per row — if it throws, nothing is persisted.

The 'rollback on failure' in the task description refers to the service-level guarantee: never return SubmissionSuccess unless save() completed without exception. There is no multi-step transaction to manually roll back. Document this clearly in the service code. After completing this task, run `flutter analyze` across all five tasks' files as a final integration check before marking the epic complete.

Ensure the Supabase RLS policy is defined in a migration file (not applied manually) so it is repeatable across environments.

Testing Requirements

Two levels of testing required. (1) Unit tests using `flutter_test` with mocked MileageClaimRepository: verify save() called exactly once; SubmissionSuccess returned with correct claimId on mock success; SubmissionFailure returned when save() throws a generic Exception; SubmissionFailure returned when save() throws a Supabase-specific exception (PostgrestException). (2) Integration test against a local Supabase instance (via Docker or Supabase CLI): insert a valid MileageClaim and assert the returned claimId is a valid UUID, then query the `mileage_claims` table and assert the row exists with correct field values including `resolved_status`. Integration tests are optional for CI but required before merging to main.

Tag integration tests with `@Tags(['integration'])` so they can be excluded from fast unit test runs.

Component
Mileage Claim Service
service medium
Epic Risks (2)
high impact medium prob integration

The auto-approval rule requires checking whether any additional expense lines are attached to the claim. The interface between the mileage claim and any co-submitted expense items is not fully defined within this feature's component scope. If the domain model does not include an explicit additionalExpenses collection, the evaluator cannot make a correct determination, which could auto-approve claims that should require manual review.

Mitigation & Contingency

Mitigation: Define the MileageClaim domain object interface with an explicit additionalExpenses: List field (nullable/empty for mileage-only claims) before implementing the service. Coordinate with the Expense Type Selection feature team to agree on the shared domain contract.

Contingency: If the cross-feature contract cannot be finalised before implementation, implement the evaluator to treat any non-null additionalExpenses list as requiring manual review and document the assumption for review during integration testing.

medium impact medium prob technical

A peer mentor who taps the submit button multiple times rapidly (e.g. due to slow network) could cause MileageClaimService to be invoked concurrently, resulting in duplicate claim records being persisted with the same trip data.

Mitigation & Contingency

Mitigation: Implement a submission-in-progress guard in MileageClaimService using a BLoC/Cubit state flag that prevents re-entrant calls. The UI layer (implemented in Epic 4) will also disable the submit button during processing.

Contingency: Add a Supabase-level unique constraint or idempotency key on (user_id, origin, distance, submitted_at truncated to minute) to prevent duplicate rows reaching the database even if the application guard fails.