medium priority low complexity backend pending backend specialist Tier 2

Acceptance Criteria

ReportFieldValidator exposes a single synchronous method: ValidationResult validate(FieldConfig config, dynamic value)
ValidationResult is a sealed class: Valid() and Invalid(List<FieldValidationError>); FieldValidationError carries errorCode (String), messageNb (String), messageEn (String)
required rule: null, empty string, and empty list all fail with errorCode 'required'
minLength / maxLength: applied only to String values; non-string values with these rules return Invalid(errorCode: 'type_mismatch')
pattern: compiled once per FieldConfig instance (not per call); invalid regex in schema logs a warning and skips the rule rather than crashing
min / max: applied to num values; string representations of numbers are NOT auto-coerced — caller must parse before validating
Norwegian messages use plain bokmål (no Nynorsk), English messages use plain British English; both messages are present in every FieldValidationError
Validator is a plain Dart class (no Flutter dependency, no Riverpod, no Supabase) — instantiated directly: const ReportFieldValidator()
validateAll(Map<String, dynamic> values, List<FieldConfig> configs) convenience method returns Map<String, ValidationResult> keyed by fieldId
All error messages are defined in a single ValidationMessages constant class for easy translation maintenance

Technical Requirements

frameworks
Dart (pure, no Flutter dependency)
data models
FieldConfig
ValidationResult (sealed)
FieldValidationError
performance requirements
validate() must run in under 1ms for any single field on any device
validateAll() for a 30-field form must complete under 10ms
security requirements
Pattern validation must guard against ReDoS: reject regex patterns that take longer than 50ms to compile or match (use a timeout wrapper if Dart supports it, otherwise document the risk and validate patterns server-side before storing in Supabase)

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Keep the validator stateless: `class ReportFieldValidator { const ReportFieldValidator(); }`. Use Dart's switch expression for clean rule dispatch. Pre-compile RegExp in a private cache: `static final _regexCache = {}`. For the validateAll convenience method, iterate configs and short-circuit after collecting all errors per field (not all fields).

ValidationMessages class should be structured as static const strings grouped by errorCode: `class ValidationMessages { static const requiredNb = 'Dette feltet er obligatorisk'; static const requiredEn = 'This field is required'; ... }`. Do not use the Flutter localisation (intl) package here — the validator must remain a pure Dart library with zero Flutter dependencies. Error codes should be machine-readable snake_case strings (required, min_length, max_length, pattern, min_value, max_value, type_mismatch) for programmatic handling in the form BLoC.

Testing Requirements

Unit tests (flutter_test, no mocks needed): parametrized test table covering every rule × edge case combination. Required cases: (1) required=true, value=null → Invalid, (2) required=true, value='' → Invalid, (3) required=false, value=null → Valid, (4) minLength=3, value='ab' → Invalid, (5) minLength=3, value='abc' → Valid, (6) maxLength=5, value='toolong' → Invalid, (7) pattern='^\d+$', value='123' → Valid, (8) pattern='^\d+$', value='abc' → Invalid, (9) min=0, value=-1 → Invalid, (10) max=100, value=101 → Invalid, (11) multiple rules failing → single Invalid with multiple FieldValidationErrors, (12) validateAll with mixed valid/invalid fields → correct map. Assert both messageNb and messageEn are non-empty strings in every Invalid result. Minimum 100% branch coverage is achievable and expected for this pure logic class.

Component
Report Field Validator
service low
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.