high priority medium complexity backend pending backend specialist Tier 0

Acceptance Criteria

compressReceiptImage(XFile) returns a Uint8List whose byte length is under 500 KB (512,000 bytes) for any input image
Output MIME type is JPEG regardless of the source format (PNG, HEIC, WebP)
Compression quality defaults to 80% and is overridable via CompressionConfig injected at construction time
Maximum output dimension (longest side) defaults to 2048 px and is overridable via CompressionConfig
If the source image is already under 500 KB after dimension capping, it is returned without further quality reduction
An invalid or unreadable XFile path throws a typed CompressionFailure, not a platform exception
compressReceiptImage is declared async and does not block the main isolate
Unit tests cover: standard JPEG input, HEIC input (iOS), oversized image exceeding 2048 px, already-small image, and invalid path
Compressed output is visually legible for receipt OCR purposes (text readable in manual review)

Technical Requirements

frameworks
Flutter
flutter_image_compress
data models
CompressionConfig (quality: int, maxDimension: int, maxFileSizeBytes: int)
CompressionFailure (sealed failure: invalidFile, unsupportedFormat, compressionError)
performance requirements
Compression of a 4 MB source image must complete within 3 seconds on a mid-range device
Must run off the main isolate to avoid UI jank — use compute() or a dedicated isolate
security requirements
Compressed bytes are held in memory only — never written to a temporary file on disk unless flutter_image_compress requires it, in which case the temp file must be deleted immediately after reading
Input file path must be validated as an accessible app-sandbox path before processing

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

flutter_image_compress operates natively on iOS (CoreImage) and Android (libjpeg-turbo) — wrap it behind an abstract interface immediately so tests can stub it. HEIC is the default format on iOS — the library handles conversion but requires the heif format to be explicitly requested on some versions; check the plugin's format enum. Use a two-pass strategy: first resize to maxDimension, then compress to maxQuality. If the result still exceeds maxFileSizeBytes, reduce quality in steps of 5 down to a floor of 50 before giving up.

Document this adaptive compression in code comments. Run the compression in compute() to keep the UI at 60 fps during the operation. Return the Uint8List (not a file path) so the caller (ReceiptAttachmentService) can stream it directly to Supabase without an extra disk read.

Testing Requirements

Unit tests (flutter_test): mock flutter_image_compress via an abstract IImageCompressor interface to test happy path and failure branches without native plugin overhead. Integration test on device: capture a real HEIC photo from iOS camera, pass through service, assert output < 500 KB and is valid JPEG. Benchmark test: time compression of a 5 MB image on debug build (document baseline; do not enforce in CI). Test with null/empty XFile to verify CompressionFailure is returned.

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.