Define compatibility matrix data structure
epic-expense-type-selection-foundation-task-010 — Model the mutual-exclusion compatibility matrix as an immutable Dart class MutualExclusionMatrix backed by a const Set<ExpenseTypePair> where ExpenseTypePair is an unordered pair of ExpenseType values. Implement isCompatible(ExpenseType a, ExpenseType b) and getConflictingTypes(List<ExpenseType> selected) as pure functions with O(n) complexity. The initial matrix must encode the HLF rule: kilometre-reimbursement and transit-ticket are mutually exclusive.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Implement ExpenseTypePair as an unordered pair by normalising the two elements on construction (e.g. store the lexicographically smaller enum index first). Override == and hashCode using the two normalised indices so Set
Avoid using a 2D matrix array ā the Set approach scales better when new types are added because only conflicting pairs need to be listed. Keep the file in `lib/features/expense/domain/models/mutual_exclusion_matrix.dart` to align with clean architecture layering. Document the O(1) pair lookup guarantee in dartdoc so future developers do not accidentally replace the Set with a List.
Testing Requirements
Unit tests are covered in task-012; however, this task must include minimal smoke tests proving the class compiles and the HLF rule is encoded correctly. Write at least: one test asserting isCompatible(kilometre-reimbursement, transit-ticket) == false, one asserting isCompatible(kilometre-reimbursement, parking) == true, and one asserting getConflictingTypes([kilometre-reimbursement, transit-ticket]) returns both types. These serve as contract tests for downstream tasks.
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.