critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

toggleExpenseType event adds the type ID to selectedIds when it is not currently selected and the new set passes rule engine validation
toggleExpenseType event removes the type ID from selectedIds when it is currently selected and the resulting set passes rule engine validation
When the rule engine rejects the tentative set, the selection state is reverted to the pre-toggle value exactly — no partial mutations remain in state
When the rule engine rejects the toggle, an ExpenseSelectionValidationError state is emitted containing the rejected type ID and the human-readable violation message
The rule engine is injected via constructor dependency injection and never instantiated inside the BLoC
The rule engine is invoked synchronously within the event handler so state transitions are atomic — no async gaps between tentative update and validation
After a successful toggle, recalculation is triggered (as defined by task-009 integration); the BLoC does not wait for recalculation to emit the updated selection state
All emitted states include the full current selectedIds set, not a delta
Toggling the same type ID twice in sequence returns to the original state with no side effects
Unit tests cover: add new type (valid), remove existing type (valid), add type rejected by rule engine, remove type rejected by rule engine, toggle on empty set

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
data models
activity_type
performance requirements
Rule engine invocation must complete synchronously within the event handler — no Future/async allowed in the validation path
State emission must occur within a single event handler execution cycle with no observable intermediate states
security requirements
Expense type IDs must be validated against the known set of activity_type records before being added to selectedIds — reject unknown IDs
No raw user input is passed directly to the rule engine; only normalised type UUIDs

Execution Context

Execution Tier
Tier 2

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 ids) → ValidationResult method so it is trivially mockable.

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.

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.