critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

Abstract interface IReceiptStorageRepository is defined with methods: upload(Uint8List bytes, String storagePath, String mimeType), getSignedUrl(String storagePath, {Duration expiry}), delete(String storagePath)
upload() returns the canonical storagePath string on success so callers can persist it
upload() throws ReceiptStorageException with a StorageErrorType enum value (networkError, quotaExceeded, unauthorized, unknown) on failure
getSignedUrl() returns a signed URL string valid for the requested expiry window (default 300 seconds / 5 minutes)
getSignedUrl() throws ReceiptStorageException with type unauthorized if the path does not belong to the current user's org
delete() returns true on successful deletion; throws ReceiptStorageException on network or permission error
ReceiptStorageException carries a human-readable message safe to display in the UI and a StorageErrorType for programmatic handling
The Supabase client is injected via a Riverpod Provider<SupabaseClient> — no static/global client references inside the repository
A Riverpod Provider<IReceiptStorageRepository> is exported
All methods are async and do not block the UI thread
Repository does not generate filenames or paths — it accepts pre-built paths from the caller following the receipts/{org_id}/{user_id}/{claim_id}/{filename} convention

Technical Requirements

frameworks
Flutter
Riverpod
supabase_flutter
apis
Supabase Storage API (upload, createSignedUrl, remove)
data models
ClaimReceiptAttachment
performance requirements
Upload progress callback must be supported (onUploadProgress) so UI can show a progress bar for files > 500 KB
Signed URL generation must complete in under 2 seconds on a normal mobile connection
security requirements
Never log signed URLs — they grant temporary unauthenticated read access
Default signed URL expiry must not exceed 3600 seconds (1 hour) in production
The repository must not cache signed URLs — always generate fresh ones to avoid serving expired URLs

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

The supabase_flutter SDK's storage client exposes supabase.storage.from('receipts').uploadBinary(path, bytes, fileOptions: FileOptions(contentType: mimeType, upsert: false)). Set upsert: false to prevent silent overwrites — callers should always use unique filenames (UUID-based). For progress reporting, use the onUploadProgress callback in FileOptions. Map StorageException.statusCode to StorageErrorType in a private _mapException() helper to keep the catch block clean.

For getSignedUrl, use supabase.storage.from('receipts').createSignedUrl(path, expiresIn) where expiresIn is in seconds. Wrap all three SDK calls in try/catch StorageException and re-throw as ReceiptStorageException. Do not catch generic Exception — let unexpected errors propagate so they surface in crash reporting.

Testing Requirements

Unit tests using a mock IReceiptStorageRepository (via mockito or mocktail) to test callers. For the repository implementation itself, write integration tests (covered in task-015) against a real Supabase test bucket. At minimum, add one unit test per public method that verifies the typed exception is thrown when the underlying Supabase SDK throws a StorageException — use a mock SupabaseClient for this. Assert that ReceiptStorageException.type is correctly mapped from Supabase's HTTP status codes: 403 → unauthorized, 413 → quotaExceeded, network timeout → networkError.

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