critical priority medium complexity backend pending backend specialist Tier 4

Acceptance Criteria

Calling submitRegistration() emits ActivityRegistrationSubmitting as the first state transition, before any async work begins
On service success, ActivityRegistrationSuccess is emitted with an ActivityRecord that has syncStatus == SyncStatus.pending and a local_ prefixed UUID id
The optimistic record is emitted synchronously relative to the service call — the UI confirmation screen must appear before network round-trip completes
On service failure (network error, validation error, or domain exception), ActivityRegistrationFailure is emitted with a non-null, non-empty plain-language errorMessage string (no stack traces or technical codes exposed to user)
After a successful submit, RegistrationDefaultsManager.saveLastUsedActivityType() is called with the activity type from the submitted payload
saveLastUsedActivityType() is NOT called on failure — defaults only updated on confirmed successful submission
The cubit does not enter an unrecoverable state on failure — user can retry submission after an ActivityRegistrationFailure
No state transitions occur after the cubit is closed (guards against async completion after widget disposal)
Submit cannot be triggered while already in ActivityRegistrationSubmitting state (idempotency guard)

Technical Requirements

frameworks
Flutter
BLoC
bloc
flutter_bloc
apis
ActivityRegistrationService.submitRegistration()
RegistrationDefaultsManager.saveLastUsedActivityType()
data models
activity
activity_type
performance requirements
Optimistic state emission must occur within one event-loop tick of the service call — no artificial delays
Total state transition from Submitting → Success must feel instantaneous to the user regardless of network latency
security requirements
Plain-language error messages must not expose internal exception details, database errors, or PII
Cubit must not retain submitted payload in state longer than needed — clear sensitive fields after success

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Use `emit(ActivityRegistrationSubmitting())` as the very first line inside submitRegistration() before any await. Build the optimistic ActivityRecord using a local UUID (`'local_${const Uuid().v4()}'`) and syncStatus.pending immediately, then pass it to the service. Wrap the service call in try/catch catching both typed domain exceptions and generic Exception. In the catch block, map exceptions to user-friendly strings using a private _toUserMessage() helper — never expose raw exception.toString().

Guard against post-close emission with `if (isClosed) return` after each await. For the idempotency guard, check `state is ActivityRegistrationSubmitting` at the top of the method. Keep saveLastUsedActivityType() inside the success branch, after emit, so a failure in saving defaults does not corrupt the success state — wrap it in a separate try/catch that logs silently.

Testing Requirements

Unit tests using bloc_test package. Test that emit sequence for happy path is exactly [ActivityRegistrationSubmitting, ActivityRegistrationSuccess]. Test that optimistic record in success state has syncStatus == pending and a local_ prefixed id. Test that failure path emits [ActivityRegistrationSubmitting, ActivityRegistrationFailure] with non-empty errorMessage.

Test that saveLastUsedActivityType is called exactly once on success and zero times on failure using mocktail verify(). Test that a second call to submitRegistration() while in Submitting state is a no-op. Test cubit.close() mid-flight does not throw. Aim for 100% branch coverage on submitRegistration().

Component
Activity Registration Cubit
service medium
Dependencies (3)
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.