Implement anonymised analytics event model
epic-expense-type-selection-foundation-task-004 — Define the AnonymisedExpenseEvent data class with fields: eventType (enum: typeSelected, typeDeselected, conflictBlocked, selectionConfirmed), sessionId (UUID, not user-linked), selectedTypes (List<ExpenseType>), blockedPair (nullable ExpenseTypePair), and timestampMs. Ensure no PII is present — the class must pass a static PII-field linter rule.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Use freezed for immutability and code generation — this eliminates manual copyWith and == boilerplate. Annotate with @JsonSerializable(explicitToJson: true) to ensure nested ExpenseType and ExpenseTypePair serialise correctly. For the PII lint rule, the simplest approach is a custom dart_code_metrics or custom_lint rule that checks field names against a blocklist; alternatively document it as a code review checklist item enforced in CI. Generate sessionId with the uuid package (Uuid().v4()) at the call site (in the tracker), not inside the model constructor — keeping the model a pure data container makes it easier to test with known session IDs.
Use DateTime.now().millisecondsSinceEpoch for timestampMs at event creation time.
Testing Requirements
Unit tests covering: construction of all four eventType variants, JSON round-trip for each variant (including null blockedPair and populated blockedPair), assertion that sessionId is a valid v4 UUID format (regex check), assertion that selectedTypes serialises as string keys not integers, and a PII-field presence check (assert no field named userId/email/name etc. exists via reflection or manual check). Use flutter_test. No async or platform dependencies.
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.