high priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

AnonymisedExpenseEventType enum defines exactly four variants: typeSelected, typeDeselected, conflictBlocked, selectionConfirmed
AnonymisedExpenseEvent is an immutable data class (freezed or const constructor) with fields: eventType, sessionId (String UUID), selectedTypes (List<ExpenseType>), blockedPair (ExpenseTypePair?), timestampMs (int)
sessionId is generated from uuid package (v4) and is NOT derived from user ID, device ID, or any persistent identifier
The class serialises to JSON via toJson() and deserialises via fromJson() with lossless round-trip
A custom lint rule or analysis_options.yaml entry flags any field named userId, email, name, phone, address, or personalNumber — AnonymisedExpenseEvent must contain none of these
blockedPair is nullable and correctly serialised as null when absent
selectedTypes serialises as a JSON array of ExpenseType string keys, not integer indices
timestampMs is an int (milliseconds since epoch), never a DateTime object, to avoid timezone serialisation edge cases
The file is placed in lib/features/expense_type/models/anonymised_expense_event.dart

Technical Requirements

frameworks
Flutter
freezed
json_serializable
data models
AnonymisedExpenseEvent
AnonymisedExpenseEventType
ExpenseType
ExpenseTypePair
performance requirements
Object construction is O(1) — no async operations at creation time
JSON serialisation completes synchronously
security requirements
sessionId must be a transient UUID (v4) generated per session, not stored in Supabase user table or linked to auth state
Class must contain zero PII fields — enforce via lint rule in analysis_options.yaml
No user-identifying data may appear even in debug toString() output

Execution Context

Execution Tier
Tier 1

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.

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.