high priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

ReportSchemaService exposes: resolveSchema(String orgId, String reportType) → Future<ReportSchema>
ReportSchema contains: schemaVersion (String), reportType (String), fields (List<FieldConfig>) in render order, resolvedAt (DateTime), isFromCache (bool)
Schema resolution order: (1) ReportSchemaCache for matching orgId+reportType+version, (2) OrgFieldConfigLoader network fetch if cache miss or version mismatch, (3) base schema fallback if both fail
Org-level visibility overrides: if OrgFieldConfig marks a base field as hidden (visible=false), it is excluded from the returned fields list
Schema versioning: service compares the fetched schema version against the cached version; if versions match, network fetch is skipped
Base schema fallback: a minimal hardcoded schema (title + way_forward_section) is returned when both cache and network fail, so the form is never completely broken
Riverpod provider is family-scoped by (orgId, reportType) tuple: reportSchemaProvider((orgId, reportType))
Provider emits AsyncValue.loading() immediately, then AsyncValue.data(schema) or AsyncValue.error() after resolution
isFromCache=true when schema came from cache; isFromCache=false when fetched from network
resolveSchema() is idempotent: calling it twice with the same args in the same session returns the same schema without a second network call

Technical Requirements

frameworks
Dart
Riverpod
Supabase Flutter SDK
apis
OrgFieldConfigLoader (task-005)
ReportSchemaCache (task-002)
Supabase .from('report_schemas').select().eq('org_id', orgId).eq('report_type', reportType).single()
data models
ReportSchema
FieldConfig (from task-005)
SchemaVersion
performance requirements
Cache hit resolution must complete within 50ms
Network fetch + parse + cache write must complete within 3 seconds on 3G
Fields list must be sorted by FieldConfig.order before returning — sort is O(n log n) and acceptable for up to 100 fields
security requirements
orgId must match the authenticated user's org_id from session — prevent cross-org schema resolution by verifying against Supabase auth session before querying
Schema data is not PII but org configuration is confidential — cache must use app-sandboxed storage only
reportType values must be validated against an allowlist before being used in Supabase query to prevent injection

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Implement as AsyncNotifierProvider.family with a (String orgId, String reportType) parameter record. The notifier's build() drives the resolution pipeline. Use Dart's record type for the family key: `AsyncNotifierProvider.family()`. Extract schema resolution logic into a private _resolveFromNetwork() and _resolveFromCache() to keep build() readable.

Visibility override application: filter fields with `fields.where((f) => f.visible != false).toList()` — default is visible=true if the override is absent. Base fallback schema: define as a static const ReportSchema in the service with a sentinel version 'FALLBACK_0'. Version comparison: use simple string equality; semantic versioning is not required unless the schema evolves frequently. Cache key: combine orgId + reportType + version as a single string key.

The idempotency requirement is naturally satisfied by Riverpod's keepAlive or ref.keepAlive() call in build().

Testing Requirements

Unit tests with mocked OrgFieldConfigLoader, ReportSchemaCache, and Supabase client: (1) cache hit matching version → ReportSchema with isFromCache=true, no network call, (2) cache miss → network fetch → cache write → ReportSchema with isFromCache=false, (3) version mismatch → network fetch overrides cache, (4) org visibility override removes hidden field from returned fields list, (5) network failure + cache available → cached schema returned, (6) network failure + no cache → base fallback schema returned with isFromCache=false, (7) second call with same args returns cached result without second network call, (8) fields in returned schema are sorted by order ascending, (9) cross-org attempt (orgId ≠ auth session orgId) throws UnauthorisedSchemaAccessException. Test Riverpod provider with ProviderContainer: assert loading state emitted before data. Minimum 85% branch coverage.

Component
Report Schema Service
service medium
Epic Risks (3)
high impact medium prob technical

Dynamically rendered form fields built from runtime JSON schema are significantly harder to make accessible than statically declared widgets — Flutter's Semantics tree must be correct for every possible field type and every validation state. Failures here block the entire feature for Blindeforbundet's visually impaired peer mentors.

Mitigation & Contingency

Mitigation: Define WCAG 2.2 AA semantics requirements for each field type before implementation and write widget tests using Flutter's SemanticsController for every type. Include a real-device VoiceOver test session in the acceptance gate for this epic before marking it done.

Contingency: If dynamic semantics prove too difficult to get right generically, implement field-type-specific Semantics wrappers (one per supported field type) instead of a single generic renderer, accepting slightly more code duplication in exchange for reliable accessibility.

high impact medium prob technical

The report-form-orchestrator must manage a complex state machine — schema loading, draft persistence, per-field validation, submission retries, and error recovery — across multiple async operations. Incorrect state transitions could result in lost user data, double submissions, or UI freezes.

Mitigation & Contingency

Mitigation: Define all Bloc states and events explicitly as sealed classes before writing any logic. Use a state machine diagram reviewed by the team before implementation. Write exhaustive Bloc unit tests covering every state transition, including concurrent events and network interruption mid-submission.

Contingency: If Bloc complexity becomes unmanageable, extract draft persistence into a separate DraftManagerCubit and keep report-form-orchestrator focused solely on the submit workflow. The additional granularity makes each component independently testable.

medium impact low prob scope

Organisations may require field types beyond the five currently specified (text, multiline, checkbox group, radio, date). If a new type is discovered during pilot testing, the dynamic-field-renderer must be extended, potentially requiring changes across multiple layers.

Mitigation & Contingency

Mitigation: Design dynamic-field-renderer as a registry of field-type renderers with a clear extension point. Document the pattern for adding a new field type so that it can be done in one file without touching existing renderers.

Contingency: If an unhandled field type is encountered at runtime, dynamic-field-renderer renders a labelled plain-text fallback widget and logs a warning so the missing type is surfaced in monitoring, preventing a crash while making the gap visible.