high priority low complexity backend pending backend specialist Tier 5

Acceptance Criteria

An InitialiseExpenseSelection event is defined and handled in ExpenseSelectionBloc
On handling InitialiseExpenseSelection, the BLoC calls ExpenseTypeRepository.loadDraft(claimContextId)
If a draft is found, the BLoC emits a state with selectedIds and amountsByTypeId hydrated from the draft
After hydrating from a draft, recalculation is immediately triggered (using the same mechanism as task-009) to restore a valid ExpenseCalculationResult
If no draft is found, the BLoC emits an initial state with empty selectedIds, empty disabledTypeIds, and null/empty ExpenseCalculationResult
The initial state (draft or empty) is emitted before any user interaction is possible — the screen must not be shown until initialisation completes
computeDisabledTypes (task-008) is called on the hydrated selection so disabledTypeIds is correct from the first render
InitialiseExpenseSelection is dispatched automatically when the BLoC is created (in the constructor or via an initialisation method called by the widget) — the widget does not need to manually fire it
Unit tests cover: initialise with existing draft → state has draft selection + triggers recalculation, initialise with no draft → state is empty, initialise with corrupted draft (loadDraft returns null) → state is empty

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
data models
activity_type
performance requirements
Draft load is a local read — must complete in under 50ms to avoid a visible loading flash on screen open
security requirements
The claim context ID must be validated as a non-null UUID before being passed to loadDraft — prevent loading a draft under a malformed key

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Add a handler for InitialiseExpenseSelection in the BLoC constructor registration. Dispatch the event in the BLoC constructor: `add(const InitialiseExpenseSelection())`. Pass the claimContextId to the BLoC constructor so it is available throughout the BLoC lifetime. The event handler should: (1) call await repository.loadDraft(claimContextId), (2) compute the initial selectedIds (draft or empty), (3) compute disabledTypeIds via computeDisabledTypes, (4) emit the hydrated state, (5) trigger recalculation if selectedIds is non-empty.

Wrap the entire handler in try/catch so a draft load failure (e.g., storage permission error on first launch) falls back to an empty state gracefully. This addresses the UX concern raised in the workshop: HLF peer mentors with 380 registrations per year need mid-flow recovery to be seamless.

Testing Requirements

Unit tests with flutter_test and bloc_test. Mock ExpenseTypeRepository.loadDraft. Test cases: (1) loadDraft returns a valid draft → emitted state has hydrated selectedIds and recalculation is triggered, (2) loadDraft returns null → emitted state has empty selectedIds, (3) loadDraft throws → state is empty and error is swallowed gracefully. Verify that computeDisabledTypes is invoked on the hydrated set (check disabledTypeIds in the emitted state).

Use fakeAsync to verify recalculation is triggered after hydration.

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.