critical priority medium complexity integration pending integration specialist Tier 5

Acceptance Criteria

ActivityTypeCacheProvider exposes a list of ActivityTypeWithMetadata objects (or equivalent resolved wrapper) rather than raw ActivityType records
Each ActivityTypeWithMetadata exposes all typed accessor methods from ActivityTypeMetadataResolver without requiring callers to instantiate the resolver
The registration wizard can call item.isTravelExpenseEligible, item.requiresContactSelection, and similar resolver accessors directly on cached objects
The expense eligibility check feature can determine eligibility without importing or instantiating ActivityTypeMetadataResolver
The Bufdir export module can read Bufdir-specific metadata fields from cached items without additional resolver setup
Resolver is instantiated once per activity type during the cache population phase, not on every read
If resolver throws or returns unexpected values for a given type, that type is still included in the cache with safe default values — the cache population does not fail entirely
ActivityTypeWithMetadata is an immutable value object (freezed or equivalent) to prevent accidental mutation of cached state
The provider's type signature change (from List<ActivityType> to List<ActivityTypeWithMetadata>) does not break the existing consumer interface — all existing call sites updated

Technical Requirements

frameworks
Riverpod
Flutter
freezed (for ActivityTypeWithMetadata immutable wrapper)
apis
ActivityTypeMetadataResolver.resolve(ActivityType) -> ActivityTypeWithMetadata
ActivityTypeCacheProvider (from task-009)
Bufdir export module metadata accessors
Activity registration wizard eligibility checks
data models
activity_type (id, organization_id, name, description, is_travel_expense_eligible, metadata JSONB)
activity_type_metadata (JSONB structure with typed fields)
performance requirements
Resolver instantiation during cache population must add less than 50 ms overhead for up to 200 activity types
Accessor calls on cached ActivityTypeWithMetadata objects must be O(1) — no repeated JSONB parsing on each access
security requirements
Metadata JSONB must be sanitised during resolution — no raw JSONB strings exposed to UI layer
Resolver must not log or expose sensitive metadata fields (e.g. internal org configuration flags) to debug output

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Define ActivityTypeWithMetadata as a freezed data class with all fields from ActivityType plus the resolved metadata accessors as computed getters or pre-resolved fields. Two valid approaches: (A) eager resolution — resolve all metadata during cache.build() and store ActivityTypeWithMetadata directly; (B) lazy wrapper — store ActivityType + resolver reference and resolve on first accessor call with memoisation. Approach A is preferred for simplicity and predictability. In the cache provider's build() method, call: state = await repository.fetchActiveByOrganisation(orgId) then map each type through resolver.resolve(type).

Wrap the resolver call in a try/catch per item to implement per-item error isolation. Define the ActivityTypeWithMetadata model in a shared domain layer so all three downstream consumers (wizard, expense check, Bufdir) can import it without circular dependencies.

Testing Requirements

Unit tests using flutter_test and mocktail. Test cases: (1) ActivityTypeCacheProvider returns List after population; (2) each item in the list has resolver accessors pre-wired and returning correct values; (3) a type with malformed metadata JSONB is included in the list with defaults, not excluded; (4) resolver is called exactly once per activity type during cache population (not on each consumer read); (5) activity registration wizard obtains expense eligibility from cached item without instantiating resolver; (6) Bufdir export module reads Bufdir metadata fields from cached items correctly. Integration test: full round-trip from Supabase mock → repository → resolver → cache → consumer accessor.

Component
Activity Type Cache Provider
data medium
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.