high priority low complexity infrastructure pending backend specialist Tier 0

Acceptance Criteria

ContactFormValidator is a pure Dart class (no Flutter widget dependency) so it can be tested without a widget tree
ValidationResult is a sealed class with two subtypes: ValidationSuccess and ValidationFailure; ValidationFailure contains a Map<String, String> of fieldName → localised error message
validateRequired(ContactFormData data) returns ValidationFailure when the name field is blank or whitespace-only, with error key 'name'
validateRequired returns ValidationFailure when all contact method fields (email, phone, other) are simultaneously empty, with error key 'contactMethod'
validateRequired returns ValidationSuccess when name is present and at least one contact method is non-empty
Error messages are returned as localisation keys (e.g., 'validation.name.required') not hardcoded strings, so the UI layer can pass them through Flutter's localisation system
ContactFormValidator exposes a Flutter-compatible FormFieldValidator<String> factory method for each required field so it can be passed directly to TextFormField(validator:) without adaptation code
Validator is stateless and has no dependency on Supabase or Riverpod — it is a plain Dart utility
All validation logic is covered by unit tests with zero reliance on Flutter test infrastructure

Technical Requirements

frameworks
Flutter
Dart
data models
ContactFormData
ValidationResult
ValidationSuccess
ValidationFailure
performance requirements
All validation methods must execute synchronously — no async validation in this component
Validation of a complete ContactFormData object must complete in under 1ms
security requirements
Validator must trim and normalise whitespace before presence checks to prevent blank-space bypass
Error messages must not echo back user input to avoid reflected XSS if error strings are ever rendered as HTML (use localisation keys only)
ui components
TextFormField (validator integration via FormFieldValidator<String> factory)
Form widget (wraps fields; validator is called on Form.validate())

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Model ContactFormData as a simple immutable Dart class with named fields. Use a sealed class for ValidationResult with Dart 3 pattern matching so callers are forced to handle both cases at compile time. Implement the Flutter FormFieldValidator integration as a static method: `static FormFieldValidator nameValidator() => (value) { final result = validateRequired(...); return result is ValidationFailure ? result.errors['name'] : null; }`.

Keep the core validateRequired method free of Flutter imports so the business logic stays independently testable. Localisation key naming convention: 'validation.{fieldName}.{ruleCode}' (e.g., 'validation.name.required', 'validation.contactMethod.required').

Testing Requirements

Pure Dart unit tests (flutter_test package, no widget test needed): (1) blank name fails with key 'name'; (2) whitespace-only name fails with key 'name'; (3) valid name passes; (4) all contact methods empty fails with key 'contactMethod'; (5) at least one contact method non-empty passes; (6) both name and contact method missing returns both errors in ValidationFailure.errors; (7) FormFieldValidator factory returns a non-null String for invalid input and null for valid input (Flutter convention). Aim for 100% branch coverage on the validator logic.

Component
Contact Form Validator
infrastructure low
Epic Risks (3)
high impact medium prob security

Blindeforbundet's encryption key retrieval mechanism may not be finalised at implementation time, or session key availability via Supabase RLS may be inconsistent, causing decryption failures that expose masked placeholders to users and degrade the experience.

Mitigation & Contingency

Mitigation: Agree with Blindeforbundet on key storage and retrieval contract before implementation starts. Prototype key retrieval in a spike against the staging Supabase instance and validate the full decrypt/verify cycle with real test data before committing to the implementation.

Contingency: Implement a fallback that shows a 'field temporarily unavailable' state with a retry affordance. Log decryption failures server-side for audit. Escalate to Blindeforbundet stakeholders to unblock key management before the service tier epic begins.

medium impact medium prob technical

NHF contacts may belong to up to 5 chapters, each governed by separate RLS policies. A coordinator's chapter scope may not cover all affiliations, causing partial profile reads or silent data omissions that are difficult to detect in tests.

Mitigation & Contingency

Mitigation: Map all RLS policy combinations for multi-chapter contacts early. Write integration tests that create contacts with 5 affiliations and query them from coordinators with varying chapter scopes. Use Supabase's RLS test utilities to verify row visibility per role.

Contingency: Add an explicit 'affiliation partially visible' state in the repository response model so the UI can communicate scope limitations to the coordinator rather than silently showing incomplete data.

low impact medium prob scope

Organisation-specific validation rules (e.g., NHF chapter limit, Blindeforbundet encrypted field edit flow) may expand in scope during implementation as edge cases are discovered, causing the validator to grow beyond the planned complexity.

Mitigation & Contingency

Mitigation: Define the complete validation rule set with product and org stakeholders before coding begins. Document each rule with its source organisation and acceptance test. Use a rule registry pattern so new rules can be added without modifying core validator logic.

Contingency: Timebox validator enhancements to 2 hours per additional rule. Defer non-blocking rules to a follow-on maintenance task rather than blocking the epic delivery.