high priority low complexity integration pending integration specialist Tier 4

Acceptance Criteria

ActivityTypeService.getActiveTypes(orgId) returns ActivityType objects where a resolvedLabel field (String) is populated with the org-specific terminology
If OrganizationLabelsProvider has an entry for the labelKey, resolvedLabel == the mapped string from the org's terminology map
If OrganizationLabelsProvider does NOT have an entry for the labelKey (missing key), resolvedLabel falls back to the raw labelKey value — no exception is thrown
OrganizationLabelsProvider is injected into ActivityTypeService via Riverpod (not instantiated directly inside the service)
Label resolution does not make additional network calls per activity type — the terminology map is loaded once per org session
ActivityType domain model is updated to include a resolvedLabel field (nullable String, populated at service layer, not persisted to DB)
toJson() on ActivityType excludes resolvedLabel (it is a derived, not persisted field)
fromJson() does not expect resolvedLabel in the DB response — it is set only by the service
Unit test: getActiveTypes with a known labelKey returns ActivityType with correct resolvedLabel from mock terminology map
Unit test: getActiveTypes with an unknown labelKey returns ActivityType with resolvedLabel == labelKey (fallback behavior)
Unit test: OrganizationLabelsProvider is called exactly once per getActiveTypes call regardless of number of types (map resolved once, then applied)

Technical Requirements

frameworks
Flutter
Riverpod
Dart
data models
activity_type
performance requirements
Label resolution must be O(n) where n is number of activity types — single map lookup per type
Terminology map must be pre-loaded (not fetched per-type) — if not yet loaded, trigger load and await once
security requirements
Terminology map values must be treated as display strings only — never evaluated as HTML or code
org-specific labels must not leak between org sessions — validate orgId matches before using cached terminology map

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Add resolvedLabel as a nullable late field or include it in copyWith/fromJson as an optional parameter defaulting to null. The cleanest pattern is to add a withResolvedLabel(String label) method on ActivityType that returns a copyWith-constructed instance with the label set, keeping the domain model pure. In the service, after fetching from cache/repository, resolve labels in a single pass: final labelMap = await labelsProvider.getTerminologyMap(orgId); return types.map((t) => t.withResolvedLabel(labelMap[t.labelKey] ?? t.labelKey)).toList().

The OrganizationLabelsProvider should already be a Riverpod provider — inject it into ActivityTypeService via constructor parameter typed as OrganizationLabelsProvider (or its interface). Ensure this integration does not break existing callers of getActiveTypes — resolvedLabel being populated is additive, not breaking.

Testing Requirements

Unit tests using flutter_test with mocked OrganizationLabelsProvider: (1) known labelKey resolves to org-specific display string, (2) unknown labelKey falls back to raw labelKey without error, (3) empty terminology map results in all types using labelKey as resolvedLabel, (4) provider injection: service calls OrganizationLabelsProvider.getLabel(orgId, labelKey) not a static method, (5) toJson() output does not include resolvedLabel key, (6) fromJson() creates ActivityType with resolvedLabel as null until service resolves it. Verify OrganizationLabelsProvider is called exactly once per getActiveTypes invocation (not once per activity type) using mockito verify call count.

Component
Activity Type Service
service medium
Epic Risks (2)
medium impact medium prob scope

Metadata flag combination rules differ between organisations (e.g., Blindeforbundet honorarium thresholds, HLF mutual exclusion of km and transit). Encoding these as generic service-level validation may be insufficient, forcing organisation-specific branching inside the service that becomes unmaintainable as new organisations are onboarded.

Mitigation & Contingency

Mitigation: Model flag validation as a pure function that accepts an ActivityTypeMetadata object and an org configuration record, making org-specific rules data-driven rather than hardcoded. Establish the validation contract in the foundation epic so the service just delegates to the validator.

Contingency: Defer complex cross-flag validation to a lightweight edge function that can be updated without a mobile app release, accepting that initial validation in the mobile service layer is permissive and corrected server-side.

high impact low prob technical

Blindeforbundet users rely on VoiceOver and JAWS. If the selection screen is built with non-semantic widgets that fail accessibility audit late in the sprint, a significant rework of the widget tree may be required, blocking the registration wizard integration.

Mitigation & Contingency

Mitigation: Build the selection screen against the project's established accessibility design tokens and semantics wrapper conventions from the start. Run Flutter's semantic tree inspector and a manual VoiceOver pass before marking any widget task complete.

Contingency: Wrap all tappable items in the project's SemanticsWrapperWidget and schedule a dedicated accessibility review session with a screen reader user from Blindeforbundet before the epic is closed.