critical priority high complexity backend pending backend specialist Tier 4

Acceptance Criteria

Dispatching SubmitExpenseEvent triggers ExpenseValidationService.validate() before any network call
Validation failures emit ExpenseFormValidationErrorState with a Map<String, String> of field-id to plain-language error message sourced exclusively from the error message registry
Each form field widget rebuilds with its error message visible inline when the error state is emitted for that field
Network or Supabase errors emit ExpenseFormNetworkErrorState with a retryable flag set to true and an actionable user-facing message (e.g. 'Could not save. Tap to try again.')
Successful submission emits ExpenseFormSubmittedState carrying the new expense_claim UUID returned by the backend
Tapping retry from the network error state re-dispatches SubmitExpenseEvent without requiring user re-entry of data
BLoC transitions are logged at debug level; no PII is logged
BLoC is closed and streams cleaned up when the widget is disposed
Unit tests cover: validation error path, network error path, success path, and retry path with 100% branch coverage on the event handler

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc)
Dart
apis
ExpenseValidationService.validate()
ExpenseSubmissionService.submit()
Supabase PostgREST (expense_claims insert)
Error message registry lookup
data models
ExpenseClaim
ExpenseFormValidationError
ExpenseFormState (sealed class with Loading, ValidationError, NetworkError, Submitted variants)
performance requirements
BLoC event processing must complete within 200 ms excluding network I/O
State emissions must not cause full-tree rebuilds; use BlocBuilder with buildWhen predicate scoped to individual fields
security requirements
Supabase RLS policy must be validated server-side; client-side validation is UX-only and must not be relied on for authorization
No expense field values (amounts, descriptions) may appear in crash logs or analytics events
Submission payload must be sent over HTTPS only
ui components
Per-field inline error text widget
Retry snack-bar or inline retry button

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Use a sealed class (Dart 3) for ExpenseFormState to get exhaustive pattern matching in BlocBuilder. The submit event handler should follow the sequence: (1) emit loading state, (2) call validation service, (3) if errors return validation error state early, (4) call submission service in try/catch, (5) on catch emit network error, (6) on success emit submitted. Keep the BLoC free of direct Supabase calls — all I/O goes through the service layer. The error message registry should be a simple Map or a dedicated class with named getters; never hardcode error strings in the BLoC.

Use transformEvents with sequential transformer to prevent duplicate submissions from rapid taps. Register the BLoC with BlocProvider above the route to survive navigation events during the multi-step flow.

Testing Requirements

Unit tests using flutter_test and bloc_test package. Test the BLoC in isolation by mocking ExpenseValidationService and ExpenseSubmissionService. Scenarios: (1) all fields valid → submitted state with correct expense ID; (2) one required field empty → validation error state with correct field key and plain-language message; (3) multiple fields invalid simultaneously → all errors present in single emission; (4) network timeout → network error state with retryable=true; (5) Supabase returns 409 conflict → specific error message; (6) retry after network error succeeds → submitted state. Widget tests for the form should verify error text appears beneath the correct field after validation error state.

Aim for 100% branch coverage on ExpenseFormBloc event handlers.

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.