critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

ActivityTypeMetadata is an immutable Dart class (all fields final) with documented field meanings in dartdoc comments
All known metadata fields are typed: requiresExpense (bool), requiresReceipt (bool), bufdirCategory (String?), honorariumEligible (bool), and an extensionFields (Map<String, dynamic>) catch-all for org-specific flags not yet formalised
fromJson handles missing/null fields gracefully using default values (false for booleans, null for nullable strings) — no field is required in the JSON
toJson serialises all fields including non-null extensionFields entries, round-tripping correctly
A written schema contract document (metadata_schema.md or inline dartdoc on the class) lists every field, its type, default value, which organisations use it, and its business meaning
The schema is versioned: a schemaVersion field (int, default 1) in the metadata JSONB allows future migrations without breaking existing records
fromJson is tolerant of unknown keys — extra fields from future schema versions are silently stored in extensionFields rather than throwing
All four known flag fields (requiresExpense, requiresReceipt, honorariumEligible) default to false when absent — never null — so callers do not need null checks for boolean flags
ActivityType.fromJson correctly delegates metadata parsing to ActivityTypeMetadata.fromJson()

Technical Requirements

frameworks
Dart (pure)
apis
Supabase JSONB column — metadata on activity_types table
data models
ActivityTypeMetadata
ActivityType
performance requirements
fromJson must not allocate unnecessary intermediate maps — parse directly from the provided Map<String, dynamic>
security requirements
extensionFields must not be serialised to logs or error messages — may contain org-specific configuration details
No PII should be stored in metadata — document this constraint in the schema contract

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Place in lib/domain/activity_type/activity_type_metadata.dart. The key design decision is how to handle unknown org-specific flags: a Map extensionFields property captures them without losing data. This is preferable to a strict schema that throws on unknown keys, as different organisations (NHF, HLF, Blindeforbundet) may add their own flags over time. Use explicit key constants as static const String fields on the class (e.g.

static const String kRequiresExpense = 'requires_expense') to avoid magic string bugs when other code constructs metadata maps. For the schema version, store it as metadata['schema_version'] in the JSONB — increment when adding required new fields. Document the bufdirCategory values as an enum or set of constants: categories come from Bufdir's reporting specification and must match exactly for automated export (see section 1.4 of the requirements). The catch-all extensionFields approach means ActivityTypeMetadataResolver (task-007) can still access org-specific flags via resolver.extensionFlag('hlf_requires_sign_off') without requiring schema changes.

Testing Requirements

Unit tests with flutter_test: (1) fromJson with all known fields present — assert each field deserialises correctly; (2) fromJson with empty JSON object — assert all boolean flags default to false and nullable strings to null; (3) fromJson with unknown extra fields — assert they are stored in extensionFields and no exception is thrown; (4) toJson round-trip — serialise and re-parse, assert equality; (5) fromJson with null metadata column value (null passed from database) — assert a valid default ActivityTypeMetadata is returned (all booleans false); (6) schemaVersion field — assert it defaults to 1 when absent. Property-based testing with dart_test random generators is a bonus but not required.

Component
Activity Type Metadata Resolver
service low
Epic Risks (3)
high impact medium prob technical

The JSONB metadata column has no enforced schema at the database level. If the Dart model and the stored JSON diverge (e.g., a field is renamed or a new required flag is added without a migration), the metadata resolver will silently return null or throw at parse time, breaking conditional wizard logic for all organisations.

Mitigation & Contingency

Mitigation: Define a versioned Dart Freezed model for ActivityTypeMetadata and add a Supabase check constraint or trigger that validates the JSONB structure on write. Document the canonical metadata schema in a shared constants file and require schema review for any metadata field additions.

Contingency: Implement a lenient parse path in ActivityTypeMetadataResolver that returns safe defaults for missing fields and logs a structured warning to Supabase edge logs, allowing the app to degrade gracefully rather than crash.

high impact low prob security

If RLS policies on the activity_types table are misconfigured, a coordinator from one organisation could read or mutate activity types belonging to another organisation, violating data isolation guarantees required by all three client organisations.

Mitigation & Contingency

Mitigation: Write integration tests against the Supabase local emulator that explicitly assert cross-org isolation: a token scoped to org A must receive zero rows when querying org B activity types, and upsert attempts must return permission-denied errors.

Contingency: Apply an emergency RLS policy patch via Supabase dashboard without a code deploy. Audit all activity_type rows for cross-org contamination and restore from backup if any data leakage is confirmed.

medium impact medium prob integration

If the cache invalidation call in ActivityTypeService is not reliably triggered after an admin creates, edits, or archives an activity type, peer mentors on the same device will see stale data in the registration wizard until the next app restart, leading to confusion and potential misregistrations.

Mitigation & Contingency

Mitigation: Enforce a strict pattern: ActivityTypeService always calls cacheProvider.invalidate() inside the same try block as the successful Supabase mutation, before returning to the caller. Write a widget test that verifies the cache notifier emits an updated list after a service mutation.

Contingency: Add a background Supabase Realtime subscription on the activity_types table that triggers cache invalidation automatically, providing an independent safety net independent of the service call path.