Implement ReportFileStorageClient for upload and download
epic-bufdir-report-history-foundation-task-006 — Create the ReportFileStorageClient class wrapping Supabase Storage operations. Implement uploadReportFile(organizationId, localFilePath, reportId) returning the remote file path, downloadReportFile(filePath) returning a signed URL valid for 60 minutes, deleteReportFile(filePath), and getSignedUrl(filePath, expiresIn). Enforce organization-scoped file paths (e.g., {orgId}/{reportId}/{filename}). Handle upload progress stream for UI feedback.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Use `supabase.storage.from('bufdir-reports').uploadBinary(path, bytes, onUploadProgress: (sent, total) => ...)` for progress tracking. Expose progress via a StreamController
Path construction helper: `String _buildPath(String orgId, String reportId, String filename) => '$orgId/$reportId/$filename'` — make this private and call it from every method to prevent path injection. MIME type mapping: use the `mime` package to detect MIME from file extension, pass as `fileOptions: FileOptions(contentType: mimeType)`. For the abstract interface, define `abstract class IReportFileStorageClient` with the same method signatures. The Riverpod provider should be `final reportFileStorageClientProvider = Provider
Testing Requirements
Write unit tests using flutter_test with a mocked Supabase Storage client: (1) uploadReportFile constructs the correct storage path `{orgId}/{reportId}/{filename}` and calls upload with the correct bucket name; (2) uploadProgress stream emits increasing values and completes at 1.0 after upload; (3) downloadReportFile calls createSignedUrl with 3600 seconds and returns the URL string; (4) getSignedUrl with custom expiresIn=1800 passes 1800 to createSignedUrl; (5) deleteReportFile calls remove with the correct path; (6) StorageException is mapped to the correct domain exception type; (7) checksum is computed correctly for a known test file (compare against expected SHA-256 value). Write one integration test against local Supabase: upload a small PDF, retrieve signed URL, verify URL is accessible within expiry. Run unit tests with `flutter test test/features/bufdir_report/data/clients/`.
Incorrectly authored RLS policies could silently allow cross-organization data reads, exposing sensitive report history of one organization to coordinators of another in a multi-tenant environment.
Mitigation & Contingency
Mitigation: Write integration tests that explicitly authenticate as a user from organization A and assert zero rows are returned for organization B's history records. Use Supabase's built-in RLS testing utilities and review policies with a second developer.
Contingency: If a cross-tenant leak is discovered post-deployment, immediately revoke all active sessions for affected organizations, audit query logs for unauthorized access, and patch the RLS policy in a hotfix migration before re-enabling access.
The 5-year retention policy for report files may conflict with Supabase Storage's lack of native lifecycle rules, requiring a custom pg_cron job that could fail silently and either delete files prematurely or never clean up.
Mitigation & Contingency
Mitigation: Implement the retention cleanup as a documented pg_cron job with explicit logging to a separate audit_jobs table. Add a Supabase Edge Function health check that verifies the cron job ran within the last 25 hours.
Contingency: If the cron job fails, files accumulate in storage (non-critical for compliance — over-retention is safer than under-retention). Alert the ops team via monitoring and manually trigger the cleanup function once the cron issue is resolved.
Signed URL generation depends on the requesting user's Supabase session being valid at the time of the call. If sessions expire during a long screen interaction, URL generation will fail with an authorization error and confuse the coordinator.
Mitigation & Contingency
Mitigation: Wrap signed URL generation in the service layer with a session-refresh check before calling Supabase Storage. Generate URLs on demand (tap-to-download) rather than pre-generating them for all list items on screen load.
Contingency: If a URL generation fails due to session expiry, surface a clear error message prompting the coordinator to re-authenticate, then automatically retry URL generation after session refresh completes.