high priority low complexity backend pending backend specialist Tier 0

Acceptance Criteria

isReceiptRequired(amount) returns true when amount is strictly greater than 100 kr (default threshold)
isReceiptRequired(amount) returns false when amount is exactly 100 kr (boundary: threshold is exclusive)
isReceiptRequired(amount) returns false when amount is 0 or negative
getThresholdConfig() returns a ThresholdConfig object containing the threshold value and currency code
When org-specific configuration overrides the threshold, isReceiptRequired uses the org value, not the hardcoded default
If org config is unavailable or malformed, service falls back to the default 100 kr threshold and logs a warning
Service is stateless and injectable — no shared mutable state
All boundary conditions (exactly at threshold, one øre above, zero, negative) have dedicated unit tests
Currency is stored as a string code (e.g., 'NOK') and does not affect comparison logic (amounts are always in the same currency)

Technical Requirements

frameworks
Flutter
Riverpod
data models
ThresholdConfig (thresholdAmount: double, currency: String, orgId: String)
OrgConfiguration (org-level settings loaded from Supabase or local config)
performance requirements
isReceiptRequired must be synchronous and complete in under 1 ms — no async I/O on the hot path
getThresholdConfig may be async if config is fetched remotely, but result must be cached after first load
security requirements
Threshold configuration must not be modifiable by the end user — read from server-side or bundled org config only
No PII is handled by this service

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Represent monetary amounts as double or, preferably, as an integer number of øre (smallest currency unit) to avoid floating-point comparison bugs. If using doubles, use a small epsilon only at the display layer — comparisons in this service should be exact. The threshold value (100 kr) originates from the HLF workshop requirement: 'Kvitteringsbilde for utlegg over 100 kr'. Store this as a named constant (kDefaultReceiptThresholdNOK = 100.0) and document its business origin.

Load org config via a Riverpod provider that reads from Supabase's org_settings table or a locally cached version. This service will be read on every keystroke in the amount field — keep it O(1) and allocation-free.

Testing Requirements

Unit tests (flutter_test): cover isReceiptRequired with amounts of 0, 50, 99.99, 100.00, 100.01, and 500. Cover getThresholdConfig with a mock config source returning an org-specific override and with a missing/null config to verify fallback. All tests must be synchronous where the method is synchronous. Target 100% branch coverage for this service given its small surface area.

Component
Receipt Attachment Service
service high
Epic Risks (3)
high impact medium prob technical

Non-blocking upload creates a race condition: if the claim record is submitted and saved before the upload completes, the storage path may never be written to the claim_receipts table, leaving the claim with a missing receipt that was nonetheless required.

Mitigation & Contingency

Mitigation: Design the attachment service to queue a completion callback that writes the storage path to the claim record upon upload completion, even after the claim form has submitted. Use a local task queue with persistence to survive app backgrounding. Test the race condition explicitly with simulated slow uploads.

Contingency: If the async path association proves unreliable, fall back to blocking upload before claim submission with a clear progress indicator, accepting the UX trade-off in exchange for data integrity.

high impact medium prob scope

The offline capture requirement (cache locally, sync when connected) significantly increases state management complexity. If the offline queue is not durable, receipts captured without connectivity may be lost when the app is killed, causing claim submission failures users are not aware of.

Mitigation & Contingency

Mitigation: Persist the offline upload queue to local storage (e.g., Hive or SQLite) on every state transition. Implement background sync using WorkManager (Android) and BGTaskScheduler (iOS). Scope the initial delivery to online-only flow if offline sync cannot be adequately tested before release.

Contingency: Ship without offline support in the first release, displaying a clear 'Upload requires connection' message. Add offline sync as a follow-on task once the core online flow is validated in production.

medium impact low prob integration

The inline bottom sheet presentation within a multi-step wizard can conflict with existing modal navigation and back-button handling, particularly if the expense wizard itself uses nested navigation or custom route management.

Mitigation & Contingency

Mitigation: Review the expense wizard navigation architecture before implementation. Use showModalBottomSheet with barrier dismissal disabled to prevent accidental dismissal. Coordinate with the expense wizard team on modal stacking behavior and ensure the camera sheet does not interfere with wizard step transitions.

Contingency: If modal stacking causes navigation issues, present the camera sheet as a full-screen dialog using PageRouteBuilder with a transparent barrier, preserving wizard state via the existing Bloc while still appearing inline.