Implement rule engine validation and filtering
epic-expense-type-selection-foundation-task-011 — Build MutualExclusionRuleEngine wrapping MutualExclusionMatrix with higher-level operations: validateSelection(List<ExpenseType>) returning a RuleViolation list, filterAllowedAdditions(List<ExpenseType> current) returning types that can still be added without conflict, and resolveConflict(List<ExpenseType> current, ExpenseType toAdd) returning a ConflictResolution containing the type to remove and the user-facing reason. All methods must be pure (no side effects) so they can be called freely from BLoC and tests.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Define RuleViolation as a sealed class or simple immutable value object with (typeA: ExpenseType, typeB: ExpenseType). Define ConflictResolution as an immutable value object with (typeToRemove: ExpenseType, reason: String). Keep the reason strings in a dedicated l10n-ready constant map (even if currently only English) so they can be localised later without changing engine logic. The engine itself should be a plain Dart class (no Flutter dependency) placed under `lib/features/expense/domain/services/`.
This keeps it testable without a Flutter test environment. Use Equatable or manual == override on RuleViolation and ConflictResolution so test assertions are clean.
Testing Requirements
Covered extensively in task-012. This task must include contract-level tests: one test per public method verifying the return type and at least one happy-path and one conflict scenario. These tests guard against API signature regressions during refactoring.
The compatibility matrix might be under-specified in source documentation. If a new organisation adds expense types or redefines rules, hardcoded pairwise logic becomes a maintenance liability and can silently allow previously excluded combinations.
Mitigation & Contingency
Mitigation: Model the matrix as a const Map<ExpenseType, Set<ExpenseType>> rather than if-else chains; add a unit test that exhaustively asserts every pair combination so any future matrix change forces explicit test updates.
Contingency: If per-organisation matrix variants are requested before the epic closes, extract matrix loading into expense-type-config with an org-override slot and defer per-org configuration to the repository epic.
VoiceOver (iOS) and TalkBack (Android) handle Semantics widget announcements differently in Flutter. Live-region behaviour for disabled state changes is inconsistent across Flutter versions and may require platform-specific workarounds that are not yet documented.
Mitigation & Contingency
Mitigation: Write accessibility integration tests using Flutter's SemanticsController targeting both iOS and Android simulators from the outset; pin to a Flutter version known to handle Semantics.liveRegion correctly.
Contingency: If platform parity is unachievable before release, ship with a known gap documented in the WCAG audit log and schedule a dedicated accessibility sprint; do not block other epics.