Wire recalculation trigger on selection change
epic-expense-type-selection-core-services-task-009 — Connect ExpenseSelectionBloc to ExpenseCalculationService so that every validated selection change immediately triggers an async recalculation. The BLoC emits a loading calculation state while awaiting the result, then emits the updated state with the populated ExpenseCalculationResult. Ensure that rapid successive toggles cancel any in-flight calculation using a debounce or switchMap equivalent pattern to avoid stale result races.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 4 - 323 tasks
Can start after Tier 3 completes
Implementation Notes
In flutter_bloc, the cleanest way to implement cancellation of in-flight async work is to use a Completer or store a reference to the current calculation Future and check whether the event ID has changed before emitting. Alternatively, use EventTransformer with rxdart's switchMap: `transformer: (events, mapper) => events.switchMap(mapper)`. This is the idiomatic BLoC approach for cancellable async operations. The debounce can be added on top: `transformer: (events, mapper) => events.debounceTime(const Duration(milliseconds: 300)).switchMap(mapper)`.
Inject the transformer via the on
Testing Requirements
Unit tests with flutter_test and bloc_test. Use a fake ExpenseCalculationService that supports configurable async delay and error injection. Test cases: (1) single toggle → calculating state → loaded state with result, (2) two rapid toggles → only the second result applied (first cancelled), (3) calculation error → error state with selection preserved, (4) recovery: successful calculation after an error. Use fakeAsync and pump to control time in tests so the debounce window can be tested deterministically without real delays.
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.