critical priority high complexity backend pending backend specialist Tier 2

Acceptance Criteria

ExpenseFormBloc extends Bloc<ExpenseFormEvent, ExpenseFormState> from flutter_bloc.
All six states are modelled as sealed classes or freezed data classes: ExpenseFormInitial, ExpenseFormEditing, ExpenseFormValidating, ExpenseFormSubmitting, ExpenseFormSubmitted, ExpenseFormError.
All seven events are modelled as sealed/freezed classes: TypeSelected, AmountChanged, ReceiptAttached, ReceiptRemoved, SubmitExpense, ResetForm, and at minimum one validation-trigger event.
ExpenseFormEditing state includes: selectedTypes (Set<ExpenseTypeId>), amount (String), distanceKm (String?), receipt (ReceiptAttachment?), receiptRequired (bool), showDistanceField (bool), validationErrors (Map<String, String>).
Selecting a mileage expense type sets showDistanceField: true; deselecting it (or selecting an incompatible type that replaces it) sets showDistanceField: false.
Selecting a fixed-amount type sets showDistanceField: false regardless of prior state.
AmountChanged event re-evaluates receiptRequired against ExpenseThresholdConfig and emits updated state.
ReceiptAttached event stores the ReceiptAttachment in state; ReceiptRemoved clears it and re-evaluates receiptRequired.
SubmitExpense event transitions to ExpenseFormValidating, runs all field validators, and either transitions to ExpenseFormSubmitting (all valid) or back to ExpenseFormEditing with populated validationErrors.
ResetForm event returns the BLoC to ExpenseFormInitial, clearing all fields.
BLoC is injectable and does not directly reference any Widget — it depends only on abstractions (ExpenseThresholdConfig, ExpenseSubmissionRepository).
All state transitions are pure (no side effects in event handlers except via injected dependencies).

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc ≥ 8.x)
freezed (optional but recommended for sealed state/event classes)
apis
ExpenseSubmissionRepository (internal abstraction over Supabase)
data models
ExpenseType
ExpenseTypeConflictRule
ExpenseThresholdConfig
ReceiptAttachment
ExpenseFormEditing
ValidationError
performance requirements
All event handlers complete synchronously or emit at least one intermediate state before any async operation.
No blocking calls on the event loop; async work uses await with proper error handling.
BLoC emits distinct states only; use Equatable or freezed to prevent redundant rebuilds.
security requirements
Amount values are validated as non-negative numeric strings before submission; reject non-numeric input with a validation error.
No raw user input is passed unsanitised to the repository layer.
BLoC does not store authentication tokens or PII beyond what is strictly required for submission.

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Use flutter_bloc 8.x Bloc (not Cubit) for explicit event traceability. Model states and events with sealed classes (Dart 3) or freezed — this gives exhaustive pattern matching and copyWith for free. Keep the BLoC constructor clean: ExpenseFormBloc({required ExpenseThresholdConfig config, required ExpenseSubmissionRepository repository}). Implement a private _validateForm() method that returns Map — call it only in the SubmitExpense handler.

Delegate conflict rule evaluation to the same ConflictRuleEngine used by the selector widget to keep logic DRY. Register the BLoC with a BlocProvider at the expense form screen level, not globally, so it is disposed when the screen is popped. Use emit.forEach() or on() syntax (not mapEventToState which is deprecated in bloc 8.x).

Testing Requirements

Unit tests (flutter_test / bloc_test): use bloc_test's blocTest() helper for every event/state transition. Test: initial state is ExpenseFormInitial; TypeSelected with mileage type → showDistanceField true; TypeSelected with fixed-amount type → showDistanceField false; AmountChanged below threshold → receiptRequired false; AmountChanged above threshold → receiptRequired true; ReceiptAttached → receipt set in state; ReceiptRemoved → receipt null; SubmitExpense with missing required fields → ExpenseFormEditing with validationErrors; SubmitExpense with all valid fields → ExpenseFormSubmitting → ExpenseFormSubmitted (mock repository success); SubmitExpense with repository error → ExpenseFormError; ResetForm → ExpenseFormInitial. Target 100% event-handler coverage.

Component
Expense Form BLoC
service high
Epic Risks (3)
medium impact medium prob dependency

The image_picker Flutter plugin requires platform-specific permissions (NSPhotoLibraryUsageDescription, camera permission) and behaves differently across iOS and Android versions. Permission denial or plugin misconfiguration can silently prevent receipt attachment.

Mitigation & Contingency

Mitigation: Configure all required permission strings in Info.plist and AndroidManifest.xml during initial plugin setup. Use the permission_handler package to check and request permissions before launching the picker, with clear user-facing explanations. Test on both platforms across at least two OS versions.

Contingency: If image_picker proves unreliable on a specific platform version, fall back to file_picker as an alternative that uses the OS document picker interface, which requires fewer permissions on some Android versions.

high impact medium prob technical

The expense form BLoC manages interconnected state across expense type selection, field visibility, receipt requirement, threshold evaluation, and submission flow. Incorrect state transitions can cause UI inconsistencies such as required receipt indicator not updating after amount change, or form appearing valid when mutual exclusion is violated.

Mitigation & Contingency

Mitigation: Model BLoC states as sealed classes with exhaustive pattern matching. Write state transition unit tests covering every combination of: type selection change, amount field change above/below threshold, receipt attachment/removal, and offline mode toggle. Use bloc_test for comprehensive state sequence assertions.

Contingency: If BLoC complexity becomes unmanageable, split into two BLoCs — one for type selection/exclusion state and one for field values/submission — coordinating via a parent provider, accepting the small overhead of inter-BLoC communication.

high impact medium prob technical

The expense type selector must enforce mutual exclusion visually by disabling options and showing conflict tooltips, while remaining fully accessible to screen reader users who cannot perceive visual disable states. Incorrect semantics labelling will fail WCAG 2.2 AA requirements critical for Blindeforbundet and HLF users.

Mitigation & Contingency

Mitigation: Use Flutter Semantics widgets to explicitly set disabled state and provide conflict explanations as semanticLabel strings on disabled options. Run accessibility audits with TalkBack and VoiceOver during widget development, not post-completion. Reference the project's accessibility test harness for required test coverage.

Contingency: If custom widget accessibility is difficult to certify, implement the selector as a standard Flutter Radio/Checkbox group with built-in accessibility semantics and an explanatory Text widget below each conflicting option, sacrificing visual elegance for guaranteed WCAG compliance.