critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

ActivityRegistrationService exposes an async method isEligibleForCompensation(ActivityRegistrationPayload payload) returning CompensationEligibilityResult
CompensationEligibilityResult is a value object with: isEligible (bool), reason (String — human-readable, suitable for audit log), activityTypeId (String), organisationId (String)
The method retrieves organisation configuration from RegistrationPreferencesStore using payload.organisationId — no hard-coded organisation IDs in the rules engine
If RegistrationPreferencesStore returns null/missing config for an organisation, eligibility defaults to false with reason 'Organisation configuration not found'
Eligible activity types are defined per organisation in the config (e.g. a list of eligible activityTypeIds or a flag on the activity type record)
An activity type not present in the organisation's eligible list returns isEligible: false with reason 'Activity type not configured for reimbursement for this organisation'
An activity type present in the eligible list returns isEligible: true with reason 'Activity type qualifies for HLF reimbursement'
Ineligible result does NOT throw — callers can always proceed with submission regardless of eligibility
The reason string is written in English and is safe to store in an audit log (no PII, no sensitive data)
Unit tests cover: eligible activity type, ineligible activity type, missing org config, and verify that ineligibility does not throw

Technical Requirements

frameworks
Flutter
BLoC
Dart
Riverpod
apis
Supabase REST API (read organisation preferences)
data models
ActivityRegistrationPayload
CompensationEligibilityResult
OrganisationCompensationConfig
ActivityType
performance requirements
RegistrationPreferencesStore must cache organisation config in memory after first fetch — repeated isEligibleForCompensation calls for the same org must not trigger additional network requests
Method must resolve in < 50ms when config is cached
security requirements
Organisation config must be fetched using the authenticated user's Supabase session — no unauthenticated reads
The reason string must not include raw database values or internal IDs that could leak schema information

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

The eligibility engine must be organisation-agnostic at the code level — all organisation-specific rules live in data (RegistrationPreferencesStore), not in if/switch statements on organisation IDs. This is critical for scalability across NHF, Blindeforbundet, HLF, and future organisations. RegistrationPreferencesStore should be injected as a dependency into ActivityRegistrationService (constructor injection) for testability. CompensationEligibilityResult is purely informational — the downstream reimbursement workflow reads it; this service never initiates reimbursement.

The reason field should be an enum-backed string (e.g. CompensationIneligibilityReason enum) converted to a localisation key or English string, so it is consistent and auditable. Avoid free-form strings generated ad hoc.

Testing Requirements

Unit tests with flutter_test and mocked RegistrationPreferencesStore. Test cases: (1) organisation with eligible activity type returns isEligible: true with correct reason, (2) organisation with ineligible activity type returns isEligible: false, (3) organisation not found in preferences store returns isEligible: false with 'not found' reason, (4) method does not throw when ineligible, (5) cache behaviour — call isEligibleForCompensation twice for the same org, verify RegistrationPreferencesStore.getConfig is called only once. Use mockito or mocktail consistent with the codebase's mocking library.

Component
Activity Registration Service
service medium
Epic Risks (3)
medium impact medium prob technical

The wizard Cubit manages multiple concurrent state slices (current step, each field value, submission status, error state). As the number of wizard steps grows, the state class can become unwieldy, making it difficult to reason about transitions, leading to subtle bugs where advancing a step resets a previously filled field.

Mitigation & Contingency

Mitigation: Use an immutable state model (copyWith pattern) with a separate sealed class per wizard step state. Keep the Cubit's emit calls minimal and always derive the next state from the current state to prevent accidental field resets. Document the state machine transitions explicitly in code comments.

Contingency: If state complexity becomes unmanageable, split into a parent WizardCubit (owns step navigation and submission) and per-step child Cubits (own individual field state), coordinating via a shared repository layer.

high impact medium prob scope

Organisation-specific compensation eligibility rules (e.g., activity type + duration thresholds) are business logic that may change independently of the app release cycle. Hardcoding these rules in ActivityRegistrationService means rule changes require a new app deployment, causing delays and potential financial errors if the deployed version uses outdated rules.

Mitigation & Contingency

Mitigation: Model compensation rules as configuration fetched from Supabase (stored per organisation), cached locally. ActivityRegistrationService reads from cache with a fallback to hardcoded defaults for offline scenarios. Design the rule schema to be extensible without code changes.

Contingency: If dynamic rules are not ready for initial release, ship with hardcoded rules and a feature flag that enables the remote-config path. Document the rule structure clearly so coordinators can trigger a rule update via a Supabase dashboard entry rather than a code deployment.

medium impact low prob integration

The last-used activity type stored in RegistrationPreferencesStore may become invalid if the organisation administrator deactivates that activity type between sessions. The Cubit would pre-populate a deleted type, and either the UI would show a missing item or submission would fail with a foreign-key constraint error.

Mitigation & Contingency

Mitigation: In RegistrationDefaultsManager, validate the retrieved last-used activity type ID against the current list of active types fetched from the activity type repository. If the stored ID is not in the active list, fall back to the first active type alphabetically.

Contingency: If validation cannot be performed offline, surface a non-blocking warning in the activity type step ('Your previously used activity type is no longer available') and require the user to make a new selection before advancing.