critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

pickAndUploadReceipt(String orgId, String claimId) → Future<ReceiptUploadResult> opens image_picker sheet, compresses result, uploads, and returns the storage path and initial signed URL
Compression target: output file ≤ 500 KB; quality starts at 85, reduces in steps of 10 until target is met or minimum quality of 40 is reached
Exif metadata is stripped from images before upload (flutter_image_compress handles this by default on JPEG output)
Upload progress is reported via a Stream<UploadProgress> so UI can display a progress indicator
getSignedUrl(String storagePath) → Future<String> returns a fresh signed URL, using a cache entry if the cached URL expires in more than 10 minutes
Signed URL cache uses an in-memory map keyed by storage path with expiry timestamp — cache is not persisted to disk
downloadReceiptThumbnail(String signedUrl) → Future<Uint8List> fetches and caches the thumbnail bytes in a bounded LRU cache (max 50 entries, ~25 MB total)
deleteReceipt(String storagePath) → Future<void> removes the file from Supabase Storage and invalidates the cache entry
If image_picker returns null (user cancelled), the method returns null without throwing
Network errors during upload are thrown as typed ReceiptUploadException with ReceiptUploadFailureReason enum (network, size_limit, mime_type_rejected, permission_denied)
The adapter is mockable via an abstract interface IReceiptStorageAdapter for use in BLoC tests
On iOS, the adapter requests photo library permission before opening picker and handles the PermissionDeniedException gracefully with a user-readable message

Technical Requirements

frameworks
Flutter
Riverpod
supabase_flutter
image_picker
flutter_image_compress
cached_network_image (optional for thumbnail cache)
apis
Supabase Storage (upload, signed URL generation, delete)
Supabase Auth (automatic JWT injection)
data models
claim_event
performance requirements
Compression of a 3 MB photo must complete in under 2 seconds on a mid-range device
Upload of a 500 KB compressed image must show first progress update within 500ms on 4G
Signed URL generation (cached) must return within 10ms; uncached within 300ms
security requirements
Exif metadata stripped before upload — images from peer mentor home visits may contain location data
Compressed image stored only in memory (Uint8List) before upload — never written to device filesystem as a temp file beyond what flutter_image_compress requires internally
Signed URLs never logged to console or crash reporting tools
Picker restricted to images only (ImageSource.gallery + camera) — no document picker that could expose arbitrary files
Receipt images treated as sensitive PII — stored in private Supabase bucket (from task-005)
ui components
ReceiptUploadProgressIndicator widget (linear progress bar)
ReceiptThumbnailWidget (displays cached thumbnail with loading/error states)

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use flutter_image_compress's compressWithList method on the raw bytes from image_picker to avoid writing temp files. The compression loop should be: compress at quality Q → check output size → if still >500 KB and Q > 40, reduce Q by 10 and retry. Cap at 3 iterations to avoid UI stalls. For the signed URL cache, use a simple Map — no need for a full cache library.

Invalidate cache entries where expiry < DateTime.now().add(Duration(minutes: 10)) to avoid serving near-expiry URLs. For upload progress, use Supabase Storage's onUploadProgress callback (available in supabase_flutter). Be aware that on Android, image_picker may return a Uri that requires FileProvider context — handle this with the `path_provider` package if needed for temp file resolution. Define storage path as: `$orgId/$claimId/${DateTime.now().millisecondsSinceEpoch}_receipt.jpg` to ensure uniqueness within a claim.

Testing Requirements

Unit tests (flutter_test + mocktail): mock Supabase storage client and image_picker, test compression logic with oversized mock image bytes (verify output ≤500 KB), test cache hit/miss behaviour for signed URLs, test cancellation (null picker result), test each ReceiptUploadFailureReason mapping. Widget tests: ReceiptThumbnailWidget renders loading spinner while future is pending, renders image on success, renders error icon on failure. Integration test: upload a real JPEG to local Supabase instance, verify it is retrievable via signed URL, verify the file size in storage is ≤500 KB. Test on both iOS simulator and Android emulator for permission handling differences.

Component
Receipt Storage Adapter
data medium
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.