From 5553fee32e15712c0e34694ec824cc87cf2a5c2c Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 3 Jun 2026 10:42:13 +0000 Subject: [PATCH] Add EPC round-trip fidelity fixes and property_baseline_performance table to Drizzle schema --- src/app/db/schema/property.ts | 157 ++++++++++++++++++++++++++++------ 1 file changed, 132 insertions(+), 25 deletions(-) diff --git a/src/app/db/schema/property.ts b/src/app/db/schema/property.ts index f5a2433..3efec56 100644 --- a/src/app/db/schema/property.ts +++ b/src/app/db/schema/property.ts @@ -10,6 +10,7 @@ import { smallint, bigint, uniqueIndex, + jsonb, } from "drizzle-orm/pg-core"; import { portfolio, PortfolioStatus } from "./portfolio"; import { InferModel } from "drizzle-orm"; @@ -514,25 +515,25 @@ export const epcProperty = pgTable( energyIsDwellingExportCapable: boolean("energy_is_dwelling_export_capable").notNull(), energyWindTurbinesTerrainType: text("energy_wind_turbines_terrain_type").notNull(), energyElectricitySmartMeterPresent: boolean("energy_electricity_smart_meter_present").notNull(), - energyPvConnection: text("energy_pv_connection"), + energyPvConnection: jsonb("energy_pv_connection"), energyPvPercentRoofArea: integer("energy_pv_percent_roof_area"), energyPvBatteryCapacity: real("energy_pv_battery_capacity"), energyWindTurbineHubHeight: real("energy_wind_turbine_hub_height"), energyWindTurbineRotorDiameter: real("energy_wind_turbine_rotor_diameter"), // Heating config - heatingCylinderSize: text("heating_cylinder_size"), + heatingCylinderSize: jsonb("heating_cylinder_size"), heatingWaterHeatingCode: integer("heating_water_heating_code"), heatingWaterHeatingFuel: integer("heating_water_heating_fuel"), - heatingImmersionHeatingType: text("heating_immersion_heating_type"), - heatingCylinderInsulationType: text("heating_cylinder_insulation_type"), + heatingImmersionHeatingType: jsonb("heating_immersion_heating_type"), + heatingCylinderInsulationType: jsonb("heating_cylinder_insulation_type"), heatingCylinderThermostat: text("heating_cylinder_thermostat"), heatingSecondaryFuelType: integer("heating_secondary_fuel_type"), - heatingSecondaryHeatingType: text("heating_secondary_heating_type"), + heatingSecondaryHeatingType: jsonb("heating_secondary_heating_type"), heatingCylinderInsulationThicknessMm: integer("heating_cylinder_insulation_thickness_mm"), heatingWwhrsIndexNumber1: integer("heating_wwhrs_index_number_1"), heatingWwhrsIndexNumber2: integer("heating_wwhrs_index_number_2"), - heatingShowerOutletType: text("heating_shower_outlet_type"), + heatingShowerOutletType: jsonb("heating_shower_outlet_type"), heatingShowerWwhrs: integer("heating_shower_wwhrs"), // Ventilation @@ -553,6 +554,27 @@ export const epcProperty = pgTable( mechanicalVentDuctInsulation: integer("mechanical_vent_duct_insulation"), mechanicalVentilationIndexNumber: integer("mechanical_ventilation_index_number"), mechanicalVentMeasuredInstallation: text("mechanical_vent_measured_installation"), + mechanicalVentDuctInsulationLevel: integer("mechanical_vent_duct_insulation_level"), + + // Addendum flags + addendumStoneWalls: boolean("addendum_stone_walls"), + addendumSystemBuild: boolean("addendum_system_build"), + addendumNumbers: jsonb("addendum_numbers"), + + // Heating counts + heatingNumberBaths: integer("heating_number_baths"), + heatingNumberBathsWwhrs: integer("heating_number_baths_wwhrs"), + heatingElectricShowerCount: integer("heating_electric_shower_count"), + heatingMixerShowerCount: integer("heating_mixer_shower_count"), + + // Ventilation detail + ventilationPresent: boolean("ventilation_present").notNull().default(false), + ventilationShelteredSides: integer("ventilation_sheltered_sides"), + ventilationHasSuspendedTimberFloor: boolean("ventilation_has_suspended_timber_floor"), + ventilationSuspendedTimberFloorSealed: boolean("ventilation_suspended_timber_floor_sealed"), + ventilationHasDraughtLobby: boolean("ventilation_has_draught_lobby"), + ventilationAirPermeabilityAp4M3HM2: real("ventilation_air_permeability_ap4_m3_h_m2"), + ventilationMechanicalVentilationKind: text("ventilation_mechanical_ventilation_kind"), }, (table) => [ uniqueIndex("uq_epc_property_property_portfolio").on( @@ -627,10 +649,10 @@ export const epcMainHeatingDetail = pgTable( .references(() => epcProperty.id), hasFghrs: boolean("has_fghrs").notNull(), - mainFuelType: text("main_fuel_type").notNull(), - heatEmitterType: text("heat_emitter_type").notNull(), - emitterTemperature: text("emitter_temperature").notNull(), - mainHeatingControl: text("main_heating_control").notNull(), + mainFuelType: jsonb("main_fuel_type").notNull(), + heatEmitterType: jsonb("heat_emitter_type").notNull(), + emitterTemperature: jsonb("emitter_temperature").notNull(), + mainHeatingControl: jsonb("main_heating_control").notNull(), fanFluePresent: boolean("fan_flue_present"), boilerFlueType: integer("boiler_flue_type"), boilerIgnitionType: integer("boiler_ignition_type"), @@ -661,10 +683,10 @@ export const epcBuildingPart = pgTable( constructionAgeBand: text("construction_age_band").notNull(), // Wall - wallConstruction: text("wall_construction").notNull(), - wallInsulationType: text("wall_insulation_type").notNull(), + wallConstruction: jsonb("wall_construction").notNull(), + wallInsulationType: jsonb("wall_insulation_type").notNull(), wallThicknessMeasured: boolean("wall_thickness_measured").notNull(), - partyWallConstruction: text("party_wall_construction").notNull(), + partyWallConstruction: jsonb("party_wall_construction"), buildingPartNumber: integer("building_part_number"), wallDryLined: boolean("wall_dry_lined"), wallThicknessMm: integer("wall_thickness_mm"), @@ -674,7 +696,7 @@ export const epcBuildingPart = pgTable( // Floor floorHeatLoss: integer("floor_heat_loss"), floorInsulationThickness: text("floor_insulation_thickness"), - flatRoofInsulationThickness: text("flat_roof_insulation_thickness"), + flatRoofInsulationThickness: jsonb("flat_roof_insulation_thickness"), floorType: text("floor_type"), floorConstructionType: text("floor_construction_type"), floorInsulationTypeStr: text("floor_insulation_type_str"), @@ -682,8 +704,11 @@ export const epcBuildingPart = pgTable( // Roof roofConstruction: integer("roof_construction"), - roofInsulationLocation: text("roof_insulation_location"), - roofInsulationThickness: text("roof_insulation_thickness"), + roofInsulationLocation: jsonb("roof_insulation_location"), + roofInsulationThickness: jsonb("roof_insulation_thickness"), + + roofConstructionType: text("roof_construction_type"), + curtainWallAge: text("curtain_wall_age"), // Room in roof (inlined) roomInRoofFloorArea: real("room_in_roof_floor_area"), @@ -737,23 +762,23 @@ export const epcWindow = pgTable( .notNull() .references(() => epcProperty.id), - glazingGap: text("glazing_gap").notNull(), - orientation: text("orientation").notNull(), - windowType: text("window_type").notNull(), - glazingType: text("glazing_type").notNull(), + glazingGap: jsonb("glazing_gap").notNull(), + orientation: jsonb("orientation").notNull(), + windowType: jsonb("window_type").notNull(), + glazingType: jsonb("glazing_type").notNull(), windowWidth: real("window_width").notNull(), // add unit? windowHeight: real("window_height").notNull(), // add unit? - draughtProofed: boolean("draught_proofed").notNull(), - windowLocation: text("window_location").notNull(), - windowWallType: text("window_wall_type").notNull(), - permanentShuttersPresent: boolean("permanent_shutters_present").notNull(), + draughtProofed: jsonb("draught_proofed").notNull(), + windowLocation: jsonb("window_location").notNull(), + windowWallType: jsonb("window_wall_type").notNull(), + permanentShuttersPresent: jsonb("permanent_shutters_present").notNull(), frameMaterial: text("frame_material"), frameFactor: real("frame_factor"), permanentShuttersInsulated: text("permanent_shutters_insulated"), // Transmission details (inlined) transmissionUValue: real("transmission_u_value"), - transmissionDataSource: text("transmission_data_source"), + transmissionDataSource: jsonb("transmission_data_source"), transmissionSolarTransmittance: real("transmission_solar_transmittance"), }, ); @@ -773,4 +798,86 @@ export const epcEnergyElement = pgTable( energyEfficiencyRating: integer("energy_efficiency_rating").notNull(), environmentalEfficiencyRating: integer("environmental_efficiency_rating").notNull(), }, +); + +// ─── epc_renewable_heat_incentive ───────────────────────────────────────────── + +export const epcRenewableHeatIncentive = pgTable( + "epc_renewable_heat_incentive", + { + id: bigserial("id", { mode: "bigint" }).primaryKey(), + epcPropertyId: bigint("epc_property_id", { mode: "bigint" }) + .notNull() + .unique() + .references(() => epcProperty.id), + spaceHeatingKwh: real("space_heating_kwh").notNull(), + waterHeatingKwh: real("water_heating_kwh").notNull(), + impactOfLoftInsulationKwh: real("impact_of_loft_insulation_kwh"), + impactOfCavityInsulationKwh: real("impact_of_cavity_insulation_kwh"), + impactOfSolidWallInsulationKwh: real("impact_of_solid_wall_insulation_kwh"), + }, +); + +// ─── property_baseline_performance ──────────────────────────────────────────── + +export const rebaselineReasonEnum = pgEnum("rebaseline_reason", [ + "none", + "pre_sap10", + "physical_state_changed", + "both", +]); + +export const propertyBaselinePerformance = pgTable( + "property_baseline_performance", + { + id: bigserial("id", { mode: "bigint" }).primaryKey(), + propertyId: bigint("property_id", { mode: "bigint" }) + .notNull() + .unique() + .references(() => property.id), + + // Lodged performance (from gov EPC register) + lodgedSapScore: integer("lodged_sap_score").notNull(), + lodgedEpcBand: epcEnum("lodged_epc_band").notNull(), + lodgedCo2EmissionsTPerYr: real("lodged_co2_emissions_t_per_yr").notNull(), + lodgedPrimaryEnergyIntensityKwhPerM2Yr: integer( + "lodged_primary_energy_intensity_kwh_per_m2_yr", + ).notNull(), + + // Effective performance (what modelling scored against) + effectiveSapScore: integer("effective_sap_score").notNull(), + effectiveEpcBand: epcEnum("effective_epc_band").notNull(), + effectiveCo2EmissionsTPerYr: real( + "effective_co2_emissions_t_per_yr", + ).notNull(), + effectivePrimaryEnergyIntensityKwhPerM2Yr: integer( + "effective_primary_energy_intensity_kwh_per_m2_yr", + ).notNull(), + + rebaselineReason: rebaselineReasonEnum("rebaseline_reason").notNull(), + + // Interim energy demand (from EPC RHI data; superseded by bill block below once populated) + spaceHeatingKwh: real("space_heating_kwh").notNull(), + waterHeatingKwh: real("water_heating_kwh").notNull(), + + // Bill block — nullable until BillDerivation wiring lands + fuelRatesPeriod: text("fuel_rates_period"), + heatingKwh: real("heating_kwh"), + heatingCostGbp: real("heating_cost_gbp"), + hotWaterKwh: real("hot_water_kwh"), + hotWaterCostGbp: real("hot_water_cost_gbp"), + lightingKwh: real("lighting_kwh"), + lightingCostGbp: real("lighting_cost_gbp"), + appliancesKwh: real("appliances_kwh"), + appliancesCostGbp: real("appliances_cost_gbp"), + cookingKwh: real("cooking_kwh"), + cookingCostGbp: real("cooking_cost_gbp"), + pumpsFansKwh: real("pumps_fans_kwh"), + pumpsFansCostGbp: real("pumps_fans_cost_gbp"), + coolingKwh: real("cooling_kwh"), + coolingCostGbp: real("cooling_cost_gbp"), + standingChargesGbp: real("standing_charges_gbp"), + segCreditGbp: real("seg_credit_gbp"), + totalAnnualBillGbp: real("total_annual_bill_gbp"), + }, ); \ No newline at end of file