critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

MutualExclusionMatrix is declared as an immutable Dart class; all fields are final and the class has no setters.
ExpenseTypePair implements equality and hashCode based on the unordered pair (i.e. Pair(a,b) == Pair(b,a)) and is usable as a Set key.
isCompatible(a, b) returns false when the pair (a,b) or (b,a) is present in the exclusion set, and true otherwise.
isCompatible(a, a) returns false (a type is incompatible with itself when considering mutual exclusion of the same type being duplicated — or returns true per domain logic; document the chosen behaviour explicitly).
getConflictingTypes(selected) returns a list containing exactly the types from `selected` that conflict with at least one other type in the same list; empty list for no conflicts.
getConflictingTypes has O(n²) worst-case complexity with n = selected.length (set lookup makes each pair check O(1)); document this in the method's dartdoc.
The constant initial matrix encodes exactly: kilometre-reimbursement ↔ transit-ticket as mutually exclusive.
The class can be constructed with a custom exclusion set for testing purposes (factory constructor or named constructor).
dart analyze reports zero warnings or errors on the file.

Technical Requirements

frameworks
Flutter
Dart
data models
ExpenseType
ExpenseTypePair
MutualExclusionMatrix
performance requirements
Set<ExpenseTypePair> lookup is O(1) per pair — relies on correct hashCode implementation
getConflictingTypes iterates pairs in O(n²) with n = number of selected types; acceptable for n < 20
security requirements
No mutable state — prevents race conditions in concurrent BLoC event processing

Execution Context

Execution Tier
Tier 1

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 lookups work correctly. Use a @immutable annotation and make the backing Set a const.

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.

Component
Mutual Exclusion Rule Engine
service medium
Epic Risks (2)
high impact medium prob scope

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.

medium impact medium prob technical

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.