high priority medium complexity database pending database specialist Tier 5

Acceptance Criteria

saveMetricSnapshot persists the computed MetricSnapshot domain object to a Supabase table (e.g., bufdir_metric_snapshots) with org_id, period_start, period_end, computed_at, and snapshot_json columns
getMetricSnapshot queries the snapshot table by org_id + period key and returns the cached snapshot if found, wrapped in Right(Some(snapshot))
getMetricSnapshot returns Right(None) (not a failure) when no snapshot exists for the given org/period combination
deleteMetricSnapshot removes the snapshot record and returns Right(unit) on success; returns Left(QueryFailure) if the record does not exist
Cache-hit path: when getMetricSnapshot finds a valid snapshot, the repository does NOT call any RPC aggregation function — verified by checking zero calls to AggregationQueryBuilder in that code path
saveMetricSnapshot is idempotent: re-saving for the same org+period upserts (not duplicates) the record
Snapshot JSON stored in Supabase does not contain raw PII — only aggregated counts and statistics
RLS policy on bufdir_metric_snapshots restricts read/write to the owning organization's users only
deleteMetricSnapshot requires coordinator or admin role claim in the JWT — peer mentor role must be rejected at RLS level
All three methods follow the same Either<DomainFailure, T> return type pattern established in task-006

Technical Requirements

frameworks
Flutter
Dart
Riverpod
apis
Supabase PostgreSQL 15 (upsert, select, delete)
Supabase REST API via supabase_flutter SDK
data models
annual_summary
bufdir_export_audit_log
performance requirements
getMetricSnapshot must return within 500ms for cache-hit path (single indexed lookup)
saveMetricSnapshot must complete within 3 seconds including network round-trip
Snapshot table must have a composite index on (org_id, period_start, period_end) for fast lookups
security requirements
RLS policy on snapshot table: USING (org_id = current_setting('app.current_org_id')::uuid)
Snapshot JSON must be sanitized to remove any PII before storage — store aggregated numbers only
deleteMetricSnapshot must enforce role check: only coordinator/admin roles can delete snapshots
Upsert operation must use conflict target (org_id, period_start, period_end) to prevent race-condition duplicates

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Use Supabase's `.upsert()` with `onConflict: 'org_id,period_start,period_end'` for idempotent saves. Store the snapshot as a JSONB column so Supabase can index into it if needed later. Define a MetricSnapshot Dart class with fromJson/toJson using json_serializable. The cache-hit check in getMetricSnapshot should be a simple database lookup — do not introduce an in-memory LRU cache at this stage unless the codebase already has one, as that adds complexity without clear benefit given Supabase's speed for indexed single-row lookups.

For the migration, add the bufdir_metric_snapshots table creation alongside the RLS policies in the same migration file as other Bufdir tables to keep schema changes atomic.

Testing Requirements

Unit tests (flutter_test + mockito): (1) saveMetricSnapshot calls Supabase upsert with correct columns and conflict target, (2) getMetricSnapshot returns Right(Some(snapshot)) on cache hit, (3) getMetricSnapshot returns Right(None) on cache miss (empty result), (4) deleteMetricSnapshot returns Right(unit) on success and Left(QueryFailure) on missing record, (5) cache-hit path invokes zero calls to AggregationQueryBuilder mock, (6) PostgrestException from save/delete maps to Left(QueryFailure). Integration test against Supabase staging: verify upsert idempotency, RLS blocks cross-org reads, and snapshot survives repository re-instantiation.

Component
Bufdir Metrics Repository
data medium
Epic Risks (2)
high impact medium prob technical

Supabase RPC functions return JSON with PostgreSQL numeric types (bigint, numeric) that do not map cleanly to Dart int/double. Silent truncation or JSON parsing errors could corrupt participant counts in the final Bufdir submission without any runtime exception.

Mitigation & Contingency

Mitigation: Define explicit Dart fromJson factories for all RPC result models with type-safe parsing and assertion checks. Add a contract test that compares raw RPC JSON output against expected Dart model values using a known seed dataset.

Contingency: If type mismatches are found in production metrics, expose a validation endpoint in BufdirMetricsRepository that re-fetches and compares raw RPC output against the persisted snapshot, flagging any discrepancies before export proceeds.

medium impact high prob scope

Persisted metric snapshots can become stale if additional activities are registered after the snapshot is saved but before the export is finalized. Coordinators might unknowingly export data that does not reflect the latest activity registrations.

Mitigation & Contingency

Mitigation: Store a snapshot_generated_at timestamp and a record_count_at_generation field in the snapshot. When the coordinator views cached results, compare the current activity count for the period against the snapshot value and display a 'Data updated since last aggregation — re-run?' warning if counts differ.

Contingency: Add a mandatory staleness check before the export confirmation dialog can proceed: if the snapshot is more than 24 hours old or the record count has changed, require the coordinator to re-run aggregation before the export button is enabled.