mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-27 22:45:03 +00:00
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:
commit
f6dff52e41
4 changed files with 13637 additions and 0 deletions
52
src/app/db/migrations/0276_romantic_synch.sql
Normal file
52
src/app/db/migrations/0276_romantic_synch.sql
Normal 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");
|
||||
13484
src/app/db/migrations/meta/0276_snapshot.json
Normal file
13484
src/app/db/migrations/meta/0276_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1926,6 +1926,13 @@
|
|||
"when": 1784656804281,
|
||||
"tag": "0275_opposite_ronan",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 276,
|
||||
"version": "7",
|
||||
"when": 1784798917178,
|
||||
"tag": "0276_romantic_synch",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue