critical priority low complexity backend pending backend specialist Tier 2

Acceptance Criteria

BenefitCalculationService is a class with no instance state — all methods are effectively static or the class has no mutable fields
The calculate() method accepts an ActivityInput and BenefitMultiplierConfig and returns a BenefitCalculationResult
hours_saved is computed as: ActivityInput.duration_hours * BenefitMultiplierConfig.hourly_value_rate
travel_cost_avoided is computed as: ActivityInput.distance_km * BenefitMultiplierConfig.travel_cost_per_km
public_health_offset is computed as: ActivityInput.activities_count * BenefitMultiplierConfig.health_offset_factor
total_societal_value equals the sum of the three fields above
When duration_hours is 0, hours_saved is exactly 0.0 (no rounding artifacts)
When distance_km is 0, travel_cost_avoided is exactly 0.0
The method is deterministic: identical inputs always produce identical outputs
No async operations, no Futures, no Streams, no side effects in this class
ActivityInput must expose at minimum: duration_hours (double), distance_km (double), activities_count (int)
The service has no import of Flutter, Supabase, or BLoC — it is a pure Dart class

Technical Requirements

data models
BenefitCalculationResult
BenefitMultiplierConfig
ActivityInput
performance requirements
Computation must complete in under 1 ms — it is four arithmetic operations
No heap allocations other than the result object
security requirements
Input validation: reject negative duration_hours, distance_km, or activities_count with an ArgumentError
Guard against floating-point overflow for pathologically large inputs

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Make the class a const-constructible singleton or expose a static calculate() method — either pattern is fine, but be consistent with other service classes in the codebase. Dart's double arithmetic is IEEE 754; for currency display, callers should round to 2 decimal places in the UI layer, not here. Do not introduce package:decimal or similar — the values are estimates, not financial ledger entries. ActivityInput likely already exists in the codebase (it is referenced in the activity wizard); if it lacks duration_hours or distance_km, add those fields in this task.

Document the formula inline with a comment referencing the source (e.g., NAV time-value rate) so future maintainers understand the multiplier semantics.

Testing Requirements

Exhaustive unit tests using flutter_test — no mocks needed. Cover: (1) nominal inputs (e.g., 2 hours, 15 km, 3 activities); (2) all zeros input produces all-zero result; (3) fractional hours (0.25, 0.5, 0.75) produce correct proportional output; (4) large km value (e.g., 500 km) does not overflow; (5) activities_count = 1 with non-zero factor; (6) boundary multiplier config (all rates = 1.0) passes through input values unchanged; (7) negative input throws ArgumentError; (8) total_societal_value == hours_saved + travel_cost_avoided + public_health_offset for every test case. Aim for 100% branch coverage.

Component
Benefit Calculation Service
service 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.