critical priority medium complexity integration pending backend specialist Tier 4

Acceptance Criteria

After every successful toggle, a recalculation is triggered automatically without requiring a separate user action
The BLoC emits an ExpenseSelectionCalculating state (with the current selectedIds and disabledTypeIds preserved) immediately after triggering recalculation, before the result arrives
When the calculation completes, the BLoC emits ExpenseSelectionLoaded with the populated ExpenseCalculationResult
If the user toggles a type while a calculation is in-flight, the in-flight calculation is cancelled and a new calculation is started for the latest selection
The final emitted state always corresponds to the most recent validated selection — no stale results from earlier in-flight calculations are ever applied
If the calculation service throws an error, the BLoC emits ExpenseSelectionCalculationError with the error detail; the selectedIds and disabledTypeIds are preserved
Debounce or switchMap cancellation is implemented so that toggling 5 types rapidly results in at most 1–2 calculation calls, not 5
ExpenseCalculationService is injected via constructor — the BLoC does not instantiate the service directly
Unit tests verify: single toggle triggers recalculation, rapid toggles result in only the last calculation being applied, calculation error emits error state without losing selection

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
rxdart (for switchMap/debounce if needed)
data models
activity_type
activity
performance requirements
Debounce window of 300ms recommended to avoid excessive recalculation calls on rapid toggling
In-flight calculation cancellation must prevent stale results from being applied regardless of network latency
security requirements
Calculation requests must include the organisation context from the authenticated session to prevent cross-org data leakage

Execution Context

Execution Tier
Tier 4

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 registration. Ensure that the ExpenseCalculationResult is a sealed/immutable value object — never mutate the result after emission. The 'loading calculation' state should still carry the full selectedIds and disabledTypeIds so the UI can show a spinner without hiding the already-made selections.

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.

Component
Expense Selection BLoC
service medium
Epic Risks (2)
high impact medium prob dependency

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.

medium impact low prob technical

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.