Implement BenefitCalculatorBloc event handlers
epic-benefit-calculator-core-logic-task-006 — Implement the BenefitCalculatorBloc class. On init, call BenefitMultiplierConfigRepository.fetchConfig() and emit a loading state. On InputChanged, call BenefitCalculationService.calculate() with the latest input and config, emit BenefitCalculatorSuccess with the result. On ResetRequested, clear inputs and emit BenefitCalculatorInitial. On ShareRequested, emit the current result for the UI share sheet. Handle repository fetch errors by emitting BenefitCalculatorError with an error message.
Acceptance Criteria
Technical Requirements
Execution Context
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.
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.
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.