From 2960b99c8b5e8d2aeef71b64c5598899cfb1e92c Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 23 Apr 2026 15:49:53 +0000 Subject: [PATCH] tables for new epc data --- src/app/db/schema/property.ts | 482 +++++++++++++++++++++++++++++++++- 1 file changed, 481 insertions(+), 1 deletion(-) diff --git a/src/app/db/schema/property.ts b/src/app/db/schema/property.ts index 58e956f..cceb8af 100644 --- a/src/app/db/schema/property.ts +++ b/src/app/db/schema/property.ts @@ -162,7 +162,120 @@ export const FeatureRating: [string, ...string[]] = [ "Poor", "Very poor", "N/A", -]; +];export const propertyDetailsEpc = pgTable( + "property_details_epc", + { + id: bigserial("id", { mode: "bigint" }).primaryKey(), + propertyId: bigint("property_id", { mode: "bigint" }) + .notNull() + .references(() => property.id), + portfolioId: bigint("portfolio_id", { mode: "bigint" }) + .notNull() + .references(() => portfolio.id), + fullAddress: text("full_address"), + // Date the EPC was lodged + lodgementDate: timestamp("lodgement_date"), + isExpired: boolean("is_expired"), + totalFloorArea: real("total_floor_area"), + walls: text("walls"), + wallsRating: smallint("walls_rating"), + roof: text("roof"), + roofRating: smallint("roof_rating"), + floor: text("floor"), + floorRating: smallint("floor_rating"), + windows: text("windows"), + windowsRating: smallint("windows_rating"), + heating: text("heating"), + heatingRating: smallint("heating_rating"), + heatingControls: text("heating_controls"), + heatingControlsRating: smallint("heating_controls_rating"), + hotWater: text("hot_water"), + hotWaterRating: smallint("hot_water_rating"), + lighting: text("lighting"), + lightingRating: smallint("lighting_rating"), + mainfuel: text("mainfuel"), + ventilation: text("ventilation"), + solarPv: real("solar_pv"), + solarHotWater: boolean("solar_hot_water"), + windTurbine: smallint("wind_turbine"), + floorHeight: real("floor_height"), + numberHeatedRooms: integer("number_heated_rooms"), + heatLossCorridor: boolean("heat_loss_corridor"), + unheatedCorridorLength: real("unheated_corridor_length"), + numberOpenFireplaces: integer("number_of_open_fireplaces"), + numberExtensions: integer("number_of_extensions"), + numberStoreys: integer("number_of_storeys"), + mainsGas: boolean("mains_gas"), + energyTariff: text("energy_tariff"), + // This is heat demand + primaryEnergyConsumption: real("primary_energy_consumption"), + co2Emissions: real("co2_emissions"), + // Bad naming but currentEnergyDemand is the current kwh consumption - needs to be renamed + currentEnergyDemand: real("current_energy_demand"), + currentEnergyDemandHeatingHotwater: real( + "current_energy_demand_heating_hotwater", + ), + estimated: boolean("estimated").default(false), + // We indicate if the property has an overwritten SAP 05 EPC. I.e. there is a valid EPC, however it's a SAP 05 + // EPC which isn't particularly useful. This value is defaulted to False + sap05Overwritten: boolean("sap_05_overwritten").default(false), + // When we've overwritten a SAP 05 EPC, we store the SAP 05 score and rating here for reference + sap05Score: real("sap_05_score"), + sap05EpcRating: epcEnum("sap_05_epc_rating"), + // Include current estimates for energy bills, across the different types of energy + // These predictions are based on the EPC predicted consumptions + current energy prices + heatingEnergyCostCurrent: real("heating_cost_current"), + hotWaterEnergyCostCurrent: real("hot_water_cost_current"), + lightingEnergyCostCurrent: real("lighting_cost_current"), + appliancesEnergyCostCurrent: real("appliances_cost_current"), + gasStandingCharge: real("gas_standing_charge"), + electricityStandingCharge: real("electricity_standing_charge"), + + // When we have already installed measures, we will adjust the carbon, bills, kwh, heat demandto reflect this. We keep a record of + // 1) The adjustments + // 2) original values + // 3) a flag to indicate whether the values have been adjusted, for easily filtering + + // original values - we don't need bills because we don't actually adjust any of the originals we just subtract adjustments from current values + // TODO - deprecate + originalCo2Emissions: real("original_co2_emissions"), + originalPrimaryEnergyConsumption: real( + "original_primary_energy_consumption", + ), + originalCurrentEnergyDemand: real("original_current_energy_demand"), + originalCurrentEnergyDemandHeatingHotwater: real( + "original_current_energy_demand_heating_hotwater", + ), + + // adjustment quantities - TODO: deprecate + installedMeasuresCo2Adjustment: real("installed_measures_co2_adjustment"), + installedMeasuresEnergyDemandAdjustment: real( + "installed_measures_energy_demand_adjustment", + ), + installedMeasuresTotalEnergyBillAdjustment: real( + "installed_measures_total_energy_bill_adjustment", + ), + installedMeasuresHeatDemandAdjustment: real( + "installed_measures_heat_demand_adjustment", + ), + isEpcAdjustedForInstalledMeasures: boolean( + "is_epc_adjusted_for_installed_measures", + ).default(false), + + // Lodged values + lodgedCo2Emissions: real("lodged_co2_emissions"), + lodgedHeatDemand: real("lodged_heat_demand"), + hasBeenRemodelled: boolean("has_been_remodelled").default(false), + // additional fields + environment_impact_current: real("environment_impact_current"), + }, + (table) => [ + uniqueIndex("uq_property_details_epc_property_portfolio").on( + table.propertyId, + table.portfolioId, + ), + ], +); export const FeatureRatingNumeric: [number, ...number[]] = [5, 4, 3, 2, 1]; @@ -398,3 +511,370 @@ export interface NonIntrusiveSurveyData { surveyor: string; notes: { title: string; note: string }[]; } + +// ─── Enums ──────────────────────────────────────────────────────────────────── + +export const energyElementTypeEnum = pgEnum("energy_element_type", [ + "roof", "wall", "floor", "main_heating", "window", + "lighting", "hot_water", "secondary_heating", "main_heating_controls", +]); + +// ─── epc_property ───────────────────────────────────────────────────────────── + +export const epcProperty = pgTable( + "epc_property", + { + id: bigserial("id", { mode: "bigint" }).primaryKey(), + propertyId: bigint("property_id", { mode: "bigint" }) + .notNull() + .references(() => property.id), + portfolioId: bigint("portfolio_id", { mode: "bigint" }) + .notNull() + .references(() => portfolio.id), + + // Identity / admin + uprn: bigint("uprn", { mode: "bigint" }), + uprnSource: text("uprn_source"), + reportReference: text("report_reference"), + reportType: text("report_type"), + assessmentType: text("assessment_type"), + sapVersion: real("sap_version"), + schemaType: text("schema_type"), + schemaVersionsOriginal: text("schema_versions_original"), + status: text("status"), + calculationSoftwareVersion: text("calculation_software_version"), + + // Address + addressLine1: text("address_line_1"), + addressLine2: text("address_line_2"), + postTown: text("post_town"), + postcode: text("postcode"), + regionCode: text("region_code"), + countryCode: text("country_code"), + languageCode: text("language_code"), + + // Property description + dwellingType: text("dwelling_type").notNull(), + propertyType: text("property_type"), + builtForm: text("built_form"), + tenure: text("tenure").notNull(), + transactionType: text("transaction_type").notNull(), + + // Dates + inspectionDate: timestamp("inspection_date").notNull(), + completionDate: timestamp("completion_date"), + registrationDate: timestamp("registration_date"), + + // Measurements + totalFloorAreaM2: real("total_floor_area_m2").notNull(), + measurementType: integer("measurement_type"), + + // Flags + solarWaterHeating: boolean("solar_water_heating").notNull(), + hasHotWaterCylinder: boolean("has_hot_water_cylinder").notNull(), + hasFixedAirConditioning: boolean("has_fixed_air_conditioning").notNull(), + hasConservatory: boolean("has_conservatory"), + hasHeatedSeparateConservatory: boolean("has_heated_separate_conservatory"), + conservatoryType: integer("conservatory_type"), + + // Counts + doorCount: integer("door_count").notNull(), + wetRoomsCount: integer("wet_rooms_count").notNull(), + extensionsCount: integer("extensions_count").notNull(), + heatedRoomsCount: integer("heated_rooms_count").notNull(), + openChimneysCount: integer("open_chimneys_count").notNull(), + habitableRoomsCount: integer("habitable_rooms_count").notNull(), + insulatedDoorCount: integer("insulated_door_count").notNull(), + cflFixedLightingBulbsCount: integer("cfl_fixed_lighting_bulbs_count").notNull(), + ledFixedLightingBulbsCount: integer("led_fixed_lighting_bulbs_count").notNull(), + incandescentFixedLightingBulbsCount: integer("incandescent_fixed_lighting_bulbs_count").notNull(), + blockedChimneysCount: integer("blocked_chimneys_count"), + draughtproofedDoorCount: integer("draughtproofed_door_count"), + energyRatingAverage: integer("energy_rating_average"), + lowEnergyFixedLightingBulbsCount: integer("low_energy_fixed_lighting_bulbs_count"), + fixedLightingOutletsCount: integer("fixed_lighting_outlets_count"), + lowEnergyFixedLightingOutletsCount: integer("low_energy_fixed_lighting_outlets_count"), + numberOfStoreys: integer("number_of_storeys"), + anyUnheatedRooms: boolean("any_unheated_rooms"), + + // Misc + hydro: boolean("hydro"), + photovoltaicArray: boolean("photovoltaic_array"), + wasteWaterHeatRecovery: text("waste_water_heat_recovery"), + pressureTest: integer("pressure_test"), + pressureTestCertificateNumber: integer("pressure_test_certificate_number"), + percentDraughtproofed: integer("percent_draughtproofed"), + insulatedDoorUValue: real("insulated_door_u_value"), + multipleGlazedProportion: integer("multiple_glazed_proportion"), + windowsTransmissionUValue: real("windows_transmission_u_value"), + windowsTransmissionDataSource: integer("windows_transmission_data_source"), + windowsTransmissionSolarTransmittance: real("windows_transmission_solar_transmittance"), + + // Energy source + energyMainsGas: boolean("energy_mains_gas").notNull(), + energyMeterType: text("energy_meter_type").notNull(), + energyPvBatteryCount: integer("energy_pv_battery_count").notNull(), + energyWindTurbinesCount: integer("energy_wind_turbines_count").notNull(), + energyGasSmartMeterPresent: boolean("energy_gas_smart_meter_present").notNull(), + 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"), + 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"), + heatingWaterHeatingCode: integer("heating_water_heating_code"), + heatingWaterHeatingFuel: integer("heating_water_heating_fuel"), + heatingImmersionHeatingType: text("heating_immersion_heating_type"), + heatingCylinderInsulationType: text("heating_cylinder_insulation_type"), + heatingCylinderThermostat: text("heating_cylinder_thermostat"), + heatingSecondaryFuelType: integer("heating_secondary_fuel_type"), + heatingSecondaryHeatingType: text("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"), + heatingShowerWwhrs: integer("heating_shower_wwhrs"), + + // Ventilation + ventilationType: text("ventilation_type"), + ventilationDraughtLobby: boolean("ventilation_draught_lobby"), + ventilationPressureTest: text("ventilation_pressure_test"), + ventilationOpenFluesCount: integer("ventilation_open_flues_count"), + ventilationClosedFluesCount: integer("ventilation_closed_flues_count"), + ventilationBoilerFluesCount: integer("ventilation_boiler_flues_count"), + ventilationOtherFluesCount: integer("ventilation_other_flues_count"), + ventilationExtractFansCount: integer("ventilation_extract_fans_count"), + ventilationPassiveVentsCount: integer("ventilation_passive_vents_count"), + ventilationFluelessGasFiresCount: integer("ventilation_flueless_gas_fires_count"), + ventilationInPcdfDatabase: boolean("ventilation_in_pcdf_database"), + mechanicalVentilation: integer("mechanical_ventilation"), + mechanicalVentDuctType: integer("mechanical_vent_duct_type"), + mechanicalVentDuctPlacement: integer("mechanical_vent_duct_placement"), + mechanicalVentDuctInsulation: integer("mechanical_vent_duct_insulation"), + mechanicalVentilationIndexNumber: integer("mechanical_ventilation_index_number"), + mechanicalVentMeasuredInstallation: text("mechanical_vent_measured_installation"), + }, + (table) => [ + uniqueIndex("uq_epc_property_property_portfolio").on( + table.propertyId, + table.portfolioId, + ), + ], +); + +// ─── epc_property_energy_performance ───────────────────────────────────────── + +export const epcPropertyEnergyPerformance = pgTable( + "epc_property_energy_performance", + { + id: bigserial("id", { mode: "bigint" }).primaryKey(), + epcPropertyId: bigint("epc_property_id", { mode: "bigint" }) + .notNull() + .unique() + .references(() => epcProperty.id), + + // Current + energyRatingCurrent: integer("energy_rating_current"), + energyConsumptionCurrent: integer("energy_consumption_current"), + environmentalImpactCurrent: integer("environmental_impact_current"), + heatingCostCurrent: real("heating_cost_current"), + lightingCostCurrent: real("lighting_cost_current"), + hotWaterCostCurrent: real("hot_water_cost_current"), + co2EmissionsCurrent: real("co2_emissions_current"), + co2EmissionsCurrentPerFloorArea: integer("co2_emissions_current_per_floor_area"), + currentEnergyEfficiencyBand: text("current_energy_efficiency_band"), + + // Potential + energyRatingPotential: real("energy_rating_potential"), + energyConsumptionPotential: integer("energy_consumption_potential"), + environmentalImpactPotential: integer("environmental_impact_potential"), + heatingCostPotential: real("heating_cost_potential"), + lightingCostPotential: real("lighting_cost_potential"), + hotWaterCostPotential: real("hot_water_cost_potential"), + co2EmissionsPotential: real("co2_emissions_potential"), + potentialEnergyEfficiencyBand: text("potential_energy_efficiency_band"), + }, +); + +// ─── epc_flat_details ───────────────────────────────────────────────────────── + +export const epcFlatDetails = pgTable( + "epc_flat_details", + { + id: bigserial("id", { mode: "bigint" }).primaryKey(), + epcPropertyId: bigint("epc_property_id", { mode: "bigint" }) + .notNull() + .unique() + .references(() => epcProperty.id), + + level: integer("level").notNull(), + topStorey: text("top_storey").notNull(), + flatLocation: integer("flat_location").notNull(), + heatLossCorridor: integer("heat_loss_corridor").notNull(), + storeyCount: integer("storey_count"), + unheatedCorridorLengthM: integer("unheated_corridor_length_m"), + }, +); + +// ─── epc_main_heating_detail ────────────────────────────────────────────────── + +export const epcMainHeatingDetail = pgTable( + "epc_main_heating_detail", + { + id: bigserial("id", { mode: "bigint" }).primaryKey(), + epcPropertyId: bigint("epc_property_id", { mode: "bigint" }) + .notNull() + .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(), + fanFluePresent: boolean("fan_flue_present"), + boilerFlueType: integer("boiler_flue_type"), + boilerIgnitionType: integer("boiler_ignition_type"), + centralHeatingPumpAge: integer("central_heating_pump_age"), + centralHeatingPumpAgeStr: text("central_heating_pump_age_str"), + mainHeatingIndexNumber: integer("main_heating_index_number"), + sapMainHeatingCode: integer("sap_main_heating_code"), + mainHeatingNumber: integer("main_heating_number"), + mainHeatingCategory: integer("main_heating_category"), + mainHeatingFraction: integer("main_heating_fraction"), + mainHeatingDataSource: integer("main_heating_data_source"), + condensing: boolean("condensing"), + weatherCompensator: boolean("weather_compensator"), + }, +); + +// ─── epc_building_part ──────────────────────────────────────────────────────── + +export const epcBuildingPart = pgTable( + "epc_building_part", + { + id: bigserial("id", { mode: "bigint" }).primaryKey(), + epcPropertyId: bigint("epc_property_id", { mode: "bigint" }) + .notNull() + .references(() => epcProperty.id), + + identifier: text("identifier").notNull(), + constructionAgeBand: text("construction_age_band").notNull(), + + // Wall + wallConstruction: text("wall_construction").notNull(), + wallInsulationType: text("wall_insulation_type").notNull(), + wallThicknessMeasured: boolean("wall_thickness_measured").notNull(), + partyWallConstruction: text("party_wall_construction").notNull(), + buildingPartNumber: integer("building_part_number"), + wallDryLined: boolean("wall_dry_lined"), + wallThicknessMm: integer("wall_thickness_mm"), + wallInsulationThickness: text("wall_insulation_thickness"), + + // Floor + floorHeatLoss: integer("floor_heat_loss"), + floorInsulationThickness: text("floor_insulation_thickness"), + flatRoofInsulationThickness: text("flat_roof_insulation_thickness"), + floorType: text("floor_type"), + floorConstructionType: text("floor_construction_type"), + floorInsulationTypeStr: text("floor_insulation_type_str"), + floorUValueKnown: boolean("floor_u_value_known"), + + // Roof + roofConstruction: integer("roof_construction"), + roofInsulationLocation: text("roof_insulation_location"), + roofInsulationThickness: text("roof_insulation_thickness"), + + // Room in roof (inlined) + roomInRoofFloorArea: real("room_in_roof_floor_area"), + roomInRoofConstructionAgeBand: text("room_in_roof_construction_age_band"), + + // Alternative wall 1 (inlined) + altWall1Area: real("alt_wall_1_area"), + altWall1DryLined: text("alt_wall_1_dry_lined"), + altWall1Construction: integer("alt_wall_1_construction"), + altWall1InsulationType: integer("alt_wall_1_insulation_type"), + altWall1ThicknessMeasured: text("alt_wall_1_thickness_measured"), + altWall1InsulationThickness: text("alt_wall_1_insulation_thickness"), + + // Alternative wall 2 (inlined) + altWall2Area: real("alt_wall_2_area"), + altWall2DryLined: text("alt_wall_2_dry_lined"), + altWall2Construction: integer("alt_wall_2_construction"), + altWall2InsulationType: integer("alt_wall_2_insulation_type"), + altWall2ThicknessMeasured: text("alt_wall_2_thickness_measured"), + altWall2InsulationThickness: text("alt_wall_2_insulation_thickness"), + }, +); + +// ─── epc_floor_dimension ────────────────────────────────────────────────────── + +export const epcFloorDimension = pgTable( + "epc_floor_dimension", + { + id: bigserial("id", { mode: "bigint" }).primaryKey(), + epcBuildingPartId: bigint("epc_building_part_id", { mode: "bigint" }) + .notNull() + .references(() => epcBuildingPart.id), + + floor: integer("floor"), + roomHeightM: real("room_height_m").notNull(), + totalFloorAreaM2: real("total_floor_area_m2").notNull(), + partyWallLengthM: real("party_wall_length_m").notNull(), + heatLossPerimeterM: real("heat_loss_perimeter_m").notNull(), + floorInsulation: integer("floor_insulation"), + floorConstruction: integer("floor_construction"), + }, +); + +// ─── epc_window ─────────────────────────────────────────────────────────────── + +export const epcWindow = pgTable( + "epc_window", + { + id: bigserial("id", { mode: "bigint" }).primaryKey(), + epcPropertyId: bigint("epc_property_id", { mode: "bigint" }) + .notNull() + .references(() => epcProperty.id), + + pvcFrame: text("pvc_frame").notNull(), + glazingGap: text("glazing_gap").notNull(), + orientation: text("orientation").notNull(), + windowType: text("window_type").notNull(), + glazingType: text("glazing_type").notNull(), + windowWidth: real("window_width").notNull(), + windowHeight: real("window_height").notNull(), + draughtProofed: boolean("draught_proofed").notNull(), + windowLocation: text("window_location").notNull(), + windowWallType: text("window_wall_type").notNull(), + permanentShuttersPresent: boolean("permanent_shutters_present").notNull(), + frameFactor: real("frame_factor"), + permanentShuttersInsulated: text("permanent_shutters_insulated"), + + // Transmission details (inlined) + transmissionUValue: real("transmission_u_value"), + transmissionDataSource: integer("transmission_data_source"), + transmissionSolarTransmittance: real("transmission_solar_transmittance"), + }, +); + +// ─── epc_energy_element ─────────────────────────────────────────────────────── + +export const epcEnergyElement = pgTable( + "epc_energy_element", + { + id: bigserial("id", { mode: "bigint" }).primaryKey(), + epcPropertyId: bigint("epc_property_id", { mode: "bigint" }) + .notNull() + .references(() => epcProperty.id), + + elementType: energyElementTypeEnum("element_type").notNull(), + description: text("description").notNull(), + energyEfficiencyRating: integer("energy_efficiency_rating").notNull(), + environmentalEfficiencyRating: integer("environmental_efficiency_rating").notNull(), + }, +); \ No newline at end of file