critical priority low complexity database pending database specialist Tier 0

Acceptance Criteria

OrgReceiptThresholdConfig is an immutable Dart model with fields: orgId (String), receiptRequiredThresholdNok (double, default 100.0)
A Supabase query reads the threshold from the org configuration table for the authenticated user's orgId
Default of 100.0 NOK is returned when no org-specific override exists in the database
Threshold is cached in-memory per app session after first fetch to avoid repeated network calls
Cached value is also persisted to local storage (SharedPreferences or Hive) for offline access
ReceiptThresholdValidator.isReceiptRequired(double amountNok) returns true when amountNok >= threshold
Cache invalidation: cached threshold is refreshed when the app comes to foreground after >1 hour in background
RLS enforcement: org A cannot read org B's threshold configuration
Unit tests cover: threshold loaded from Supabase, default fallback, offline cached value used when network unavailable
Repository method signature: Future<OrgReceiptThresholdConfig> getThresholdConfig(String orgId)

Technical Requirements

frameworks
Flutter
Riverpod
dart
apis
Supabase PostgreSQL 15
data models
activity_type
performance requirements
Threshold fetch must complete in <500ms on a 4G connection
Cached value must be returned synchronously (from memory) for all calls after first fetch in session
Local persistence read must complete in <50ms on app startup
security requirements
RLS policy on org configuration table must restrict reads to authenticated users within the same org
Supabase anon key used for read — service role key never used client-side
Org ID sourced from authenticated JWT claims, not user-supplied input, to prevent cross-org data access

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Store the threshold in the existing org settings table if one exists, or create a lightweight org_receipt_config table with columns: org_id (FK), receipt_required_threshold_nok (numeric, default 100). For local caching, use SharedPreferences with key 'receipt_threshold_{orgId}' and a timestamp key 'receipt_threshold_{orgId}_fetched_at'. On app resume, check if fetched_at is older than 1 hour and trigger a background refresh. Expose the config via a Riverpod AsyncNotifierProvider so UI can react to config changes.

The validator itself should be a pure function taking the threshold and amount as parameters — dependency on the config should be at the caller level, not inside the validator, to keep it testable.

Testing Requirements

Unit tests using flutter_test with mockito. Scenarios: (1) Supabase returns a row → OrgReceiptThresholdConfig constructed with correct threshold, (2) Supabase returns empty list → default 100.0 NOK returned, (3) cached value returned on second call without Supabase being called again (verify mock not called twice), (4) isReceiptRequired(99.99) returns false, isReceiptRequired(100.0) returns true, isReceiptRequired(150.0) returns true, (5) offline scenario: local cache returns last-known value when Supabase throws SocketException. Integration test (optional): verify against test Supabase project that RLS prevents cross-org threshold access.

Component
Receipt Threshold Validator
service low
Epic Risks (3)
high impact medium prob security

Supabase Storage RLS policies using org/user/claim path scoping may not enforce correctly if claim ownership is not present in the JWT or if path segments are constructed differently at upload vs. read time, leading to data leakage or access denial for legitimate users.

Mitigation & Contingency

Mitigation: Define and test RLS policies in isolation before wiring to app code. Write integration tests that assert cross-org and cross-user access is denied. Use service-role key only in edge functions, never in client code.

Contingency: If client-side RLS proves insufficient, route all storage reads through a Supabase Edge Function that validates ownership before generating signed URLs, adding a controlled server-side enforcement layer.

high impact medium prob technical

Aggressive image compression may reduce receipt legibility below the threshold required for financial auditing, causing claim rejections or compliance failures despite technically successful uploads.

Mitigation & Contingency

Mitigation: Define minimum legibility requirements with HLF finance team before implementation. Set compression targets conservatively (e.g., max 1MB, min 80% JPEG quality) and validate with sample receipt images. Provide compression statistics in verbose/debug mode.

Contingency: If post-compression quality is disputed by auditors, increase the quality floor at the cost of larger file sizes, and add a manual override allowing users to skip compression for PDFs and high-quality scans.

medium impact medium prob dependency

The Flutter image_picker package behaves differently on iOS 17+ (PHPicker) vs older Android (Intent-based), particularly for file types, permission flows, and PDF selection, which may cause platform-specific failures not caught in development.

Mitigation & Contingency

Mitigation: Test image picker integration on physical devices for both platforms early in the sprint. Pin the image_picker package version and review changelogs before updates. Write widget tests using mock file results for each platform branch.

Contingency: If PHPicker or Android Intent differences cause blocking issues, implement separate platform-specific picker delegates behind the unified interface, allowing platform-specific fixes without breaking the shared API.