Build analytics pipeline with buffered dispatch
epic-expense-type-selection-foundation-task-005 — Implement ExpenseTypeAnalyticsTracker as a singleton service with an in-memory event buffer (max 50 events) and a flush strategy: flush on buffer-full, on app-background, or every 30 s. Dispatch events via the existing push-notification infrastructure's analytics sink or a stub in non-production environments. All dispatch calls must be fire-and-forget with no UI blocking.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
Define an abstract AnalyticsSink interface with a single method: Future
The 30-second timer should be created lazily on first trackEvent() call and cancelled in a dispose() method. Use unawaited(sink.dispatch(batch)) wrapped in try/catch to ensure fire-and-forget. Be careful not to clear the buffer before creating the dispatch batch — copy the list, clear the buffer, then dispatch the copy to avoid race conditions if trackEvent is called during flush.
Testing Requirements
Unit tests (task-006 covers this fully). For this task, provide a working StubAnalyticsSink and expose the AnalyticsSink interface. Manually verify in debug mode that debugPrint output appears for each tracked event. Integration smoke test: run the app in debug mode, trigger 5 expense type selections, and confirm stub sink logs appear in the console.
No automated integration tests required here — covered by task-006.
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.