Merge pull request #443 from Hestia-Homes/feat/epc-persistence-blind-spot-1665

feat(db): persist EPC roof-window, room-in-roof, alt-wall, cylinder & wall-U fields (Model #1665)
This commit is contained in:
KhalimCK 2026-07-23 10:53:02 +01:00 committed by GitHub
commit f6dff52e41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 13637 additions and 0 deletions

View file

@ -0,0 +1,52 @@
CREATE TABLE "epc_roof_window" (
"id" bigserial PRIMARY KEY NOT NULL,
"epc_property_id" bigint NOT NULL,
"roof_window_index" integer NOT NULL,
"area_m2" double precision NOT NULL,
"u_value_raw" double precision NOT NULL,
"orientation" integer NOT NULL,
"pitch_deg" double precision NOT NULL,
"g_perpendicular" double precision NOT NULL,
"frame_factor" double precision NOT NULL,
"glazing_type" integer NOT NULL,
"window_location" jsonb NOT NULL
);
--> statement-breakpoint
CREATE TABLE "epc_room_in_roof" (
"id" bigserial PRIMARY KEY NOT NULL,
"epc_building_part_id" bigint NOT NULL,
"floor_area" double precision NOT NULL,
"construction_age_band" text NOT NULL,
"common_wall_length_m" double precision,
"common_wall_height_m" double precision,
"gable_1_length_m" double precision,
"gable_1_height_m" double precision,
"gable_2_length_m" double precision,
"gable_2_height_m" double precision,
CONSTRAINT "epc_room_in_roof_epc_building_part_id_unique" UNIQUE("epc_building_part_id")
);
--> statement-breakpoint
CREATE TABLE "epc_room_in_roof_surface" (
"id" bigserial PRIMARY KEY NOT NULL,
"epc_room_in_roof_id" bigint NOT NULL,
"surface_index" integer NOT NULL,
"kind" text NOT NULL,
"area_m2" double precision NOT NULL,
"insulation_thickness_mm" integer,
"insulation_type" text,
"u_value" double precision
);
--> statement-breakpoint
ALTER TABLE "epc_building_part" ADD COLUMN "wall_u_value" double precision;--> statement-breakpoint
ALTER TABLE "epc_building_part" ADD COLUMN "alt_wall_1_u_value" double precision;--> statement-breakpoint
ALTER TABLE "epc_building_part" ADD COLUMN "alt_wall_1_thickness_mm" integer;--> statement-breakpoint
ALTER TABLE "epc_building_part" ADD COLUMN "alt_wall_1_is_basement" boolean;--> statement-breakpoint
ALTER TABLE "epc_building_part" ADD COLUMN "alt_wall_2_u_value" double precision;--> statement-breakpoint
ALTER TABLE "epc_building_part" ADD COLUMN "alt_wall_2_thickness_mm" integer;--> statement-breakpoint
ALTER TABLE "epc_building_part" ADD COLUMN "alt_wall_2_is_basement" boolean;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "heating_cylinder_heat_loss" double precision;--> statement-breakpoint
ALTER TABLE "epc_roof_window" ADD CONSTRAINT "epc_roof_window_epc_property_id_epc_property_id_fk" FOREIGN KEY ("epc_property_id") REFERENCES "public"."epc_property"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "epc_room_in_roof" ADD CONSTRAINT "epc_room_in_roof_epc_building_part_id_epc_building_part_id_fk" FOREIGN KEY ("epc_building_part_id") REFERENCES "public"."epc_building_part"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "epc_room_in_roof_surface" ADD CONSTRAINT "epc_room_in_roof_surface_epc_room_in_roof_id_epc_room_in_roof_id_fk" FOREIGN KEY ("epc_room_in_roof_id") REFERENCES "public"."epc_room_in_roof"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "uq_epc_roof_window_property_index" ON "epc_roof_window" USING btree ("epc_property_id","roof_window_index");--> statement-breakpoint
CREATE UNIQUE INDEX "uq_epc_room_in_roof_surface_index" ON "epc_room_in_roof_surface" USING btree ("epc_room_in_roof_id","surface_index");

File diff suppressed because it is too large Load diff

View file

@ -1926,6 +1926,13 @@
"when": 1784656804281,
"tag": "0275_opposite_ronan",
"breakpoints": true
},
{
"idx": 276,
"version": "7",
"when": 1784798917178,
"tag": "0276_romantic_synch",
"breakpoints": true
}
]
}

View file

@ -583,6 +583,7 @@ export const epcProperty = pgTable(
heatingSecondaryHeatingType: jsonb("heating_secondary_heating_type"),
heatingCylinderInsulationThicknessMm: integer("heating_cylinder_insulation_thickness_mm"),
heatingCylinderVolumeMeasuredL: integer("heating_cylinder_volume_measured_l"), // litres, nullable
heatingCylinderHeatLoss: doublePrecision("heating_cylinder_heat_loss"), // nullable
heatingWwhrsIndexNumber1: integer("heating_wwhrs_index_number_1"),
heatingWwhrsIndexNumber2: integer("heating_wwhrs_index_number_2"),
heatingShowerOutletType: jsonb("heating_shower_outlet_type"),
@ -799,6 +800,11 @@ export const epcBuildingPart = pgTable(
// the result: a system-built wall bills as a basement wall, and via has_basement
// drags the whole ground floor onto the Table 23 basement-floor U-value.
wallIsBasement: boolean("wall_is_basement"),
// Lodged gov-EPC wall U-value. Unlike the columns dropped in #1661, the
// calculator now honours this over the derived construction-default (#1665).
// Nullable: null means "not lodged → the calculator derives it". double
// precision to match the other new #1665 floats and keep the round-trip exact.
wallUValue: doublePrecision("wall_u_value"),
// Room in roof (inlined)
roomInRoofFloorArea: real("room_in_roof_floor_area"),
@ -812,6 +818,12 @@ export const epcBuildingPart = pgTable(
altWall1ThicknessMeasured: text("alt_wall_1_thickness_measured"),
altWall1InsulationThickness: text("alt_wall_1_insulation_thickness"),
altWall1IsSheltered: boolean("alt_wall_1_is_sheltered"), // nullable (null when no alt wall)
altWall1UValue: doublePrecision("alt_wall_1_u_value"), // nullable
altWall1ThicknessMm: integer("alt_wall_1_thickness_mm"), // nullable
// NULLABLE, load-bearing — do NOT make it notNull().default(false). null "not
// stated" vs false "explicitly not a basement" are distinct; collapsing them
// re-enables the code-6 basement heuristic this flag exists to defeat.
altWall1IsBasement: boolean("alt_wall_1_is_basement"),
// Alternative wall 2 (inlined)
altWall2Area: real("alt_wall_2_area"),
@ -821,6 +833,10 @@ export const epcBuildingPart = pgTable(
altWall2ThicknessMeasured: text("alt_wall_2_thickness_measured"),
altWall2InsulationThickness: text("alt_wall_2_insulation_thickness"),
altWall2IsSheltered: boolean("alt_wall_2_is_sheltered"), // nullable (null when no alt wall)
altWall2UValue: doublePrecision("alt_wall_2_u_value"), // nullable
altWall2ThicknessMm: integer("alt_wall_2_thickness_mm"), // nullable
// NULLABLE, load-bearing — see altWall1IsBasement above.
altWall2IsBasement: boolean("alt_wall_2_is_basement"),
},
(table) => [
// Per-EPC part lookups: the reporting age-band subquery and the
@ -856,6 +872,84 @@ export const epcFloorDimension = pgTable(
],
);
// ─── epc_roof_window ──────────────────────────────────────────────────────────
// 0..n per epc_property. No explicit onDelete: matches every existing EPC child
// FK (epc_photovoltaic_array, epc_window, …) — the Python backend owns these
// tables and rewrites children by delete-and-reinsert on re-ingest.
export const epcRoofWindow = pgTable(
"epc_roof_window",
{
id: bigserial("id", { mode: "bigint" }).primaryKey(),
epcPropertyId: bigint("epc_property_id", { mode: "bigint" })
.notNull()
.references(() => epcProperty.id),
roofWindowIndex: integer("roof_window_index").notNull(), // preserves list order for round-trip
areaM2: doublePrecision("area_m2").notNull(),
uValueRaw: doublePrecision("u_value_raw").notNull(), // Table 24 roof-window U, pre-curtain
orientation: integer("orientation").notNull(), // SAP orientation code
pitchDeg: doublePrecision("pitch_deg").notNull(),
gPerpendicular: doublePrecision("g_perpendicular").notNull(),
frameFactor: doublePrecision("frame_factor").notNull(),
glazingType: integer("glazing_type").notNull(), // SAP Table U2 code
windowLocation: jsonb("window_location").notNull(), // int (API) or str (site-notes) — jsonb preserves type
},
(t) => [
uniqueIndex("uq_epc_roof_window_property_index").on(t.epcPropertyId, t.roofWindowIndex),
],
);
// ─── epc_room_in_roof ─────────────────────────────────────────────────────────
// 0..1 per epc_building_part. Supersedes the inlined room_in_roof_floor_area /
// room_in_roof_construction_age_band columns on epc_building_part, which are left
// in place here and dropped in a follow-up migration (see Model #1665).
export const epcRoomInRoof = pgTable(
"epc_room_in_roof",
{
id: bigserial("id", { mode: "bigint" }).primaryKey(),
epcBuildingPartId: bigint("epc_building_part_id", { mode: "bigint" })
.notNull()
.unique() // 0..1 per part
.references(() => epcBuildingPart.id),
floorArea: doublePrecision("floor_area").notNull(),
constructionAgeBand: text("construction_age_band").notNull(),
commonWallLengthM: doublePrecision("common_wall_length_m"), // nullable
commonWallHeightM: doublePrecision("common_wall_height_m"), // nullable
gable1LengthM: doublePrecision("gable_1_length_m"), // nullable
gable1HeightM: doublePrecision("gable_1_height_m"), // nullable
gable2LengthM: doublePrecision("gable_2_length_m"), // nullable
gable2HeightM: doublePrecision("gable_2_height_m"), // nullable
},
);
// ─── epc_room_in_roof_surface ─────────────────────────────────────────────────
export const epcRoomInRoofSurface = pgTable(
"epc_room_in_roof_surface",
{
id: bigserial("id", { mode: "bigint" }).primaryKey(),
epcRoomInRoofId: bigint("epc_room_in_roof_id", { mode: "bigint" })
.notNull()
.references(() => epcRoomInRoof.id),
surfaceIndex: integer("surface_index").notNull(), // preserves list order
// slope|flat_ceiling|stud_wall|gable_wall|gable_wall_external|gable_wall_sheltered
// |common_wall|connected_wall — TEXT, not a pg enum (the set has grown twice).
kind: text("kind").notNull(),
areaM2: doublePrecision("area_m2").notNull(), // CAN BE NEGATIVE (signed §3.9.2 adjustment) — no CHECK >= 0
// NULLABLE — null vs 0 is the U-value branch (Table 17 vs default).
insulationThicknessMm: integer("insulation_thickness_mm"),
insulationType: text("insulation_type"), // nullable: mineral_wool|eps|pur|pir
uValue: doublePrecision("u_value"), // NULLABLE — assessor U override; null ≠ 0
},
(t) => [
uniqueIndex("uq_epc_room_in_roof_surface_index").on(t.epcRoomInRoofId, t.surfaceIndex),
],
);
// ─── epc_window ───────────────────────────────────────────────────────────────
export const epcWindow = pgTable(