fix(reporting): negative scenario carbon impact — move carbon fragments to effective baseline

"Total carbon saved" on the reporting scenario overlay went negative
(portfolio 796: -10,673 t/yr, £/tCO2 -8,195). Cause: the baseline side
(getTotals → carbonSql) summed bp.lodged_co2_emissions_t_per_yr, which
is NULL for predicted-only properties (12,064 of 796's 31,919 — no
mirrored lodged estimate is written), so they contributed nothing to
the baseline. The scenario side counted their plans' post_co2_emissions
(21,046 t) in full, dragging saved below zero. The same subtraction fed
£-per-tonne-CO2 and average-per-unit.

carbonSql and energyConsumptionSql were left on lodged_* when ADR-0002
(effective performance is the canonical current, superseding "reporting
stays on lodged") migrated the SAP fragments — the same reporting cards
were already mixing effective SAP with lodged carbon. Move both to
effective_*, which is populated for every property with a baseline row
and is the figure the plans' post_* values were scored against. All
call sites (reporting averages/totals, both scenario metrics routes,
portfolio table CO2 column/filter) are current-state surfaces, so none
wanted lodged. Legacy branch untouched.

Verified live on 796: baseline and scenario totals computed in one
snapshot with the shared fragment give saved = +4,059 t/yr (a plan
regeneration was writing 796 mid-diagnosis, so values drift run-to-run;
pre-fix the same snapshot maths gives ~ -13k and worsening as plans
land). Legacy portfolio 433 totals unchanged.

No regression-test seam exists for cross-query consistency (no DB test
harness); consistency is structural — both sides compose the same
carbonSql fragment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-02 14:52:21 +00:00
parent 051927dda4
commit 1cd48bc78e

View file

@ -77,16 +77,19 @@ export const newApproachJoins = sql`
`;
/**
* Baseline carbon (t CO/yr). New: the lodged (gov-registry) value. Legacy:
* e.co2_emissions. Swap to `effective_co2_emissions_t_per_yr` if the
* modelling-adjusted baseline is wanted instead.
* Baseline carbon (t CO/yr). New: the effective (re-baselined) value the
* canonical "current" per ADR-0002, and the baseline the plans' post_* figures
* were scored against. The lodged value is NULL for predicted-only properties
* (no mirrored estimate is written), which made lodged-based reporting totals
* silently exclude them and drove "carbon saved" negative whenever their
* plans' post carbon counted on the scenario side. Legacy: e.co2_emissions.
*/
export const carbonSql = (e: Alias) =>
sql`CASE WHEN ${isNewApproachSql} THEN bp.lodged_co2_emissions_t_per_yr ELSE ${e}.co2_emissions END`;
sql`CASE WHEN ${isNewApproachSql} THEN bp.effective_co2_emissions_t_per_yr ELSE ${e}.co2_emissions END`;
/** Primary energy intensity (kWh/m²/yr). New: lodged value. Legacy: e.primary_energy_consumption. */
/** Primary energy intensity (kWh/m²/yr). New: effective value (ADR-0002, as carbonSql). Legacy: e.primary_energy_consumption. */
export const energyConsumptionSql = (e: Alias) =>
sql`CASE WHEN ${isNewApproachSql} THEN bp.lodged_primary_energy_intensity_kwh_per_m2_yr ELSE ${e}.primary_energy_consumption END`;
sql`CASE WHEN ${isNewApproachSql} THEN bp.effective_primary_energy_intensity_kwh_per_m2_yr ELSE ${e}.primary_energy_consumption END`;
/**
* Baseline SAP score. New: the lodged (gov-registry) score from
@ -566,9 +569,9 @@ export async function resolveConditionReport(
* effective score away from the lodged (gov-registry) EPC, so showing lodged as
* "current" misrepresents a plan's uplift e.g. lodged C/78 vs an effective
* baseline of C/71 with a post-retrofit C/71.4 reads as flat instead of the real
* improvement. The set-based reporting fragments (`sapSql`, `epcBandSql`,
* `carbonSql`, ) deliberately stay on `lodged_*` to keep portfolio/reporting
* totals matched to the gov register.
* improvement. The set-based reporting fragments (`effectiveSapSql`,
* `carbonSql`, `energyConsumptionSql`, ) read `effective_*` too (ADR-0002);
* only the dedicated "Lodged EPC" surfaces use the lodged fragments.
*/
export async function resolvePropertyHeadline(
propertyId: bigint,