high priority medium complexity testing pending testing specialist Tier 3

Acceptance Criteria

All three formula paths have dedicated test groups: per-km, flat receipt, and transit zone
Per-km tests cover: standard rate below cap, standard rate exactly at cap, standard rate above cap (capped result), zero distance (returns 0), negative distance (throws ArgumentError or returns 0 per spec)
Flat receipt tests cover: amount below threshold (no receipt required flag), amount at threshold boundary, amount above threshold (receipt required flag set), zero amount
Transit zone tests cover: single zone, exactly two zones, three or more zones (multi-zone rate), zone boundary values
Aggregation method tests verify: correct sum of all selected expense types, auto-approval flag true when total is below the auto-approval ceiling, auto-approval flag false when total meets or exceeds ceiling, mixed types (km + toll + parking) produce correct combined total
Edge case: missing or null ExpenseTypeConfig throws a typed ConfigurationException
Edge case: maximum representable amount does not cause integer overflow (use double/Decimal as appropriate)
100% branch coverage achieved on ExpenseCalculationService as reported by flutter_test --coverage
All tests are deterministic and pass without network calls or external state
Test file follows Arrange-Act-Assert structure with descriptive test names

Technical Requirements

frameworks
Flutter
flutter_test
Dart
data models
ExpenseType
ExpenseTypeConfig
FormulaParameters
ExpenseCalculationResult
performance requirements
Full test suite for this service must complete in under 2 seconds
security requirements
No real Supabase credentials or user data in test fixtures — use hardcoded NOK values

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Before writing tests, read the full ExpenseCalculationService implementation to map every branch (if/else, switch, ternary). Create a test fixture factory that builds ExpenseTypeConfig objects with controlled FormulaParameters — this avoids brittle hardcoded magic numbers spread across tests. For boundary value tests, use a data-driven table approach: final cases = [('zero distance', 0, 0.0), ('standard rate', 10, 45.0), ('at cap', 51, 229.50), ('above cap', 100, 229.50)]. For the auto-approval flag, confirm the exact threshold value from FormulaParameters (e.g., 50 km or NOK threshold) and test both sides of the boundary.

If the service currently has any untestable static dependencies, refactor those to constructor-injected parameters as part of this task.

Testing Requirements

Pure unit tests using flutter_test. No mocking frameworks needed — ExpenseCalculationService should be instantiable with injected config. Use group() blocks to organise by formula path. Use parametrised test patterns (for loops over test tables) for boundary value cases.

Run with: flutter test --coverage test/unit/expense_calculation_service_test.dart. Generate LCOV report and verify 100% branch coverage before marking task complete. Include at least 25 test cases total across all paths.

Component
Expense Calculation Service
service medium
Epic Risks (2)
high impact medium prob dependency

The per-km reimbursement rate and transit zone amounts must be read from org-specific configuration stored in Supabase. If the rate configuration table or RLS policies are not yet deployed when this epic runs, the calculation service cannot be completed and integration tests will fail.

Mitigation & Contingency

Mitigation: Define a RateConfigRepository interface and inject a stub implementation with default HLF rates from day one; write the real Supabase adapter in parallel and swap via dependency injection before merge.

Contingency: If org rate config is delayed beyond this epic's window, ship with the default-rate stub and log a prominent warning; calculate with defaults and surface a 'rates not confirmed' notice in the UI preview.

medium impact low prob technical

If the peer mentor opens an expense claim on two devices simultaneously, the local draft and the Supabase record may diverge. The repository's last-write-wins strategy could silently overwrite a valid selection with a stale one.

Mitigation & Contingency

Mitigation: Add an updated_at timestamp to the draft record and reject saves where the server timestamp is newer than the local copy; surface a conflict resolution prompt rather than silently overwriting.

Contingency: If conflict resolution UI is out of scope, fall back to server-authoritative reads on app foreground resume and discard local draft, notifying the user that their draft was refreshed from the server.