Implement transit zone amount calculation
epic-expense-type-selection-core-services-task-004 — Implement the transit zone reimbursement formula in ExpenseCalculationService. Zone amounts are looked up from expense-type-config keyed by org and zone identifier. The result populates a TransitZoneResult line item. Add support for multi-zone trips where the amount is the sum of individual zone costs. Ensure the formula returns zero gracefully when no zone config is present rather than throwing.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Transit zone config in ExpenseTypeConfigRepository should be modelled as Map
This graceful degradation is explicitly required by the task description. Do not conflate transit zone with flat receipt (transit ticket) — transit zone is a zone-rate model (lookup table), flat receipt is a declared-amount model. Keep them as separate methods and result types.
Testing Requirements
Write unit tests using flutter_test covering: (1) single zone found in config — correct amount returned, (2) two zones — correct sum, (3) one known zone + one unknown zone — unknown in missingZoneIds, known contributes its amount, (4) all zones unknown — missingZoneIds contains all, total is 0.0, (5) empty zone list — total 0.0 no error, (6) org with no transit config at all — total 0.0 no error, (7) zone IDs differing only in case are matched (case-insensitive), (8) fractional zone amounts sum and round to 2dp correctly. Mock ExpenseTypeConfigRepository.
The per-km reimbursement rate and transit zone amounts must be read from org-specific configuration stored in Supabase. If the rate configuration table or RLS policies are not yet deployed when this epic runs, the calculation service cannot be completed and integration tests will fail.
Mitigation & Contingency
Mitigation: Define a RateConfigRepository interface and inject a stub implementation with default HLF rates from day one; write the real Supabase adapter in parallel and swap via dependency injection before merge.
Contingency: If org rate config is delayed beyond this epic's window, ship with the default-rate stub and log a prominent warning; calculate with defaults and surface a 'rates not confirmed' notice in the UI preview.
If the peer mentor opens an expense claim on two devices simultaneously, the local draft and the Supabase record may diverge. The repository's last-write-wins strategy could silently overwrite a valid selection with a stale one.
Mitigation & Contingency
Mitigation: Add an updated_at timestamp to the draft record and reject saves where the server timestamp is newer than the local copy; surface a conflict resolution prompt rather than silently overwriting.
Contingency: If conflict resolution UI is out of scope, fall back to server-authoritative reads on app foreground resume and discard local draft, notifying the user that their draft was refreshed from the server.