critical priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

FormulaParameters is a Dart class (not a map or dynamic object) with typed fields: baseRate (double, NOK), distanceUnit (DistanceUnit enum: km/m), receiptThreshold (double, NOK), autoApprovalCeiling (double, km or NOK depending on expense type), vatCategory (VatCategory enum or String)
FormulaParameters implements copyWith returning a new instance with overridden fields — all fields are optional in copyWith
FormulaParameters implements == and hashCode based on all fields (value equality)
FormulaParameters implements toJson() returning a Map<String, dynamic> and a fromJson(Map<String, dynamic>) factory constructor
Round-trip test passes: FormulaParameters.fromJson(params.toJson()) == params for any valid instance
ExpenseTypeConfig in task-001 is updated to use FormulaParameters instead of raw fields (if it had inline formula fields)
The const map kExpenseTypeConfigs still compiles as const after this change
All fields have sensible defaults or are documented as required — no silent nulls
dart analyze reports no warnings or errors on the new file
The JSON key names follow camelCase and are documented with a brief comment per field

Technical Requirements

frameworks
Flutter
Dart
apis
Supabase remote config or feature flag endpoint (future — JSON shape must be compatible)
data models
FormulaParameters
ExpenseTypeConfig
DistanceUnit
VatCategory
performance requirements
fromJson and toJson must complete in under 0.1ms per call — no heavy computation
security requirements
fromJson must validate that baseRate and autoApprovalCeiling are non-negative; throw FormatException on invalid values rather than silently setting negative rates

Execution Context

Execution Tier
Tier 1

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.

Component
Expense Type Configuration
data 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.