critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

submitRegistration(ActivityRegistrationPayload payload) is an async method returning Result<ActivityRecord, RegistrationError>
Step 1 — validation: if ActivityRegistrationValidator.validate(payload) returns errors, the method immediately returns Result.failure(RegistrationError.validationFailed(errors)) without any network call
Step 2 — eligibility: isEligibleForCompensation is called and its result is attached to the ActivityRecord (eligibility flag + reason) before the optimistic record is emitted
Step 3 — optimistic record: a local ActivityRecord is created with a client-generated UUID, status: ActivityStatus.pendingSync, and all payload fields. Result.success(optimisticRecord) is returned to the caller immediately — before the Supabase write completes
Step 4 — async persist: ActivityRepository.save(optimisticRecord) is called asynchronously (unawaited from the caller's perspective). On success the record status transitions to ActivityStatus.synced
On network failure (SocketException, TimeoutException, Supabase error): the record remains in ActivityStatus.pendingSync in local storage and is added to a retry queue managed by ActivitySyncQueue
RegistrationError is a sealed class with at minimum: validationFailed(List<ValidationError>), networkError(String message), unknownError(Object error)
ActivityRecord is an immutable value object with: id (String UUID), payload fields, compensationEligibility (CompensationEligibilityResult), status (ActivityStatus), createdAt (DateTime), syncedAt (DateTime?)
The method must not throw — all errors are wrapped in Result.failure
Integration test: submitRegistration with valid payload and mocked Supabase returning success → record transitions from pendingSync to synced
Integration test: submitRegistration with valid payload and mocked Supabase throwing network error → record stays pendingSync, no exception propagates to caller

Technical Requirements

frameworks
Flutter
BLoC
Dart
Riverpod
apis
Supabase REST API (insert activity record)
Supabase Realtime (optional for sync status updates)
data models
ActivityRegistrationPayload
ActivityRecord
ActivityStatus
CompensationEligibilityResult
RegistrationError
ActivitySyncQueue
performance requirements
Result.success(optimisticRecord) must be returned within 5ms of method invocation — the Supabase write must not block the return
Retry queue must implement exponential backoff with a maximum of 5 retries before marking the record as syncFailed
Local record creation must be O(1) with no database reads
security requirements
Client-generated UUIDs must use dart:math cryptographically random source or the uuid package — not sequential IDs
The Supabase insert must use the authenticated user session — never a service role key on the client
ActivityRecord must never include raw authentication tokens or session data

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

The key architectural decision is decoupling the optimistic return from the async persist. Use Dart's fire-and-forget pattern: call `unawaited(repository.save(record))` after emitting the result, or use a microtask queue. Do NOT use then() callbacks that could silently swallow exceptions — handle errors explicitly in the async chain and update local state accordingly. For local storage of pending-sync records, use a lightweight in-memory queue backed by SQLite or Hive (match whatever local persistence the rest of the app uses).

The ActivitySyncQueue should be a singleton Riverpod provider so the UI can observe pending-sync count. Use the Result type pattern already established in the codebase — do not introduce a new error-handling monad. The uuid package (already likely in pubspec) should generate v4 UUIDs for client-side IDs.

Testing Requirements

Unit and integration tests with flutter_test. Unit tests: (1) validation failure short-circuits and returns RegistrationError.validationFailed without calling ActivityRepository, (2) valid payload creates optimistic record with correct fields and pendingSync status, (3) eligibility result is correctly embedded in the optimistic record. Integration tests with mocked ActivityRepository: (4) successful Supabase response transitions record to synced, (5) network exception keeps record at pendingSync and adds it to ActivitySyncQueue, (6) method never throws — all error paths return Result.failure. Test the retry queue separately: (7) exponential backoff intervals are correct, (8) record transitions to syncFailed after max retries.

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.