critical priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

ExpenseSelectionState is an immutable value class with fields: selectedTypeIds (Set<String>), disabledTypeIds (Set<String>), calculationResult (ExpenseCalculationResult?), isDraft (bool), isLoading (bool), error (String?), and claimId (String)
ExpenseSelectionState.initial(String claimId) factory returns a state with empty sets, null calculationResult, isDraft true, isLoading false, and null error
ExpenseSelectionBloc extends StateNotifier<ExpenseSelectionState> (Riverpod) — not flutter_bloc Bloc/Cubit — consistent with Riverpod StateNotifier pattern used in this project
The provider is defined as a StateNotifierProvider.family<ExpenseSelectionBloc, ExpenseSelectionState, String> keyed by claimId, ensuring separate notifier instances per claim
The provider is registered in the app's ProviderScope with correct override hooks for testing
ExpenseSelectionBloc is injected with ExpenseCalculationService via the Riverpod ref, not constructed manually
Skeleton event/method stubs are present for: toggleExpenseType(String typeId), updateInput(String typeId, Map<String, dynamic> inputs), submitSelection(), and resetSelection() — each with a TODO body and the correct state mutation signature
The initial state is correctly restored in tests: ExpenseSelectionBloc(ref, claimId: 'test-123').state equals ExpenseSelectionState.initial('test-123')
No circular dependencies: ExpenseSelectionBloc depends on ExpenseCalculationService, not vice versa

Technical Requirements

frameworks
Flutter
Dart
Riverpod
BLoC (StateNotifier pattern)
data models
ExpenseSelectionState
ExpenseCalculationResult
CalculationInput
performance requirements
State construction (ExpenseSelectionState.initial) must be allocation-cheap — no heavy initialization in constructor
Provider family lookup by claimId must be O(1)
security requirements
claimId must be a non-empty validated string before provider instantiation — reject empty string at provider creation to prevent anonymous claim state
State must never expose mutable collections — use UnmodifiableSetView or built_value immutable sets

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use Riverpod's StateNotifier rather than flutter_bloc's Bloc/Cubit to align with the project's stated Riverpod adoption. The provider family parameter is claimId (String) — this means each active claim gets its own isolated BLoC instance, which is critical for multi-claim navigation (e.g., user navigates away and back). Use Set for selectedTypeIds and disabledTypeIds — not List — to prevent duplicate selections and enable O(1) contains checks. The disabledTypeIds set is managed by mutual exclusion rules (e.g., per-km and transit ticket cannot be selected simultaneously per HLF requirements).

For the skeleton stubs, add descriptive TODO comments referencing the follow-on tasks that will implement them — this makes the dependency clear for AI agents implementing those tasks. Do not implement any business logic in this task — the stub methods should only contain // TODO: implement in task-XXX comments and potentially a notifyListeners() call to signal state changes are coming.

Testing Requirements

Write unit tests using flutter_test and ProviderContainer for Riverpod testing. Cover: (1) initial state has empty selectedTypeIds and disabledTypeIds, (2) initial state has isDraft true, isLoading false, error null, calculationResult null, (3) two different claimIds produce independent provider instances (family isolation test), (4) ExpenseSelectionBloc can be overridden in ProviderContainer for dependency injection in tests, (5) each stub method exists and does not throw when called on initial state (smoke test for skeleton), (6) provider disposes correctly when ProviderContainer is disposed. Use ProviderContainer.read() pattern for synchronous state access in tests.

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.