high priority medium complexity testing pending testing specialist Tier 4

Acceptance Criteria

When a conflict is detected (validateSelection returns a non-empty violation list), the ExpenseTypeAnalyticsTracker enqueues a conflictBlocked event within 50 ms.
The conflictBlocked event payload contains exactly the ExpenseTypePair that caused the conflict — no extra types, no missing types.
The event payload contains no PII fields (no user IDs, names, contact details, or session tokens).
If validateSelection returns an empty list, no conflictBlocked event is emitted.
The in-memory test sink used during tests is activated via a constructor parameter or factory, not via a global flag or environment variable.
Tests run without a Flutter widget tree — no pumpWidget or WidgetTester required.
All integration tests pass within 2 seconds.
Tests cover at least two distinct conflicting pairs to prevent false positives from hardcoded event payloads.

Technical Requirements

frameworks
Flutter
flutter_test
data models
ExpenseType
ExpenseTypePair
MutualExclusionRuleEngine
ExpenseTypeAnalyticsTracker
ConflictBlockedEvent
performance requirements
Event emission verified within 50 ms using FakeAsync or real async with a generous but bounded timeout
security requirements
Assert event payload fields using an explicit allowlist — any field not in the allowlist causes the test to fail, preventing accidental PII leakage
Do not log or print event payloads in test output

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

The integration between the rule engine and the analytics tracker should be thin and explicit — do not introduce a mediator or event bus for this layer; just call tracker.recordConflict(violation) after calling engine.validateSelection(). This keeps the wiring testable without dependency injection frameworks. The test-mode sink in ExpenseTypeAnalyticsTracker should be a StreamController exposed as a Stream getter (e.g. testEventStream).

Close the controller in tearDown. For the PII allowlist test, serialize the ConflictBlockedEvent to a Map and use expect(event.toJson().keys, containsAll(allowlist)) combined with expect(event.toJson().keys.length, equals(allowlist.length)).

Testing Requirements

Use flutter_test's test() blocks with async/await. Instantiate ExpenseTypeAnalyticsTracker in test mode (in-memory sink). Wire it with MutualExclusionRuleEngine by calling the engine's validateSelection and passing violations to the tracker. Use expectLater with a stream matcher (emitsInOrder or emits) on the tracker's test event stream, with a timeout of 100 ms.

For the PII check, define an allowlist of permitted event fields (e.g. {'eventType', 'typeA', 'typeB', 'timestamp'}) and assert no additional keys exist in the serialised event map. Test at least: (1) kilometre-reimbursement + transit-ticket conflict, (2) a second distinct conflict if one exists, (3) a valid non-conflicting selection produces no event.

Component
Expense Type Analytics Tracker
infrastructure low
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.