Implement Supabase sync for finalised expense selections
epic-expense-type-selection-core-services-task-012 — Implement the persistFinalSelection method in ExpenseTypeRepository that writes the validated, finalised expense type set and ExpenseCalculationResult to the Supabase expense_claims table via the Supabase client. Map the calculation result line items to the accounting export fields required by Xledger and Dynamics. Wrap the upsert in a transaction so partial writes cannot produce inconsistent database state.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Implement persistFinalSelection as an async method in ExpenseTypeRepository. Use supabase.rpc('finalise_expense_claim', params: {...}) rather than individual PostgREST upserts — this lets the database handle the transaction boundary atomically. Define a Supabase Edge Function or database function 'finalise_expense_claim' that accepts the full claim payload and performs the upsert + line item insert in a single transaction. The Dart client only makes one network call.
For Xledger field mapping, define a static XledgerLineItemMapper class that converts an ExpenseCalculationLineItem to a map of Xledger-specific fields — keep this mapping tested independently of the repository. Do the same for DynamicsLineItemMapper. This separation makes it easy to update mappings when Xledger or Dynamics schema changes without touching the repository. The workshop notes confirm Blindeforbundet uses Xledger and HLF uses Dynamics — the mapper must be parameterised by organisation to produce the correct output format per org.
Testing Requirements
Integration tests using a Supabase test project (or local Supabase via Docker). Test cases: (1) valid finalisation upserts correct row with all fields populated, (2) re-upsert with same claimContextId updates existing row — no duplicate rows, (3) upsert with mismatched organisation_id is rejected by RLS and throws ExpensePersistenceException, (4) simulated network failure throws ExpensePersistenceException and local draft is preserved, (5) Xledger field mapping produces correct account_code and vat_code for each expense type. Mock the Xledger and Dynamics clients in unit tests; use real Supabase in integration tests. Verify clearDraft is called after successful persist using a spy on the draft repository.
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.
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.