critical priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

ReportHistoryRecord is defined as a freezed data class in `lib/features/bufdir_report/domain/models/report_history_record.dart`
All fields match the database schema with correct nullability: id (String), organizationId (String), reportPeriodStart (DateTime), reportPeriodEnd (DateTime), submittedByUserId (String?), submittedAt (DateTime?), status (ReportStatus), filePath (String?), fileSizeBytes (int?), checksum (String?), metadata (Map<String, dynamic>), createdAt (DateTime), updatedAt (DateTime)
ReportStatus enum is defined with values: draft, submitted, acknowledged — in `lib/features/bufdir_report/domain/models/report_status.dart`
fromJson(Map<String, dynamic> json) factory constructor correctly parses all fields including DateTime from ISO 8601 strings and ReportStatus from string via enum name lookup
toJson() method produces a Map<String, dynamic> compatible with Supabase PostgREST insert/update payloads (snake_case keys matching database column names)
metadata field defaults to an empty map `{}` when absent from JSON
copyWith, ==, hashCode, and toString are auto-generated by freezed
A barrel file `lib/features/bufdir_report/domain/models/models.dart` exports ReportHistoryRecord, ReportStatus, and any related exception types
Running `dart run build_runner build` generates .freezed.dart and .g.dart files without errors
The model file has zero external dependencies beyond freezed, json_annotation, and dart core libraries

Technical Requirements

frameworks
Flutter
freezed
json_annotation
build_runner
data models
bufdir_report_history (database schema reference for field mapping)
performance requirements
fromJson must handle null values for all nullable fields without throwing
toJson must exclude null fields or include them as JSON null based on Supabase insert requirements — document the chosen approach
security requirements
Immutability enforced by freezed — no mutable state on domain models
metadata Map must be typed as Map<String, dynamic> — no dynamic casting at use sites

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Use `@freezed` annotation with `part 'report_history_record.freezed.dart'` and `part 'report_history_record.g.dart'`. Use `@JsonKey(name: 'organization_id')` annotations on all fields to map camelCase Dart names to snake_case database column names — this is critical for Supabase compatibility. For DateTime parsing, use `@JsonKey(fromJson: _dateTimeFromJson, toJson: _dateTimeToJson)` with helper functions that handle ISO 8601 strings. For ReportStatus, define a custom `@JsonKey(unknownEnumValue: ReportStatus.draft)` to handle unexpected values gracefully.

The metadata field should use `@Default({})` in the freezed factory to avoid null. Add the model to the `pubspec.yaml` build_runner watch list. Do not add UI-specific logic (display names, color mappings) to domain models — those belong in presentation layer extension methods.

Testing Requirements

Write unit tests with flutter_test: (1) fromJson parses a complete valid JSON map correctly for all fields; (2) fromJson parses a JSON map with all nullable fields set to null without throwing; (3) fromJson correctly parses ReportStatus from each valid string ('draft', 'submitted', 'acknowledged'); (4) fromJson throws or returns a safe default when status is an unrecognized string — document and test the chosen behavior; (5) toJson round-trip: `ReportHistoryRecord.fromJson(record.toJson()) == record`; (6) copyWith correctly overrides individual fields while preserving others; (7) Two records with identical field values are equal (== returns true). Run with `flutter test test/features/bufdir_report/domain/models/`.

Component
Report History Repository
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.