critical priority low complexity backend pending backend specialist Tier 3

Acceptance Criteria

ReportFileStorageClient is implemented in `lib/features/bufdir_report/data/clients/report_file_storage_client.dart`
uploadReportFile(String organizationId, String localFilePath, String reportId) uploads the file to `bufdir-reports/{organizationId}/{reportId}/{filename}` and returns the remote path string
uploadReportFile exposes a Stream<double> uploadProgress that emits values 0.0–1.0 as the upload progresses, allowing the UI to display a progress indicator
downloadReportFile(String filePath) returns a signed URL (String) valid for 3600 seconds (60 minutes)
getSignedUrl(String filePath, {int expiresIn = 3600}) returns a signed URL with a configurable expiry
deleteReportFile(String filePath) deletes the file from storage and returns void; throws StorageFileNotFoundException if the file does not exist
File path construction always follows the pattern `{organizationId}/{reportId}/{filename}` — no caller can construct arbitrary paths
An abstract interface IReportFileStorageClient is defined to enable mocking in tests
A Riverpod Provider (reportFileStorageClientProvider) is registered and provides a singleton
Supabase StorageException errors are caught and mapped to domain exceptions: StorageUploadException, StorageDownloadException, StorageDeleteException, StorageFileNotFoundException
The client computes and returns a SHA-256 checksum of the local file before upload for storage in bufdir_report_history.checksum

Technical Requirements

frameworks
Flutter
Riverpod
Supabase Flutter client (supabase_flutter)
crypto (for SHA-256 checksum)
apis
Supabase Storage API
bufdir-reports bucket
data models
bufdir_report_history (file_path, checksum, file_size_bytes fields populated after upload)
performance requirements
Upload progress must be streamed in real time — use Supabase Storage uploadBinary with onUploadProgress callback
Signed URL generation must complete within 500 ms
SHA-256 checksum computation must be performed in an isolate for files > 5 MB to avoid blocking the main thread
security requirements
File paths must always be prefixed with organizationId — validate that the caller-supplied organizationId matches the authenticated user's JWT claim
Signed URLs must never be logged or stored in plaintext beyond the immediate use case
Local file must be read and checksummed before upload; reject upload if local file is unreadable
MIME type must be inferred from the file extension and included in the Content-Type header to enforce server-side MIME allowlist
ui components
Upload progress indicator widget (consumer of uploadProgress stream)
File size display widget (uses fileSizeBytes returned from upload)

Execution Context

Execution Tier
Tier 3

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 that the calling BLoC or Riverpod notifier can listen to. Compute SHA-256 checksum using the `crypto` package: `sha256.convert(fileBytes).toString()`. For files > 5 MB, use `compute()` to run the checksum in an isolate.

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((ref) => ReportFileStorageClient(storage: Supabase.instance.client.storage))`.

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/`.

Component
Report File Storage Client
data low
Epic Risks (3)
high impact medium prob security

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.

medium impact medium prob technical

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.

medium impact low prob integration

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.