high priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

Validation only runs when expense_type is mileage — non-mileage drafts are not affected
Distance below configured minimum returns DistanceBelowMinimum error with the minimum value in the payload
Distance above configured maximum returns DistanceAboveMaximum error with the maximum value in the payload
Distance exactly at minimum boundary is accepted
Distance exactly at maximum boundary is accepted
Zero or negative distance for a mileage expense returns DistanceBelowMinimum
DistanceBelowMinimum and DistanceAboveMaximum are distinct types (not a generic range error)
Config values are loaded from the organisation-scoped distance range config, not hardcoded
HLF auto-approval threshold (50 km) is a separate concern and must NOT be encoded here
Unit tests cover all boundary conditions and the non-mileage pass-through

Technical Requirements

frameworks
Flutter
Riverpod
data models
ExpenseDraft
DistanceRangeConfig
DistanceBelowMinimum
DistanceAboveMaximum
performance requirements
Pure synchronous check, sub-millisecond execution
security requirements
Distance range config must be sourced from server-side organisation settings, not client-editable state

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement as `_validateDistanceRange(ExpenseDraft draft, DistanceRangeConfig config)` returning `DistanceOutOfRange?` where DistanceOutOfRange is a sealed class with DistanceBelowMinimum and DistanceAboveMaximum variants. Store distance as integer metres (not double km) to avoid floating-point comparison bugs. Guard with `if (draft.expenseType != ExpenseType.mileage) return null;` at the top. Do not conflate range validation with the 50 km auto-approval rule — that belongs in the auto-approval edge function logic.

Testing Requirements

Unit tests using flutter_test. Parametrize tests over boundary values: (min-1 → fail below), (min → pass), (mid → pass), (max → pass), (max+1 → fail above). Also test: expense_type != mileage skips distance check; null distance on mileage type is treated as 0 (DistanceBelowMinimum). Use a test double for DistanceRangeConfig to avoid production config coupling.

Component
Expense Validation Service
service high
Epic Risks (3)
high impact medium prob scope

Mutual exclusion rules are stored in the expense type catalogue's exclusive_groups field. If the catalogue schema or group definitions differ between HLF and Blindeforbundet, the validation service must handle multiple group configurations without hardcoding organisation-specific logic.

Mitigation & Contingency

Mitigation: Design the validation service to be purely data-driven: it reads exclusive_groups from the cached catalogue and enforces whichever groups are defined, with no hardcoded organisation names. Write parameterised unit tests covering at least 4 different catalogue configurations to verify generality.

Contingency: If an organisation requires non-standard exclusion semantics (e.g. partial exclusion within a group), introduce an exclusion_type field to the catalogue schema and extend the service, treating it as a catalogue configuration change rather than a code fork.

medium impact high prob technical

The attestation service subscribes to Supabase Realtime for live queue updates. On mobile, Realtime WebSocket connections can be dropped during network transitions, causing the coordinator queue to become stale without the user being aware.

Mitigation & Contingency

Mitigation: Implement connection lifecycle management: reconnect on network-change events, show a 'reconnecting' indicator when the subscription is broken, and perform a full queue refresh on reconnect rather than relying solely on delta events.

Contingency: Add a manual pull-to-refresh gesture on the attestation queue screen as a guaranteed fallback. If Realtime proves unreliable in production, switch to periodic polling (30-second interval) as a degraded but functional mode.

medium impact medium prob integration

If a peer mentor submits a draft while offline and then submits the same claim again after connectivity is restored (thinking the first attempt failed), duplicate claims may be persisted in Supabase.

Mitigation & Contingency

Mitigation: Assign a client-generated idempotency key (UUID) to each draft at creation time. The submission service sends this key as an upsert key to Supabase, preventing duplicate inserts. The draft is marked 'submitted' locally after first successful upload.

Contingency: Implement a server-side duplicate detection trigger on the expense_claims table checking (activity_id, claimant_id, created_date) within a 24-hour window and returning the existing record ID rather than inserting a duplicate.