critical priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

RegistrationDefaultsManager implements IRegistrationDefaultsManager and is registered in the DI container (Riverpod provider or BLoC service locator)
getDefaults() returns a RegistrationDefaults with date = DateTime.now() (date portion only, no time component), duration = Duration(minutes: 30), and lastUsedActivityTypeId read from RegistrationPreferencesStore
On first run (no stored preference), getDefaults() returns RegistrationDefaults with lastUsedActivityTypeId = null — no exception is thrown
getDefaults() completes in under 50ms (local storage read, not a network call)
RegistrationPreferencesStore is injected via constructor (not accessed as a singleton) enabling test substitution
saveLastUsedActivityType(String activityTypeId) writes the ID to RegistrationPreferencesStore and returns a completed Future on success
If RegistrationPreferencesStore throws during read, getDefaults() catches the error, logs it, and returns the null-safe fallback RegistrationDefaults rather than propagating the exception
Class is placed under lib/features/activity_registration/data/ following the project layer structure

Technical Requirements

frameworks
Flutter
Riverpod or BLoC
apis
RegistrationPreferencesStore (local key-value storage, e.g. shared_preferences)
data models
RegistrationDefaults
IRegistrationDefaultsManager
performance requirements
getDefaults() must complete within 50ms (synchronous shared_preferences read after initialization)
No network I/O in this class — all persistence is local
security requirements
Activity type IDs stored in shared_preferences are non-sensitive; no encryption required
Do not store full activity objects — only the opaque string ID

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Introduce a thin IRegistrationPreferencesStore abstraction around shared_preferences (one read method, one write method, one clear method) — this makes unit testing trivial without initialising Flutter binding. Inject it via constructor. For the date field, call DateTime.now() inside getDefaults() on each invocation rather than caching it, so the date is always today even if the manager is a long-lived singleton. Use a constant string key (e.g.

'last_used_activity_type_id') for the preferences key — define it as a private static const to avoid typo-related bugs. The error handling in getDefaults() should use a try/catch around the store read only, not around the entire method, so programming errors still surface clearly during development.

Testing Requirements

Unit tests using flutter_test with a mocked IRegistrationPreferencesStore: test getDefaults() returns null lastUsedActivityTypeId when store returns null, test getDefaults() returns the stored ID when store has a value, test getDefaults() returns fallback RegistrationDefaults when store throws, test saveLastUsedActivityType() calls store.write() with correct key and value. Verify DateTime.now() is used for date (use a clock abstraction or check the date is today's date). Use mockito or mocktail to mock IRegistrationPreferencesStore. Target 100% branch coverage.

Component
Registration Defaults Manager
service low
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.