critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

ExpenseCalculationService.calculateAll(List<CalculationInput> inputs) returns a single ExpenseCalculationResult with: lineItems (all individual results), totalReimbursementAmount (sum of all line item amounts), autoApprovalEligible (bool), isIncomplete (bool — true if any line item is incomplete), and accountingExportPayload (Map<String, dynamic>)
autoApprovalEligible is true only when: total mileage across all per-km line items is <= 50 km AND no flat receipt line item has isIncomplete true AND no receipt-required type is present without a receipt
When inputs list is empty, the method returns an ExpenseCalculationResult with zero total, empty lineItems, autoApprovalEligible true, and isIncomplete false
accountingExportPayload contains at minimum: orgId, claimId, totalAmount, currency, lineItemCount, and a lineItems array with individual breakdown — sufficient for Xledger/Dynamics export
The method dispatches each CalculationInput to the correct formula method (calculatePerKm, calculateFlatReceipt, calculateTransitZone) based on formulaType
If a single formula calculation throws an unexpected exception, it is caught, the line item is omitted, and the result includes a calculationErrors list with the error details — the aggregation does not throw
The method is pure and synchronous — no side effects, no database writes
Performance: calling calculateAll with 10 simultaneous inputs completes in under 10ms

Technical Requirements

frameworks
Flutter
Dart
Riverpod
apis
expense-type-config (via individual formula methods)
data models
CalculationInput
ExpenseCalculationResult
PerKmResult
FlatReceiptResult
TransitZoneResult
ExpenseLineItem
performance requirements
Synchronous aggregation over at most ~10 inputs — no async needed
No redundant config lookups — config is fetched once at service initialization
security requirements
Auto-approval threshold (50 km) must be read from org config, not hardcoded, to support org-level overrides
The accounting export payload must not include raw user-input data without validation — only computed values

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Implement calculateAll using a fold/map over the inputs list: map each CalculationInput to its result via a switch on formulaType (exhaustive — sealed class guarantees all cases handled), wrap each call in a try/catch to collect errors without crashing. The auto-approval logic should be extracted into a private _isAutoApprovalEligible(List items, ExpenseTypeConfig config) method for testability. The 50 km threshold check must sum ALL PerKmResult items in lineItems (there could be multiple if a claim has two separate mileage legs). The accounting export payload shape should be agreed with the Xledger/Dynamics integration team — use a flexible Map for now and replace with a typed class in a later integration task.

This is the hot path called on every BLoC state change (every expense type toggle), so keep it lean — no logging, no async, no DB access.

Testing Requirements

Write unit tests using flutter_test covering: (1) empty input list — zero result, autoApprovalEligible true, (2) single per-km input — correct delegation and result, (3) mixed inputs (per-km + flat receipt) — correct total and line items, (4) mileage exactly at 50 km threshold — autoApprovalEligible true, (5) mileage at 51 km — autoApprovalEligible false, (6) flat receipt with missing receipt — isIncomplete true on result, autoApprovalEligible false, (7) one formula throws — error is captured in calculationErrors, other line items still computed, (8) accountingExportPayload contains required fields, (9) three transit zones + two flat receipts — correct total sum. Aim for 90%+ branch coverage.

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.