high priority low complexity backend pending backend specialist Tier 2

Acceptance Criteria

announce(String message, {required AnnouncementPoliteness politeness}) is added to ExpenseTypeAccessibilityService, routing through an injectable AnnouncementSink
AnnouncementPoliteness is an enum with two variants: assertive (conflict blocks) and polite (selection confirmations)
The real AnnouncementSink implementation calls SemanticsService.announce(message, textDirection) with TextDirection.ltr
A debounce of exactly 150 ms is applied per politeness level: multiple polite announcements within 150 ms collapse to the last one; assertive announcements bypass the debounce (or use a shorter 50 ms debounce) to ensure conflicts are never silently dropped
Calling announce() is synchronous from the caller's perspective — debounce is handled internally via Timer
A TestAnnouncementSink (stub implementing AnnouncementSink) records all announced messages in a List<AnnouncementRecord> without calling SemanticsService
Widget tests inject TestAnnouncementSink and assert message content and politeness level without requiring accessibility semantics to be enabled in the test environment
The service integrates with getLiveRegionAnnouncement() from task-007: a convenience method announceConflict(ExpenseTypePair) composes the message and calls announce() with assertive politeness
The service integrates with selection confirmation flow: announceSelectionConfirmed(ExpenseType) composes a polite confirmation message

Technical Requirements

frameworks
Flutter
flutter_test
apis
SemanticsService.announce (Flutter framework)
data models
ExpenseType
ExpenseTypePair
AnnouncementPoliteness
AnnouncementRecord
performance requirements
announce() returns in < 1 ms — Timer setup is synchronous
Debounce must not block the UI thread — uses Timer, not await Future.delayed
security requirements
Announced strings must not contain PII — validate against same PII-field lint rule as analytics events

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

The AnnouncementSink interface should be: abstract class AnnouncementSink { void announce(String message, TextDirection textDirection, AnnouncementPoliteness politeness); }. The real implementation wraps SemanticsService.announce — note that SemanticsService.announce does not natively support politeness levels on all platforms; map assertive to immediate dispatch and polite to debounced dispatch as a pragmatic implementation. For the debounce, maintain two Timer? fields (_politeTimer and _assertiveTimer) and cancel/restart on each call.

The convenience methods announceConflict and announceSelectionConfirmed should be the primary API that consuming BLoC/Cubit classes call — raw announce() is semi-internal. Register the service with Riverpod using a Provider that is overridable in tests, consistent with the pattern established in task-005 for AnalyticsSink.

Testing Requirements

Unit and widget tests using flutter_test. Unit tests: debounce collapses 3 rapid polite announcements to 1 (using FakeAsync), assertive announcement fires immediately (not debounced past 50 ms), sink receives correct politeness level per announcement type. Widget tests: inject TestAnnouncementSink into ExpenseTypeAccessibilityService via Riverpod override, trigger a conflict block in the UI, assert TestAnnouncementSink.records contains an assertive announcement with the expected conflict message. Do not use tester.semantics or SemanticsController — the TestAnnouncementSink approach makes widget tests fully portable across platforms.

Component
Expense Type Accessibility Service
service 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.