critical priority low complexity backend pending backend specialist Tier 0

Acceptance Criteria

ExpenseCalculationResult is an immutable Dart class (or freezed data class) with fields: totalReimbursementAmount (double), currency (String, default 'NOK'), lineItems (List<ExpenseLineItem>), formulaType (ExpenseFormulaType enum), autoApprovalEligible (bool), and accountingExportPayload (Map<String, dynamic>?)
ExpenseTypeFormula is a sealed class with three concrete subtypes: PerKmFormula (rate, distanceKm, capKm), FlatReceiptFormula (declaredAmount, receiptRequired, approvalRequired), and TransitZoneFormula (zoneIds, zoneAmounts)
CalculationInput is an immutable value object with fields: expenseTypeId (String), formulaType (ExpenseFormulaType), rawInputs (Map<String, dynamic>), orgId (String), and claimId (String)
ExpenseLineItem is a sealed class with subtypes PerKmResult, FlatReceiptResult, and TransitZoneResult, each containing the relevant typed breakdown fields
All models implement copyWith, equality (==), hashCode, and toString — either manually or via freezed code generation
ExpenseFormulaType enum contains at minimum: perKm, flatReceipt, transitZone
All models are exported from a single barrel file (e.g., expense_calculation_models.dart) with no circular imports
Unit tests verify equality, copyWith behavior, and JSON serialization/deserialization for all model types
No business logic lives inside model classes — they are pure data containers

Technical Requirements

frameworks
Flutter
Dart
freezed (optional but recommended for immutability)
data models
ExpenseCalculationResult
ExpenseTypeFormula
CalculationInput
ExpenseLineItem
PerKmResult
FlatReceiptResult
TransitZoneResult
performance requirements
Model instantiation must be allocation-efficient — avoid unnecessary object graphs
JSON serialization must handle null fields gracefully without throwing
security requirements
No sensitive org-specific rate data should be embedded in model defaults — rates come from config at runtime
Currency field must be validated to an allowlist (e.g., NOK only) to prevent injection

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use Dart sealed classes (available since Dart 3.0) for ExpenseTypeFormula and ExpenseLineItem — this gives exhaustive switch support at the BLoC layer and prevents unhandled formula types at compile time. Consider using the freezed package to auto-generate copyWith, equality, and JSON methods; if freezed is already used elsewhere in the project, align with that pattern. Place all models under lib/features/expense/models/ or a shared domain models directory. The CalculationInput.rawInputs map should be typed as Map to allow each formula type to carry its own input shape without requiring a separate class per formula at this stage — the service layer will cast to typed accessors.

Define currency as a String constant 'NOK' for now; do not introduce a Currency enum unless multi-currency is scoped. Avoid placing the 100 NOK receipt threshold or the 50 km auto-approval threshold as model-level constants — these belong in config/service, not models.

Testing Requirements

Write unit tests using flutter_test for all model classes. Cover: (1) default construction with required fields, (2) copyWith producing a new instance with changed fields and leaving others unchanged, (3) equality between two instances with identical values, (4) JSON round-trip (toJson → fromJson produces equal object), (5) sealed class exhaustive pattern matching — ensure the Dart compiler enforces all subtypes are handled. No integration or e2e tests required for this task. Target 100% line coverage on all model files.

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.