high priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

ExpenseThresholdConfig is injected into ExpenseFormBloc and exposes receiptRequiredAbove (double, default 100.0 NOK).
On every AmountChanged event, the BLoC computes: receiptRequired = parsedAmount >= config.receiptRequiredAbove.
When amount is below threshold, receiptRequired is false; when amount equals threshold, receiptRequired is true; when amount is above threshold, receiptRequired is true.
When receiptRequired transitions from false to true, the emitted ExpenseFormEditing state has receiptRequired: true — the receipt capture widget observes this and shows the mandatory-attachment banner.
When receiptRequired transitions from true to false (user decreases amount), receiptRequired: false is emitted and the banner is dismissed.
SubmitExpense validation fails with a specific validationError on the 'receipt' field if receiptRequired is true and receipt is null.
SubmitExpense validation passes the receipt check if receiptRequired is false, regardless of receipt attachment.
SubmitExpense validation passes the receipt check if receiptRequired is true and a receipt is attached.
Non-numeric or empty amount input is treated as amount = 0.0 for threshold evaluation (no crash).
All threshold boundary conditions and mandatory-field combinations are covered by bloc_test unit tests.
ExpenseThresholdConfig threshold value is overridable at test time without modifying production code.

Technical Requirements

frameworks
Flutter
BLoC (flutter_bloc ≥ 8.x)
data models
ExpenseThresholdConfig
ExpenseFormEditing
ValidationError
performance requirements
Threshold evaluation is a synchronous pure function — no async work, no database calls.
receiptRequired recomputation completes within the same event-handling cycle as AmountChanged, ensuring no intermediate inconsistent states are emitted.
security requirements
Threshold value must be sourced from a trusted configuration provider (remote config or compile-time constant); it must not be settable by end users.
Validation must always run server-side in addition to the client-side BLoC check to prevent bypass.

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Extract threshold evaluation into a standalone pure function: bool isReceiptRequired(String rawAmount, ExpenseThresholdConfig config) { final amount = double.tryParse(rawAmount) ?? 0.0; return amount >= config.receiptRequiredAbove; }. Call this from the AmountChanged, ReceiptAttached, and ReceiptRemoved handlers so receiptRequired is always recalculated consistently. Include receipt validation in the private _validateForm() method introduced in task-005: if (state.receiptRequired && state.receipt == null) errors['receipt'] = 'A receipt is required for expenses of ${config.receiptRequiredAbove} NOK or more.'.

Make ExpenseThresholdConfig an abstract interface with a SupabaseRemoteExpenseThresholdConfig production implementation and a ConstExpenseThresholdConfig test implementation. This pattern keeps the BLoC fully testable without mocking remote config infrastructure.

Testing Requirements

Unit tests using bloc_test blocTest(): (1) amount = 0 → receiptRequired false; (2) amount = threshold - 0.01 → receiptRequired false; (3) amount = threshold → receiptRequired true; (4) amount = threshold + 50 → receiptRequired true; (5) amount decreased from above to below threshold → receiptRequired transitions to false; (6) SubmitExpense with receiptRequired true and receipt null → validationErrors contains 'receipt' key; (7) SubmitExpense with receiptRequired true and receipt attached → no receipt validationError; (8) SubmitExpense with receiptRequired false and receipt null → no receipt validationError; (9) non-numeric amount string → receiptRequired false, no exception. Use a TestExpenseThresholdConfig(receiptRequiredAbove: 100.0) in all tests to isolate threshold logic. Target 100% branch coverage on threshold evaluation and receipt validation paths.

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.