Implement MileageClaim assembly from form inputs and org config
epic-mileage-reimbursement-entry-claim-orchestration-task-003 — Implement the logic within submitClaim() that assembles a MileageClaim domain object by combining raw form inputs (distance, route, additional expenses) with the org-specific rate configuration fetched from OrgRateConfigRepository. Validate all required fields are present before proceeding to status evaluation.
Acceptance Criteria
Technical Requirements
Execution Context
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`.
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.
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.