critical priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

ExpenseTypeAccessibilityService is a pure Dart class (no Flutter widget dependencies) with three public methods
getSemanticLabel(ExpenseType type, {required bool isSelected, required bool isDisabled}) returns a non-empty Norwegian string that includes the expense type name, selection state, and disabled reason when isDisabled is true
The returned semantic label for a selected, non-disabled type passes WCAG 2.2 SC 1.3.1: it conveys role (checkbox/toggleable), state (selected/not selected), and name without relying on colour alone
getLiveRegionAnnouncement(ExpenseTypePair blocked) returns a Norwegian string explaining which two types conflict and why they cannot be combined — string must be complete and standalone (no pronoun-only references)
getFocusOrderHint(List<ExpenseType> activeTypes) returns a List<ExpenseType> sorted in logical reading order (top-left to bottom-right, matching visual layout order defined in ExpenseTypeConfig)
All user-facing strings are ARB keys looked up via AppLocalizations — zero hardcoded Norwegian text in the service file
ARB keys follow the naming convention expenseType_semanticLabel_{state} and expenseType_conflictAnnouncement
The service is injectable (accepts AppLocalizations or a LocalizationProvider in its constructor) to enable unit testing without a widget tree
The service file is at lib/features/expense_type/services/expense_type_accessibility_service.dart

Technical Requirements

frameworks
Flutter
flutter_localizations
intl
data models
ExpenseType
ExpenseTypePair
ExpenseTypeConfig
performance requirements
All three methods return synchronously — no async operations
getSemanticLabel completes in < 1 ms (pure string lookup + interpolation)
security requirements
Semantic labels must not expose internal IDs, database keys, or system state — only user-facing descriptive text

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

The key architectural decision is making the service depend on AppLocalizations (or an abstraction) rather than calling Localizations.of(context) internally — this keeps it a pure service testable without a widget tree. Define a LocalizationDelegate interface if needed. For getSemanticLabel, compose the string from three ARB fragments: the type name, the selected state, and the disabled state — this allows translators to adjust word order per language without code changes. For getFocusOrderHint, derive sort order from ExpenseTypeConfig.visualOrder (an integer field) — if this field doesn't exist yet, add it as part of this task.

WCAG 2.2 SC 1.3.1 compliance requires that the semantic label alone — without any visual context — conveys the full state to a screen reader user. Test with TalkBack on Android and VoiceOver on iOS in a manual review pass.

Testing Requirements

Unit tests using flutter_test with a stub/mock AppLocalizations that returns predictable English strings (to avoid test brittleness against Norwegian text changes). Test each method: getSemanticLabel for all combinations of isSelected × isDisabled (4 combinations) × at least 2 ExpenseType variants. getLiveRegionAnnouncement for a known conflicting pair. getFocusOrderHint for an empty list, single item, and multi-item list asserting sort order matches ExpenseTypeConfig visual order.

Assert no hardcoded Norwegian strings appear in the service file via a static analysis check (grep in CI).

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.