critical priority low complexity infrastructure pending backend specialist Tier 3

Acceptance Criteria

BenefitCalculatorBloc extends Bloc<BenefitCalculatorEvent, BenefitCalculatorState> from the bloc package
The BLoC constructor accepts an IBenefitMultiplierConfigRepository and a BenefitCalculationService (dependency injected for testability)
On construction, the BLoC immediately triggers an internal _ConfigRequested event or calls fetchConfig() in the constructor body, emitting BenefitCalculatorInitial (isConfigLoading: true) as the initial state
After a successful fetchConfig(), the BLoC stores the config internally and emits BenefitCalculatorInitial (isConfigLoading: false), ready for user input
If fetchConfig() throws, the BLoC emits BenefitCalculatorError with a user-facing error message string
On InputChanged: calls BenefitCalculationService.calculate(event.input, _cachedConfig) synchronously and emits BenefitCalculatorSuccess with the result; if config is not yet loaded, emits BenefitCalculatorLoading
On ResetRequested: emits BenefitCalculatorInitial (isConfigLoading: false, result: null) — config is preserved and not re-fetched
On ShareRequested: emits a BenefitCalculatorSuccess state with the current result (or does nothing if result is null); the UI reads state.result to trigger the share sheet
The BLoC uses on<EventType>(handler) registration, not mapEventToState (deprecated API)
The BLoC is registered in the app's provider tree (BlocProvider or Riverpod equivalent) scoped to the benefit calculator screen
Dispose releases any resources held by the repository if applicable

Technical Requirements

frameworks
Flutter
BLoC
apis
IBenefitMultiplierConfigRepository.fetchConfig()
data models
BenefitCalculationResult
BenefitMultiplierConfig
ActivityInput
performance requirements
InputChanged handler must be synchronous (no async gap) once config is loaded — UI must feel instant
Config fetch is a one-time async operation on startup; subsequent calculations are O(1)
security requirements
The cached BenefitMultiplierConfig must not be exposed via a public getter — it is internal BLoC state
Error messages emitted must not expose raw exception stack traces to the UI

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Cache the fetched BenefitMultiplierConfig as a private nullable field (_config). Guard the InputChanged handler: if _config is null, emit BenefitCalculatorLoading and queue the event for retry, or simply discard (the UI should prevent input before config loads). The simplest approach: disable the input form while isConfigLoading is true. Use transformer: sequential() on InputChanged to avoid race conditions if the user types rapidly (though since it is synchronous this is low risk).

For ShareRequested, do not trigger the platform share sheet from within the BLoC — emit a state and let the UI layer call Share.share() from the share_plus package. Keep error messages localisation-ready: store them as string keys rather than hardcoded English strings if the app supports l10n.

Testing Requirements

Unit tests using flutter_test and bloc_test package (if available in the project; otherwise use raw BLoC test helpers). Test scenarios: (1) initial state is BenefitCalculatorInitial with isConfigLoading: true; (2) successful config load transitions to BenefitCalculatorInitial with isConfigLoading: false; (3) InputChanged after config load emits BenefitCalculatorSuccess with correct BenefitCalculationResult; (4) InputChanged before config load emits BenefitCalculatorLoading; (5) fetchConfig() failure emits BenefitCalculatorError with non-empty errorMessage; (6) ResetRequested from Success state returns to Initial (result null, isConfigLoading: false); (7) ShareRequested with an existing result re-emits BenefitCalculatorSuccess; (8) ShareRequested with null result does not crash. Use FakeBenefitMultiplierConfigRepository stub to control config fetch outcomes.

Component
Benefit Calculator BLoC
infrastructure low
Epic Risks (2)
medium impact medium prob scope

The exact formulas for SROI social value (public health system cost offset) may not be agreed upon with the client organisations or Bufdir. If formulas are disputed post-implementation, the service and all downstream tests will need to be revised.

Mitigation & Contingency

Mitigation: Document the two formulas and their multiplier inputs explicitly in the BenefitCalculationService source file and obtain sign-off from the product owner before implementation begins. Store formula multipliers exclusively in the Supabase config table so adjustments require only a config update, not a code deployment.

Contingency: If formulas are revised after implementation, the pure-function architecture means changes are isolated to BenefitCalculationService. Update the service, adjust unit tests, and re-run the test suite. No UI components need modification.

medium impact low prob technical

The BLoC must handle the asynchronous config fetch from the multiplier repository during initialisation. Race conditions between the config loading state and the first InputChanged event could result in calculations running against null or stale multiplier values.

Mitigation & Contingency

Mitigation: Guard all InputChanged event handlers in the BLoC with a null check on the loaded config state. Emit BenefitCalculationLoading until config resolves. Write a BLoC test that fires InputChanged before config loads and asserts the state remains BenefitCalculationLoading.

Contingency: If race conditions surface in integration testing, add an explicit config-loaded flag and queue InputChanged events until the flag is set, draining the queue on config resolution.