critical priority low complexity backend pending backend specialist Tier 0

Acceptance Criteria

ActivityRegistrationPayload is an immutable value object (final fields, const constructor where possible) with: activityTypeId (String, non-nullable), date (DateTime, non-nullable), durationMinutes (int, non-nullable), notes (String?, nullable), organisationId (String, non-nullable), peerMentorUserId (String, non-nullable)
ActivityRegistrationPayload implements equality via == and hashCode based on all fields (use Equatable or manual override)
A pure ActivityRegistrationValidator class (or top-level function) exposes a validate(ActivityRegistrationPayload) method returning Either<List<ValidationError>, ActivityRegistrationPayload> or a typed ValidationResult
Validation rule: activityTypeId must not be null or empty string — returns ValidationError.activityTypeRequired on failure
Validation rule: date must not be more than 1 day in the future relative to DateTime.now() — returns ValidationError.dateTooFarInFuture on failure
Validation rule: durationMinutes must be >= 1 and <= 480 — returns ValidationError.durationOutOfRange on failure
ValidationError is a sealed class or enum with human-readable message properties for UI display
All validation rules are evaluated independently and all errors collected (not fail-fast), so multiple errors can be returned at once
Unit tests pass for: valid payload, null activityTypeId, empty activityTypeId, date 2 days in future, date today (valid), date yesterday (valid), duration 0 (invalid), duration 1 (valid), duration 480 (valid), duration 481 (invalid)

Technical Requirements

frameworks
Flutter
BLoC
Dart
data models
ActivityRegistrationPayload
ValidationError
ActivityType
Organisation
UserProfile
performance requirements
Validation must complete synchronously in < 1ms — no async operations
Value object construction must be O(1)
security requirements
organisationId and peerMentorUserId must never be sourced from user-controlled free-text input — always from authenticated session or database references
notes field must be sanitised before persisting (trim whitespace, enforce max length of 1000 characters)

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Model ActivityRegistrationPayload as a plain Dart class with final fields. Do NOT use freezed unless the rest of the codebase already uses it — match existing conventions. For ValidationError, a sealed class (Dart 3) is preferred over an enum because it allows attaching contextual data (e.g. the field name, the actual value) without runtime casting.

The validator should be a stateless class with a single validate() method — no dependencies, no side effects — making it trivially testable. The 'not more than 1 day in future' rule should use DateTime.now() injected via a clock parameter in tests to avoid flakiness. Use a default clock in production. Keep this class in lib/features/activity_registration/domain/models/ following clean architecture conventions.

Testing Requirements

Unit tests only. Use flutter_test. Create ActivityRegistrationPayloadTest and ActivityRegistrationValidatorTest. Cover: (1) all valid permutations for boundary values on date and duration, (2) each validation rule in isolation to confirm correct ValidationError variant returned, (3) multi-error accumulation — a payload with both invalid date and invalid duration returns both errors, (4) equality — two payloads with identical fields are equal, (5) notes field: null is valid, empty string is valid, 1000 chars is valid, 1001 chars is invalid.

Target 100% branch coverage on the validator.

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.