critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

MutualExclusionRuleEngine accepts a MutualExclusionMatrix in its constructor and exposes no mutable state.
validateSelection([]) returns an empty list.
validateSelection with a valid, non-conflicting selection returns an empty list.
validateSelection with one or more conflicting pairs returns a RuleViolation per conflicting pair, each containing the two conflicting ExpenseType values.
filterAllowedAdditions(current) returns all ExpenseType values not in `current` and not conflicting with any type in `current`.
filterAllowedAdditions([]) returns all known ExpenseType values.
resolveConflict(current, toAdd) returns a ConflictResolution specifying which existing type to remove (per domain priority rule) and a non-empty English reason string.
resolveConflict is only called when toAdd conflicts with exactly one existing type; the method documents behaviour when called with a type that has no conflict (returns null or a no-op resolution).
All three methods are annotated as pure functions (no I/O, no state mutation, deterministic output).
dart analyze reports zero warnings or errors.

Technical Requirements

frameworks
Flutter
Dart
data models
ExpenseType
MutualExclusionMatrix
RuleViolation
ConflictResolution
performance requirements
validateSelection completes in O(n²) with n = selection length
filterAllowedAdditions completes in O(m×n) with m = all types count, n = current count — both are small sets (<20)
security requirements
Reason strings in ConflictResolution must not contain user data or PII

Execution Context

Execution Tier
Tier 2

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.

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.