critical priority low complexity backend pending backend specialist Tier 2

Acceptance Criteria

ActivityRegistrationCubit constructor accepts ActivityRegistrationService and RegistrationDefaultsManager as required named parameters (constructor injection)
Constructor emits ActivityRegistrationInitial as the initial state
initializeWithDefaults() is an async method that calls RegistrationDefaultsManager.getDefaults() and on success emits ActivityRegistrationStep with all fields populated from the defaults object
Default values from RegistrationDefaultsManager must include at minimum: activityTypeId (the organisation's most recently used or most common activity type), date (today's date), durationMinutes (30 as per HLF specification), notes (null/empty)
After initializeWithDefaults() completes successfully, the emitted ActivityRegistrationStep state contains no validationErrors (empty map)
If RegistrationDefaultsManager.getDefaults() throws or returns null, the cubit emits ActivityRegistrationFailure with a user-facing message 'Unable to load registration defaults. Please try again.' and does NOT remain stuck in ActivityRegistrationInitial
initializeWithDefaults() is idempotent — calling it a second time re-fetches defaults and re-emits ActivityRegistrationStep (useful for retry after failure)
The method is called automatically from the UI layer on sheet open — do not call it in the constructor (to allow for lazy initialisation and avoid issues with async constructors)
Unit test: after calling initializeWithDefaults() with a mocked RegistrationDefaultsManager returning valid defaults, the emitted state is ActivityRegistrationStep with all expected field values
Unit test: RegistrationDefaultsManager throwing causes ActivityRegistrationFailure to be emitted

Technical Requirements

frameworks
Flutter
BLoC
Dart
Riverpod
apis
Supabase REST API (read user preferences / recent activity types)
data models
RegistrationDefaults
ActivityType
UserProfile
OrganisationCompensationConfig
performance requirements
initializeWithDefaults() must complete in < 200ms when RegistrationDefaultsManager returns cached data
The UI sheet must be interactive (showing defaults) within 300ms of opening — RegistrationDefaultsManager must cache aggressively
security requirements
RegistrationDefaultsManager must read user preferences using the authenticated Supabase session
Default peerMentorUserId must always come from the authenticated session, never from cached user input

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use the BLoC library's Cubit base class (not Bloc — no events needed here, only method calls). Inject dependencies via constructor to enable easy mocking in tests. The RegistrationDefaultsManager should be a Riverpod provider so it can share cached state across the app — the Cubit reads it via its injected interface, not directly from Riverpod. Do not use emit() after the cubit is closed — guard all async emit calls with `if (!isClosed) emit(...)`.

The date default must be DateTime.now() evaluated at the time initializeWithDefaults() is called, not at construction time. Document that this method must be called from the widget's initState or equivalent — not from the constructor.

Testing Requirements

Unit tests with flutter_test and bloc_test package. Use bloc_test's blocTest() helper to test state emission sequences. Tests: (1) initial state is ActivityRegistrationInitial, (2) after initializeWithDefaults() with valid mocked defaults, states are [ActivityRegistrationInitial, ActivityRegistrationStep(date: today, durationMinutes: 30, ...)], (3) after initializeWithDefaults() with RegistrationDefaultsManager throwing, states are [ActivityRegistrationInitial, ActivityRegistrationFailure(...)], (4) calling initializeWithDefaults() twice re-emits ActivityRegistrationStep — no stuck state, (5) verify RegistrationDefaultsManager.getDefaults() is called exactly once per initializeWithDefaults() invocation.

Component
Activity Registration Cubit
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.