critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

ActivityTypeMetadataResolver is a pure Dart class (no Flutter dependency) that accepts an ActivityType in its constructor
isExpenseEligible() returns bool — true only when requiresExpense is true in metadata; never throws
requiresReceipt() returns bool — true only when requiresReceipt is true in metadata; never throws
bufdirCategory() returns String? — null when no category is set; valid category string otherwise
isHonorariumEligible() returns bool — true only when honorariumEligible is true in metadata; never throws
An extensionFlag(String key) → bool accessor is provided for org-specific flags stored in extensionFields, defaulting to false for missing/non-boolean values
All accessors are unit-tested with ActivityType fixtures covering true, false, and missing/null metadata field scenarios
No other class in the codebase accesses activityType.metadata.requiresExpense directly — all access goes through the resolver (enforced via code review and lint rule if possible)
A Riverpod Provider family is defined: activityTypeMetadataResolverProvider(ActivityType activityType) returning ActivityTypeMetadataResolver
Resolver is immutable — it does not hold mutable state and produces the same output for the same ActivityType input

Technical Requirements

frameworks
Dart (pure)
Riverpod (for provider family)
data models
ActivityType
ActivityTypeMetadata
performance requirements
All accessor methods must be O(1) — no iteration or parsing at call time (parsing is done once in ActivityTypeMetadata.fromJson)
security requirements
Resolver must not cache or expose raw metadata map — consumers receive only typed values
extensionFlag() must return false (not throw) for any unknown key to prevent information leakage via exceptions

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Place in lib/domain/activity_type/activity_type_metadata_resolver.dart. The resolver pattern is a 'facade over a value object' — it does not change state, only provides a typed API. This is the correct place to add business rules: for example, 'an activity requires receipt when requiresReceipt is true AND the expense amount exceeds 100 kr (HLF rule)' — complex conditions live here, not scattered in UI code. For the Riverpod provider family, use `final activityTypeMetadataResolverProvider = Provider.family((ref, activityType) => ActivityTypeMetadataResolver(activityType))`.

The family approach means widgets can call `ref.watch(activityTypeMetadataResolverProvider(activityType))` inline. Consider defining accessor methods as getters (bool get isExpenseEligible) rather than methods for cleaner call-site syntax — this also works with Dart's const propagation. Add a fromId factory that takes an ActivityType ID and a repository reference if resolving by ID is a common pattern, but only after task-005 is complete.

Testing Requirements

Unit tests with flutter_test covering all accessors: (1) isExpenseEligible returns true when requiresExpense=true; (2) isExpenseEligible returns false when requiresExpense=false; (3) isExpenseEligible returns false when metadata is default (all false); (4) identical tests for requiresReceipt() and isHonorariumEligible(); (5) bufdirCategory returns correct string when set; (6) bufdirCategory returns null when not set; (7) extensionFlag('known_key') returns true when present and true; (8) extensionFlag('unknown_key') returns false without throwing; (9) resolver with a fully default ActivityTypeMetadata — all boolean accessors return false, bufdirCategory returns null. Target 100% branch coverage on the resolver class itself.

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.