critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

ActivityTypeService implements IActivityTypeService from task-001
getActiveTypes(orgId) delegates to ActivityTypeCacheNotifier.getActiveTypes(orgId) and returns List<ActivityType>
createActivityType validates all required fields are non-null and non-empty before calling repository
createActivityType enforces: if triggersReimbursementWorkflow == true AND org has strictReimbursementConfig == true, then isTravelExpenseEligible must be true — throws ActivityTypeValidationException if violated
updateActivityType applies the same metadata flag validation as create
deactivateActivityType calls repository.softDelete and then calls cacheProvider.invalidate(orgId)
createActivityType calls cacheProvider.invalidate(orgId) after successful repository.create
updateActivityType calls cacheProvider.invalidate(orgId) after successful repository.update
All mutations return the updated ActivityType domain object
Service throws ActivityTypeValidationException (typed) for business rule violations, distinct from RepositoryException for DB errors
ActivityTypeValidationException includes a human-readable message field suitable for display in UI error states
Unit test: createActivityType with triggersReimbursementWorkflow=true and isTravelExpenseEligible=false throws ActivityTypeValidationException when strictReimbursementConfig is true
Unit test: createActivityType with triggersReimbursementWorkflow=true and isTravelExpenseEligible=false succeeds when strictReimbursementConfig is false
Unit test: successful create calls invalidate on cache provider

Technical Requirements

frameworks
Flutter
Riverpod
Dart
apis
Supabase PostgreSQL 15
data models
activity_type
performance requirements
Validation must be synchronous and complete before any async repository call
Service methods must not block the UI thread — all async operations use Future/async-await
security requirements
Service must verify orgId from authenticated session matches the orgId in mutation requests — never trust client-provided orgId alone
ActivityTypeValidationException messages must not leak internal system details (table names, column names)
strictReimbursementConfig must be fetched from a trusted server-side source, not from client state

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Place in lib/features/activity_type/domain/services/activity_type_service.dart. Inject IActivityTypeRepository and ActivityTypeCacheNotifier via constructor for testability. The org strictReimbursementConfig flag should come from an OrganizationConfigProvider (or equivalent) injected into the service — do not hard-code which orgs are strict. The validation logic for triggersReimbursementWorkflow/isTravelExpenseEligible should be extracted into a private _validateMetadataFlags() method to keep mutation methods readable.

Always invalidate cache AFTER a successful mutation (not before) — if the mutation fails, the cache remains valid. Use Result pattern or standard Dart exceptions — stay consistent with the pattern used elsewhere in the codebase. Do not map DB rows in the service layer; rely on ActivityType.fromJson() defined in task-001.

Testing Requirements

Unit tests using flutter_test with mocked IActivityTypeRepository and mocked ActivityTypeCacheNotifier: (1) getActiveTypes delegates to cache provider, (2) createActivityType with valid data calls repository.create and then cache.invalidate, (3) updateActivityType with valid data calls repository.update and then cache.invalidate, (4) deactivateActivityType calls repository.softDelete and cache.invalidate, (5) triggersReimbursementWorkflow validation: strict org + incompatible flags throws ActivityTypeValidationException, (6) triggersReimbursementWorkflow validation: non-strict org + same flags succeeds, (7) repository.create failure propagates RepositoryException without calling cache.invalidate, (8) all mutations return correct ActivityType object. Test matrix covers all flag combinations for metadata validation.

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.