high priority low complexity infrastructure pending backend specialist Tier 0

Acceptance Criteria

ExpenseThresholdConfig class exposes synchronous getters: maxKmForAutoApproval → double?, maxAmountNoReceiptRequired → double?, currency → String, requiresReceiptAbove → double?, maxKmWithoutReceipt → double?
initialize(String orgId) → Future<void> fetches thresholds from Supabase and populates the in-memory cache before the method returns
Cached values persist to local storage (flutter_secure_storage or Hive) with a TTL of 24 hours so they are available immediately on next cold start
On cold start, cached values are loaded synchronously into memory during app init, then a background refresh is triggered if TTL has elapsed
If Supabase fetch fails and no cache exists, initialize() throws ExpenseConfigUnavailableException — this must be handled by app startup to show a user-friendly error
If Supabase fetch fails but a valid cache exists (even if TTL expired), the stale cache is used and a warning is emitted to the error logger
isAutoApprovalEligible(double amountNok, double? distanceKm) → bool evaluates thresholds synchronously using the cached values
isReceiptRequired(double amountNok) → bool returns true if amount exceeds the receipt threshold
The service is a singleton (Riverpod Provider.keepAlive or static instance) — threshold config does not change during an app session unless explicitly refreshed
refreshIfStale() → Future<void> can be called on foreground app resume to silently update stale values
Threshold config is scoped per organisation — switching org in app triggers a new initialize() call clearing the previous org's thresholds
All threshold fields default to null (no restriction) if not configured for an org — isAutoApprovalEligible returns false when thresholds are null (conservative default)

Technical Requirements

frameworks
Flutter
Riverpod
supabase_flutter
hive_flutter or flutter_secure_storage (cache persistence)
apis
Supabase PostgreSQL 15 (select from org_expense_thresholds or similar config table)
data models
activity_type
annual_summary
performance requirements
Synchronous getter calls must return in under 1ms (in-memory map lookup)
initialize() must complete within 1 second on a 4G connection
Cold start cache load must complete before first frame render — use early initialization in main()
security requirements
Threshold config is organisation-scoped — verify fetched config belongs to the authenticated user's org via RLS
Threshold values are not sensitive PII, but must not be user-modifiable from the client — config table must be read-only via RLS for non-admin roles
Cached threshold data does not require encryption (non-sensitive operational config)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Model the threshold config as a Supabase table (e.g., `organization_expense_settings`) with columns: organization_id, max_km_auto_approval, max_amount_no_receipt, receipt_required_above_nok, auto_approval_enabled. Use a separate table rather than embedding in the organizations table to keep org data and operational config separate. For local cache, Hive is preferred over flutter_secure_storage for this use case since the data is not sensitive and Hive's synchronous read API maps cleanly to the synchronous getter requirement. Cache the raw JSON response and parse on load.

The singleton pattern in Riverpod should use `ref.keepAlive()` in a `StreamProvider` or a plain `Provider` with a `dispose` callback that cancels any background refresh timers. Expose the service via a Riverpod `Provider` that throws if not yet initialized — force initialization at app startup in the ProviderScope's `overrides` or via a splash screen guard.

Testing Requirements

Unit tests (flutter_test + mocktail): mock Supabase client and test initialize() with successful fetch, test fallback to stale cache on network failure, test ExpenseConfigUnavailableException when both network and cache unavailable. Test isAutoApprovalEligible() with boundary values (exactly at threshold, 1 NOK over, 1 km over). Test TTL logic: inject a mock clock that returns a time 25 hours after cache write to verify stale detection. Test org switching clears previous org's thresholds.

Integration test: load real thresholds from local Supabase, verify synchronous getters return correct values after initialize(). Minimum 90% line coverage on the service class.

Component
Expense Threshold Configuration
infrastructure low
Epic Risks (3)
high impact medium prob security

Row-level security policies for expense claims must correctly scope data to organisation, role (peer mentor sees own claims only, coordinator sees org-wide queue), and claim status. Incorrect RLS can expose claims cross-organisation or prevent coordinators from accessing the attestation queue.

Mitigation & Contingency

Mitigation: Define RLS policies in code-reviewed migration files. Write integration tests that attempt cross-org reads with different JWT roles and assert access denial. Review with a second engineer before merging migrations.

Contingency: If RLS is misconfigured post-deployment, disable the affected policy temporarily and apply a hotfix migration within the same release window. No claim data is exposed publicly due to Supabase project-level auth requirement.

medium impact medium prob technical

The auto-approval Edge Function is triggered server-side on expense insert. Cold-start latency or Edge Function failures can block the submission response and degrade UX, especially on mobile networks.

Mitigation & Contingency

Mitigation: Implement the auto-approval Edge Function client with a timeout and graceful fallback: if no result is received within 5 seconds, treat the claim as 'pending' and poll for the status update via Supabase Realtime. Keep the Edge Function warm with a periodic ping.

Contingency: If Edge Function reliability is unacceptable, move auto-approval evaluation to a database trigger or Postgres function as an interim measure, accepting that threshold configuration changes require a migration rather than a settings update.

medium impact low prob scope

The expense type catalogue and threshold configuration are cached locally for offline use. If an organisation updates their catalogue exclusion rules or thresholds while a peer mentor is offline, the local cache may allow submissions that violate the new policy.

Mitigation & Contingency

Mitigation: Cache entries include a TTL (24 hours). On connectivity restore, refresh cache before allowing new submissions. Server-side validation in the Edge Function and save functions provides a second enforcement layer.

Contingency: If a stale-cache submission passes client validation but fails server validation, surface a clear error message explaining that the expense type rules have been updated and prompt the user to review their selection with the refreshed catalogue.