critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

submitClaim() fetches OrgRateConfig from OrgRateConfigRepository using the submitter's organization ID before assembly — not from a cached parameter.
A MileageClaim domain object is constructed using the fetched OrgRateConfig snapshot and raw form inputs from MileageClaimFormInput.
Field validation runs before domain object construction: `distanceKm` must be > 0, `route` must be non-empty, `submitterId` must be non-empty, `submittedAt` is set to `DateTime.now().toUtc()` by the service (not by the caller).
If any required field is invalid, submitClaim() returns `SubmissionFailure` with a descriptive error string — no exception is thrown to the caller.
The assembled MileageClaim's `orgRateConfigSnapshot` is a deep-copied value object — mutating the repository's config after assembly does not affect the snapshot.
ExpenseFlags on the assembled claim correctly reflect the `hasAdditionalExpenses` and typed expense selections from MileageClaimFormInput.
Unit tests cover: valid assembly, zero-distance failure, empty-route failure, empty-submitterId failure, and OrgRateConfigRepository throwing an exception (must return SubmissionFailure, not rethrow).
No UI or widget code is imported in this method — assembly is pure domain logic.

Technical Requirements

frameworks
Flutter
Riverpod
Dart
apis
OrgRateConfigRepository.getConfigForOrg(orgId)
data models
MileageClaim
MileageClaimFormInput
OrgRateConfig
ExpenseFlag
SubmissionOutcome
performance requirements
OrgRateConfig fetch must be awaited before assembly — no optimistic construction with placeholder config.
Assembly itself (after fetch) must complete synchronously.
security requirements
submittedAt must always be set server-side or by the service layer — never trust a timestamp from MileageClaimFormInput.
Validate that submitterId matches the authenticated session in the Supabase auth context — reject mismatches with SubmissionFailure.

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Structure submitClaim() as a clear sequential pipeline: (1) validate raw inputs → early return SubmissionFailure if invalid; (2) fetch OrgRateConfig — wrap in try/catch → return SubmissionFailure on error; (3) assemble MileageClaim — this is synchronous after the fetch; (4) pass to AutoApprovalEvaluator (task-004); (5) persist (task-005). Tasks 003–005 implement steps 1–5 of this pipeline incrementally. In this task, implement steps 1–3 and leave steps 4–5 as `// TODO` stubs that immediately throw UnimplementedError. For the OrgRateConfig deep copy, implement a `OrgRateConfig.snapshot()` factory constructor or equivalent that returns a new instance with all fields copied — do not rely on Dart's default reference semantics.

ExpenseFlag validation should cross-check that mutually exclusive flags (e.g., `publicTransport` and `tollRoad` cannot both be present per HLF business rules) are not simultaneously set — return SubmissionFailure if a forbidden combination is detected.

Testing Requirements

Unit tests using `flutter_test`. Mock OrgRateConfigRepository using `mocktail` or `mockito`. Required test cases: (1) happy path — all valid inputs produce a MileageClaim with correct field values; (2) distanceKm = 0 returns SubmissionFailure; (3) distanceKm < 0 returns SubmissionFailure; (4) empty route string returns SubmissionFailure; (5) empty submitterId returns SubmissionFailure; (6) OrgRateConfigRepository throws Exception → submitClaim returns SubmissionFailure (does not rethrow); (7) assembled MileageClaim.orgRateConfigSnapshot is a distinct object from the one returned by the repository (deep copy test); (8) submittedAt on the assembled claim is in UTC. Run tests with `flutter test`.

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.