critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

PostSessionReport Dart model has fields: reportId (String), activityId (String), createdBy (String), orgId (String), status (ReportStatus enum: draft, submitted), fieldValues (Map<String, dynamic>), createdAt (DateTime), updatedAt (DateTime); includes fromJson and toJson
ReportStatus is a Dart enum with values draft and submitted with fromString factory
PostSessionReportRepository accepts SupabaseClient via constructor injection
createReport(String activityId, String orgId, Map<String, dynamic> initialFieldValues) → Future<PostSessionReport> inserts a new draft report linked to the given activityId and returns the created report
getReportById(String reportId) → Future<PostSessionReport?> returns the report or null if not found (uses .maybeSingle())
getReportsByActivityId(String activityId) → Future<List<PostSessionReport>> returns all reports for the activity ordered by created_at desc
getReportsByPeerMentor(String userId) → Future<List<PostSessionReport>> returns all reports where created_by = userId ordered by created_at desc
updateReport(String reportId, Map<String, dynamic> fieldsToUpdate) → Future<PostSessionReport> performs a partial update — only the keys present in fieldsToUpdate are sent in the Supabase update payload; returns the updated report
deleteReport(String reportId) → Future<void> deletes the report; throws ReportNotDeletableException if status is 'submitted'
submitReport(String reportId) → Future<PostSessionReport> sets status to 'submitted' and sets submitted_at timestamp; throws ReportAlreadySubmittedException if already submitted
All methods throw a typed PostSessionReportRepositoryException hierarchy (ReportNotFoundException, ReportPermissionException, ReportNotDeletableException, ReportAlreadySubmittedException, ReportRepositoryException as base) for all error scenarios
fieldValues Map is serialised as JSONB — null values in the map are preserved (not stripped) to allow draft saving with explicitly cleared fields
No raw Supabase or PostgrestException types are exposed to callers

Technical Requirements

frameworks
Flutter
Supabase (supabase_flutter)
apis
Supabase PostgREST REST API — post_session_reports table
data models
PostSessionReport
ReportStatus
post_session_reports (Supabase table)
FieldConfigSchema (for field validation context)
performance requirements
getReportsByActivityId and getReportsByPeerMentor must use server-side .eq() filters — no client-side filtering on full table results
updateReport must send only changed fields in the PATCH payload — never re-send the full fieldValues JSONB if only a status field changes
Pagination support (optional in v1 but method signatures should accept optional limit/offset parameters for future extensibility)
security requirements
Repository trusts Supabase RLS for access control — do not re-implement permission checks in Dart
submitReport must be an atomic operation — use a Supabase RPC function or conditional update (.update().eq('report_id', id).eq('status', 'draft')) to prevent double-submission race conditions
deleteReport application-level guard (check status before delete) is a UX safeguard only; the authoritative guard must be a database CHECK or trigger in the migration from task-001
fieldValues must not be logged at any level — may contain sensitive health information per the Blindeforbundet requirements

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

For submitReport atomicity, use a conditional Supabase update: .update({'status': 'submitted', 'submitted_at': DateTime.now().toIso8601String()}).eq('report_id', reportId).eq('status', 'draft').select().maybeSingle() — if the result is null, the report was already submitted (or does not exist), throw the appropriate typed exception. The fieldValues JSONB field should use Supabase's built-in JSONB handling — pass the Dart Map directly in the update payload and let the Supabase Dart client serialise it; do not manually call jsonEncode on the map. For partial updates in updateReport, build the update Map from only the provided keys — never merge with existing server data client-side. Align the exception hierarchy with other repositories in the codebase to avoid proliferating inconsistent exception types.

The submitted_at column must exist in the post_session_reports table from task-001 — verify and request a schema addition if absent.

Testing Requirements

Write unit tests using flutter_test with mocked SupabaseClient (mocktail). Cover: (1) createReport returns mapped PostSessionReport with draft status, (2) getReportById returns report on success and null when not found, (3) getReportsByActivityId returns correctly filtered and ordered list, (4) getReportsByPeerMentor returns correctly filtered list, (5) updateReport sends only specified fields in payload (verify mock call arguments), (6) updateReport returns updated model, (7) deleteReport succeeds for draft report, (8) deleteReport throws ReportNotDeletableException for submitted report, (9) submitReport sets status to submitted and throws ReportAlreadySubmittedException on second call, (10) each method throws PostSessionReportRepositoryException on PostgrestException, (11) fromJson/toJson round-trip for PostSessionReport including null fieldValues entries. Write a separate integration test against local Supabase for submitReport atomicity.

Component
Post-Session Report Repository
data medium
Epic Risks (3)
high impact medium prob security

Supabase RLS policies for multi-org report access may be more complex than anticipated — coordinators need cross-peer-mentor access within their org but not across orgs, and draft reports should be invisible to coordinators until submitted. Misconfigured RLS could expose sensitive health data or block legitimate access.

Mitigation & Contingency

Mitigation: Define and test RLS policies in isolation before writing repository code. Create a dedicated SQL migration file with policy definitions and an automated integration test suite that verifies each role's access boundaries using real Supabase auth tokens.

Contingency: If RLS proves too complex to express declaratively, implement application-level access control in the repository layer with explicit org and role checks, and add a security audit task before the feature goes to production.

high impact medium prob integration

The org field config JSON stored in Supabase may lack a stable, versioned schema contract. If different organisations have drifted to different field-definition formats, org-field-config-loader will fail silently or crash, breaking form rendering for those orgs.

Mitigation & Contingency

Mitigation: Define a canonical JSON Schema for field config and validate all existing org configs against it before implementation begins. Store a schema version field in every config record and handle version migrations explicitly in the loader.

Contingency: If existing configs are too heterogeneous, implement a config normalisation pass in org-field-config-loader that coerces known variants to the canonical format, logging warnings for fields that cannot be normalised so operations can fix them in the admin console.

medium impact low prob technical

TTL-based schema cache invalidation may cause peer mentors to use stale field definitions for up to the TTL window after an admin updates the org config, potentially collecting data against outdated field structures.

Mitigation & Contingency

Mitigation: Set a conservative TTL (e.g. 15 minutes) and expose a manual cache-bust mechanism triggered on app foreground-resume. Document the maximum staleness window in the admin console so org admins know to plan config changes outside active reporting windows.

Contingency: If stale schema causes a data quality incident, add a Supabase Realtime subscription to the org config table that invalidates the cache immediately on any config update.