mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
883 lines
No EOL
37 KiB
TypeScript
883 lines
No EOL
37 KiB
TypeScript
import {
|
|
bigserial,
|
|
text,
|
|
timestamp,
|
|
pgTable,
|
|
real,
|
|
pgEnum,
|
|
integer,
|
|
boolean,
|
|
smallint,
|
|
bigint,
|
|
uniqueIndex,
|
|
jsonb,
|
|
} from "drizzle-orm/pg-core";
|
|
import { portfolio, PortfolioStatus } from "./portfolio";
|
|
import { InferModel } from "drizzle-orm";
|
|
import { materialTypeEnum } from "./materials";
|
|
import { sql } from "drizzle-orm";
|
|
import { uploadedFiles } from "./uploaded_files";
|
|
|
|
// This is a placeholder for the property schema
|
|
export interface PropertyMeta {
|
|
id: number;
|
|
uprn: number;
|
|
address: string;
|
|
postcode: string;
|
|
hasPreConditionReport: boolean;
|
|
hasRecommendations: boolean;
|
|
createdAt: string;
|
|
propertyType: string;
|
|
builtForm: string;
|
|
localAuthority: string;
|
|
constituency: string;
|
|
numberOfRooms: number;
|
|
yearBuilt: number;
|
|
tenure: string;
|
|
currentEpcRating: string;
|
|
currentSapPoints: number;
|
|
originalSapPoints: number | null;
|
|
updatedAt: string;
|
|
currentValuation: number | null;
|
|
detailsEpc: {
|
|
currentEnergyDemand: number | null;
|
|
co2Emissions: number | null;
|
|
estimated: boolean;
|
|
};
|
|
}
|
|
|
|
export type Rating = "Very good" | "Good" | "Poor" | "Very poor" | "N/A";
|
|
|
|
export interface Feature {
|
|
feature: string;
|
|
description: string;
|
|
rating: Rating;
|
|
}
|
|
|
|
export interface GeneralFeature {
|
|
feature: string;
|
|
description: string | number;
|
|
}
|
|
|
|
export interface NonInstrusiveFeature {
|
|
title: string;
|
|
note: string;
|
|
}
|
|
|
|
export interface ConditionReportData {
|
|
id: number;
|
|
lastUpdated: string;
|
|
fullAddress: string;
|
|
postcode: string;
|
|
currentEpcRating: string;
|
|
inConservationArea: "Yes" | "No" | "Unknown";
|
|
propertyType: string;
|
|
builtForm: string;
|
|
totalFloorArea: number;
|
|
tenure: string;
|
|
retrofitFeatures: Feature[];
|
|
generalFeatures: GeneralFeature[];
|
|
heatingDemand: GeneralFeature[];
|
|
yearBuilt: string;
|
|
}
|
|
|
|
export const PropertyCreationStatus: [string, ...string[]] = [
|
|
"LOADING",
|
|
"READY",
|
|
"ERROR",
|
|
];
|
|
|
|
export const Epc: [string, ...string[]] = ["A", "B", "C", "D", "E", "F", "G"];
|
|
|
|
export const propertyCreationStatusEnum = pgEnum(
|
|
"creation_status",
|
|
PropertyCreationStatus,
|
|
);
|
|
export const epcEnum = pgEnum("epc", Epc);
|
|
export const propertyStatusEnum = pgEnum("status", PortfolioStatus);
|
|
|
|
export const property = pgTable(
|
|
"property",
|
|
{
|
|
id: bigserial("id", { mode: "bigint" }).primaryKey(),
|
|
|
|
portfolioId: bigint("portfolio_id", { mode: "bigint" })
|
|
.notNull()
|
|
.references(() => portfolio.id),
|
|
|
|
creationStatus: propertyCreationStatusEnum("creation_status").notNull(),
|
|
uprn: bigint("uprn", { mode: "bigint" }),
|
|
|
|
landlordPropertyId: text("landlord_property_id"),
|
|
buildingReferenceNumber: bigint("building_reference_number", {
|
|
mode: "bigint",
|
|
}),
|
|
|
|
status: propertyStatusEnum("status"),
|
|
address: text("address"),
|
|
postcode: text("postcode"),
|
|
userInputtedAddress: text("user_inputted_address"),
|
|
userInputtedPostcode: text("user_inputted_postcode"),
|
|
lexiscore: real("lexiscore"),
|
|
|
|
hasPreConditionReport: boolean("has_pre_condition_report"),
|
|
hasRecommendations: boolean("has_recommendations"),
|
|
|
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
|
|
propertyType: text("property_type"),
|
|
builtForm: text("built_form"),
|
|
localAuthority: text("local_authority"),
|
|
constituency: text("constituency"),
|
|
|
|
numberOfRooms: integer("number_of_rooms"),
|
|
yearBuilt: text("year_built"),
|
|
tenure: text("tenure"),
|
|
|
|
currentEpcRating: epcEnum("current_epc_rating"),
|
|
currentSapPoints: real("current_sap_points"),
|
|
currentValuation: real("current_valuation"),
|
|
|
|
// When we have already installed measures, we will adjust the SAP points to reflect this. We keep a record of
|
|
// 1) The number of points we've adjusted by
|
|
// 2) a flag to indicate whether the SAP points have been adjusted, for easily filtering
|
|
installedMeasuresSapPointAdjustment: real(
|
|
"installed_measures_sap_point_adjustment",
|
|
),
|
|
isSapPointsAdjustedForInstalledMeasures: boolean(
|
|
"is_sap_points_adjusted_for_installed_measures",
|
|
).default(false),
|
|
// To be deprecated
|
|
originalSapPoints: real("original_sap_points"),
|
|
// lodged data
|
|
lodgedSapPoints: real("lodged_sap_points"),
|
|
lodgedEpcRating: epcEnum("lodged_epc_rating"),
|
|
},
|
|
(table) => [
|
|
uniqueIndex("uq_property_portfolio_uprn")
|
|
.on(table.portfolioId, table.uprn)
|
|
.where(sql`${table.uprn} IS NOT NULL`),
|
|
],
|
|
);
|
|
|
|
export const FeatureRating: [string, ...string[]] = [
|
|
"Very good",
|
|
"Good",
|
|
"Poor",
|
|
"Very poor",
|
|
"N/A",
|
|
];
|
|
|
|
export const FeatureRatingNumeric: [number, ...number[]] = [5, 4, 3, 2, 1];
|
|
|
|
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 propertyDetailsSpatial = pgTable(
|
|
"property_details_spatial",
|
|
{
|
|
id: bigserial("id", { mode: "bigint" }).primaryKey(),
|
|
uprn: bigint("uprn", { mode: "bigint" }),
|
|
xCoordinate: real("x_coordinate"),
|
|
yCoordinate: real("y_coordinate"),
|
|
latitude: real("latitude"),
|
|
longitude: real("longitude"),
|
|
conservationStatus: boolean("conservation_status"),
|
|
isListedBuilding: boolean("is_listed_building"),
|
|
isHeritageBuilding: boolean("is_heritage_building"),
|
|
},
|
|
(table) => [uniqueIndex("uq_property_details_spatial_uprn").on(table.uprn)],
|
|
);
|
|
|
|
export const propertyDetailsMeter = pgTable("property_details_meter", {
|
|
id: bigserial("id", { mode: "bigint" }).primaryKey(),
|
|
uprn: bigint("uprn", { mode: "bigint" }),
|
|
energySupplier: text("energy_supplier"),
|
|
gasSupplier: text("gas_supplier"),
|
|
meterReadingTotal: real("meter_reading_total"),
|
|
meterReadingElectricity: real("meter_reading_electricity"),
|
|
meterReadingGas: real("meter_reading_gas"),
|
|
});
|
|
|
|
export const propertyTargets = pgTable("property_targets", {
|
|
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),
|
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
epc: epcEnum("epc"),
|
|
heatDemand: text("heat_demand"),
|
|
});
|
|
|
|
// This is the model for the results of non-invasive surveys, associated with a property.
|
|
// We store the data against a uprn. Each row will contain a metadata about the survey itself
|
|
export const nonInstrusiveSurvey = pgTable("non_intrusive_survey", {
|
|
id: bigserial("id", { mode: "bigint" }).primaryKey(),
|
|
uprn: bigint("uprn", { mode: "bigint" }).notNull(),
|
|
surveyDate: timestamp("survey_date").notNull(),
|
|
surveyor: text("surveyor").notNull(),
|
|
});
|
|
|
|
// This model contains the actual notes that were taken down during the non-invasive survey
|
|
export const nonIntrusiveSurveyNotes = pgTable("non_intrusive_survey_notes", {
|
|
id: bigserial("id", { mode: "bigint" }).primaryKey(),
|
|
surveyId: bigint("survey_id", { mode: "bigint" })
|
|
.notNull()
|
|
.references(() => nonInstrusiveSurvey.id),
|
|
title: text("title").notNull(),
|
|
note: text("note").notNull(),
|
|
});
|
|
|
|
export type Property = InferModel<typeof property, "select">;
|
|
export type PropertyDetailsEpc = InferModel<
|
|
typeof propertyDetailsEpc,
|
|
"select"
|
|
>;
|
|
export type PropertyDetailsSpatial = InferModel<
|
|
typeof propertyDetailsSpatial,
|
|
"select"
|
|
>;
|
|
|
|
// This type is used for the getProperties function in src/app/portfolio/[slug]/utils.ts
|
|
export interface PropertyToRecommendation {
|
|
estimatedCost?: number | null;
|
|
sapPoints?: number | null;
|
|
}
|
|
|
|
export interface PropertyWithRelations extends Record<string, unknown> {
|
|
id: number | string | bigint;
|
|
portfolioId: number | string | bigint;
|
|
address: string | null;
|
|
postcode: string | null;
|
|
status: string | null;
|
|
creationStatus: string | null;
|
|
currentEpcRating: string | null;
|
|
currentSapPoints: number | null;
|
|
targetEpc: string | null;
|
|
planId: number | null;
|
|
fundingScheme: string | null;
|
|
totalRecommendationSapPoints: number | null;
|
|
totalRecommendationCost: number | null;
|
|
// New fields
|
|
landlordPropertyId: string | null;
|
|
originalSapPoints: number | null;
|
|
epcLodgementDate: string | null;
|
|
epcIsExpired: boolean | null;
|
|
// Optional columns (hidden by default)
|
|
propertyType: string | null;
|
|
builtForm: string | null;
|
|
tenure: string | null;
|
|
yearBuilt: string | null;
|
|
totalFloorArea: number | null;
|
|
co2Emissions: number | null;
|
|
mainfuel: string | null;
|
|
lexiscore: number | null;
|
|
}
|
|
|
|
export type NonIntrusiveSurveyNotes = InferModel<
|
|
typeof nonIntrusiveSurveyNotes,
|
|
"select"
|
|
>;
|
|
export type NonInstrusiveSurvey = InferModel<
|
|
typeof nonInstrusiveSurvey,
|
|
"select"
|
|
>;
|
|
export interface NonIntrusiveSurveyData {
|
|
uprn: bigint;
|
|
surveyDate: Date;
|
|
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),
|
|
uploadedFileId: bigint("uploaded_file_id", { mode: "bigint" })
|
|
.unique()
|
|
.references(() => uploadedFiles.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").notNull(),
|
|
addressLine2: text("address_line_2"),
|
|
postTown: text("post_town").notNull(),
|
|
postcode: text("postcode").notNull(),
|
|
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: 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: jsonb("heating_cylinder_size"),
|
|
heatingWaterHeatingCode: integer("heating_water_heating_code"),
|
|
heatingWaterHeatingFuel: integer("heating_water_heating_fuel"),
|
|
heatingImmersionHeatingType: jsonb("heating_immersion_heating_type"),
|
|
heatingCylinderInsulationType: jsonb("heating_cylinder_insulation_type"),
|
|
heatingCylinderThermostat: text("heating_cylinder_thermostat"),
|
|
heatingSecondaryFuelType: integer("heating_secondary_fuel_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: jsonb("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"),
|
|
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(
|
|
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: 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"),
|
|
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: jsonb("wall_construction").notNull(),
|
|
wallInsulationType: jsonb("wall_insulation_type").notNull(),
|
|
wallThicknessMeasured: boolean("wall_thickness_measured").notNull(),
|
|
partyWallConstruction: jsonb("party_wall_construction"),
|
|
buildingPartNumber: integer("building_part_number"),
|
|
wallDryLined: boolean("wall_dry_lined"),
|
|
wallThicknessMm: integer("wall_thickness_mm"),
|
|
wallInsulationThickness: text("wall_insulation_thickness"),
|
|
// age band source
|
|
|
|
// Floor
|
|
floorHeatLoss: integer("floor_heat_loss"),
|
|
floorInsulationThickness: text("floor_insulation_thickness"),
|
|
flatRoofInsulationThickness: jsonb("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: 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"),
|
|
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),
|
|
|
|
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: 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: jsonb("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(),
|
|
},
|
|
);
|
|
|
|
// ─── 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"),
|
|
},
|
|
); |