Add formula parameter model to expense config
epic-expense-type-selection-foundation-task-002 — Extend ExpenseTypeConfig with a FormulaParameters value object containing: base rate (NOK), distance unit (km/m), receipt-required threshold (NOK), auto-approval ceiling (km or NOK), and applicable VAT category. Implement copyWith, equality, and JSON serialisation so config can be loaded from remote feature flags without code changes.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Place FormulaParameters at lib/features/expenses/domain/formula_parameters.dart. Use const constructors throughout so config instances remain compile-time constants when values are known. For copyWith, the idiomatic Dart pattern is: FormulaParameters copyWith({double? baseRate, DistanceUnit?
distanceUnit, ...}) => FormulaParameters(baseRate: baseRate ?? this.baseRate, ...). For equality, either implement manually or use package:equatable — check if equatable is already in pubspec.yaml before adding a dependency. For JSON keys, use lowerCamelCase matching Supabase column naming conventions (baseRate, distanceUnit, receiptThreshold, autoApprovalCeiling, vatCategory).
Define DistanceUnit as a simple enum { km, m } with a toJson/fromJson extension. VatCategory can be a String enum or a dedicated enum depending on how many VAT categories exist in the Norwegian context (typically 0%, 12%, 25%).
Testing Requirements
Unit tests covering: (1) toJson produces expected map keys and values, (2) fromJson reconstructs identical object from toJson output (round-trip), (3) copyWith with one overridden field returns new instance with that field changed and all others unchanged, (4) two instances with identical field values are equal (==) and have the same hashCode, (5) two instances with one differing field are not equal, (6) fromJson with a negative baseRate throws FormatException. All tests are pure Dart with no Flutter or Supabase dependencies. Run with flutter test.
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.