critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

When expense amount > threshold (default 100 NOK), submission is rejected if receipt_reference is null or empty
When expense amount <= threshold, submission proceeds regardless of receipt_reference presence
Threshold value is read from ExpenseThresholdConfig, not hardcoded
Returned error is of type MissingReceipt and includes the configured threshold value in its payload
Threshold config supports per-organisation overrides (HLF default 100 NOK)
Zero-amount expenses are not rejected for missing receipt
Negative amount edge case is handled gracefully (treated as zero or rejected upstream)
Validation does not make any network calls โ€” it is a pure synchronous check against the draft object
Unit tests cover: amount exactly at threshold (pass), amount one รธre over (fail), receipt present with high amount (pass), null receipt with low amount (pass)

Technical Requirements

frameworks
Flutter
Riverpod
data models
ExpenseDraft
ExpenseThresholdConfig
MissingReceipt
performance requirements
Validation must complete in under 1 ms (pure in-memory check)
No async I/O โ€” config must be pre-loaded by the time validation runs
security requirements
Threshold config must not be modifiable by the end user at runtime
Receipt reference must be server-assigned, not client-generated, to prevent bypass

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement as a private method `_validateReceiptRequirement(ExpenseDraft draft, ExpenseThresholdConfig config)` returning `MissingReceipt?`. Keep it a pure function with no side effects. ExpenseThresholdConfig should be injected via Riverpod Provider so tests can override it. Use a sealed class or freezed union for ValidationError types to guarantee exhaustive handling in the UI layer.

Avoid using dynamic or Object as the error type. The threshold comparison should use integer รธre arithmetic (multiply NOK by 100) to avoid floating-point precision issues when comparing e.g. 99.999 NOK.

Testing Requirements

Unit tests using flutter_test. Cover: (1) amount below threshold, no receipt โ†’ valid; (2) amount above threshold, no receipt โ†’ MissingReceipt error; (3) amount above threshold, receipt present โ†’ valid; (4) amount exactly at threshold โ†’ valid (inclusive lower bound); (5) threshold loaded from config, not hardcoded; (6) null receipt_reference treated same as empty string. No integration or e2e tests needed for this pure logic unit.

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.