Implement selection toggle with rule engine invocation
epic-expense-type-selection-core-services-task-007 — Implement the toggleExpenseType method in ExpenseSelectionBloc. On each call: (1) tentatively add or remove the type ID from the selection set, (2) invoke the mutual-exclusion-rule-engine to validate the new set, (3) if rejected, revert the toggle and emit a validation error state, (4) if accepted, update selectedIds and invoke recalculation. The rule engine must be injected as a dependency and called synchronously to keep state transitions atomic.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Use the standard BLoC event→state pattern. In the event handler, copy the current selectedIds set into a mutable local variable, apply the tentative toggle, pass the copy to ruleEngine.validate(tentativeSet), and branch on the result before committing to state. Do NOT mutate the existing state's selectedIds set directly — always work on a copy to preserve immutability. The rule engine interface should be a Dart abstract class with a single validate(Set
Emit ExpenseSelectionValidationError as a distinct state subclass (not a flag on the main state) so the UI can handle it separately without conflating it with a loading or success state. Keep the BLoC free of UI concerns — error messages should come from the rule engine's ValidationResult, not from the BLoC itself.
Testing Requirements
Write unit tests using flutter_test and bloc_test. Cover all toggle outcomes: valid add, valid remove, rejected add (rule violation), rejected remove (rule violation), idempotent double-toggle. Mock the MutualExclusionRuleEngine dependency using a hand-rolled fake that returns configurable validation results. Assert both emitted states and that the fake rule engine was called with the correct tentative set on each invocation.
Target 100% branch coverage on the toggleExpenseType event handler.
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.