Merge pull request #304 from Hestia-Homes/feature/pashub-fetcher-migration

Migration for changes to EpcProperty, new property baseline table, and additional file type
This commit is contained in:
Daniel Roth 2026-06-03 12:30:42 +01:00 committed by GitHub
commit 41c5b127c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 11293 additions and 26 deletions

View file

@ -74,6 +74,20 @@ See [ADR-0001](./docs/adr/0001-bulk-upload-state-machine.md) for the deliberate
- A **BulkUpload** has at most one **Task** (the orchestration handle for the FastAPI pipeline run); a Task has many **SubTasks** (one per pipeline stage: address matching, combiner).
- A **Portfolio** has many **VocabularyMappings** — one row per `(category, description)` it has ever encountered across all its BulkUploads. See [ADR-0002](./docs/adr/0002-landlord-override-vocabulary.md).
### Baseline performance
**Lodged performance**:
The SAP score, EPC band, CO₂ emissions, and primary energy intensity as submitted to the government EPC register. Ground truth from the register; never modified.
_Avoid_: original performance, registered performance
**Effective performance**:
The SAP score (and associated metrics) that the modelling engine actually uses as its baseline. Usually equals Lodged performance, but differs when a Landlord override or data-quality issue makes the lodged certificate unreliable — triggering a Rebaseline.
_Avoid_: current performance, adjusted performance
**Rebaseline**:
The act of substituting a corrected set of performance metrics in place of the Lodged values. Recorded on `property_baseline_performance` with a `rebaseline_reason` enum value: `none`, `pre_sap10`, `physical_state_changed`, or `both`.
_Avoid_: override, adjustment, correction
## Example dialogue
> **Dev:** "If the **Combiner** finishes but the user hasn't clicked Finalise, what does the user see?"

View file

@ -0,0 +1,291 @@
# Drizzle schema handoff — pending EPC migrations
**Task:** Update Drizzle table definitions in `src/app/db/schema/property.ts` to match the
Python SQLModel definitions. Do **not** run `drizzle-kit generate` or any migration
commands — the developer will run generation manually after your changes.
Two sets of changes are covered here:
1. EPC property round-trip fidelity gaps (fixes an active production error)
2. New `property_baseline_performance` table
### Before starting: update the import line
`jsonb` is not currently imported. Add it (and `pgEnum` is already present):
```typescript
import {
bigserial,
text,
timestamp,
pgTable,
real,
pgEnum,
integer,
boolean,
smallint,
bigint,
uniqueIndex,
jsonb, // ← add this
} from "drizzle-orm/pg-core";
```
---
## 1. `epc_property` — new columns
Add these columns to the `epcProperty` table. All are nullable (no `.notNull()`).
```typescript
// Mechanical ventilation
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"),
```
---
## 2. `epc_property` — type changes: `text``jsonb`
Change the following existing columns from `text(...)` to `jsonb(...)`. Preserve any
`.notNull()` that is currently present (none of these have it, but double-check).
| Property name | Column name |
| ------------------------------- | ---------------------------------- |
| `energyPvConnection` | `energy_pv_connection` |
| `heatingCylinderSize` | `heating_cylinder_size` |
| `heatingImmersionHeatingType` | `heating_immersion_heating_type` |
| `heatingCylinderInsulationType` | `heating_cylinder_insulation_type` |
| `heatingSecondaryHeatingType` | `heating_secondary_heating_type` |
| `heatingShowerOutletType` | `heating_shower_outlet_type` |
Example — before:
```typescript
heatingCylinderSize: text("heating_cylinder_size"),
```
After:
```typescript
heatingCylinderSize: jsonb("heating_cylinder_size"),
```
---
## 3. `epc_main_heating_detail` — type changes: `text``jsonb`
These four columns are currently `text(...).notNull()`. Change to `jsonb(...).notNull()`.
| Property name | Column name |
| -------------------- | ---------------------- |
| `mainFuelType` | `main_fuel_type` |
| `heatEmitterType` | `heat_emitter_type` |
| `emitterTemperature` | `emitter_temperature` |
| `mainHeatingControl` | `main_heating_control` |
---
## 4. `epc_building_part` — type changes and new columns
### 4a. Type changes: `text``jsonb`
| Property name | Column name | Currently nullable? |
| ----------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------- |
| `wallConstruction` | `wall_construction` | no (`.notNull()`) |
| `wallInsulationType` | `wall_insulation_type` | no (`.notNull()`) |
| `partyWallConstruction` | `party_wall_construction` | **drop `.notNull()`** — Python has this as nullable; the TypeScript `.notNull()` is wrong |
| `flatRoofInsulationThickness` | `flat_roof_insulation_thickness` | yes |
| `roofInsulationLocation` | `roof_insulation_location` | yes |
| `roofInsulationThickness` | `roof_insulation_thickness` | yes |
### 4b. New columns (add, nullable)
```typescript
roofConstructionType: text("roof_construction_type"),
curtainWallAge: text("curtain_wall_age"),
```
---
## 5. `epc_window` — type changes: `text``jsonb`
| Property name | Column name | Currently nullable? |
| -------------------------- | ---------------------------- | ---------------------------------------------------------- |
| `glazingGap` | `glazing_gap` | no (`.notNull()`) |
| `orientation` | `orientation` | no (`.notNull()`) |
| `windowType` | `window_type` | no (`.notNull()`) |
| `glazingType` | `glazing_type` | no (`.notNull()`) |
| `windowLocation` | `window_location` | no (`.notNull()`) |
| `windowWallType` | `window_wall_type` | no (`.notNull()`) |
| `draughtProofed` | `draught_proofed` | no (`.notNull()`) — currently `boolean`, change to `jsonb` |
| `permanentShuttersPresent` | `permanent_shutters_present` | no (`.notNull()`) — currently `boolean`, change to `jsonb` |
| `transmissionDataSource` | `transmission_data_source` | yes |
> **Note on `draughtProofed` and `permanentShuttersPresent`:** these are `boolean` in the
> current TypeScript schema but `Union[bool, str]` JSONB in the Python model. Change them
> to `jsonb(...).notNull()` — the TypeScript boolean type was incorrect.
>
> These two columns also require **boolean-specific** USING clauses in the generated migration
> (PostgreSQL will not implicitly cast `boolean` to `jsonb`):
>
> ```sql
> ALTER TABLE "epc_window" ALTER COLUMN "draught_proofed"
> SET DATA TYPE jsonb
> USING to_json("draught_proofed")::jsonb;
>
> ALTER TABLE "epc_window" ALTER COLUMN "permanent_shutters_present"
> SET DATA TYPE jsonb
> USING to_json("permanent_shutters_present")::jsonb;
> ```
>
> `to_json` converts `true`/`false` to JSON booleans (not quoted strings), which is correct
> for the `Union[bool, str]` Python type.
---
## 6. New table: `epc_renewable_heat_incentive`
Add this table to `src/app/db/schema/property.ts`:
```typescript
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"),
},
);
```
---
## 7. New table: `property_baseline_performance`
First, add the enum (before the table definition):
```typescript
export const rebaselineReasonEnum = pgEnum("rebaseline_reason", [
"none",
"pre_sap10",
"physical_state_changed",
"both",
]);
```
Then add the table to `src/app/db/schema/property.ts`:
```typescript
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"),
},
);
```
---
## Post-generation checklist (developer action, not Claude)
After running `drizzle-kit generate`, **manually edit the generated `.sql` file** before
applying it. For every `ALTER COLUMN ... SET DATA TYPE jsonb` statement, add a `USING`
expression to handle existing rows safely. Without it, any row with a bare unquoted
string (e.g. `Electric Shower`) will cause the migration to fail.
Replace the generated form:
```sql
ALTER TABLE "epc_property" ALTER COLUMN "heating_shower_outlet_type" SET DATA TYPE jsonb;
```
With:
```sql
ALTER TABLE "epc_property" ALTER COLUMN "heating_shower_outlet_type"
SET DATA TYPE jsonb
USING (
CASE
WHEN "heating_shower_outlet_type" IS NULL THEN NULL
WHEN "heating_shower_outlet_type" ~ '^-?[0-9]+$' THEN "heating_shower_outlet_type"::jsonb
ELSE to_json("heating_shower_outlet_type")::jsonb
END
);
```
Apply this pattern to **every** `text → jsonb` column across all four tables
(`epc_property`, `epc_main_heating_detail`, `epc_building_part`, `epc_window`).

View file

@ -0,0 +1,281 @@
CREATE TYPE "public"."rebaseline_reason" AS ENUM('none', 'pre_sap10', 'physical_state_changed', 'both');--> statement-breakpoint
ALTER TYPE "public"."file_type" ADD VALUE 'other';--> statement-breakpoint
CREATE TABLE "epc_renewable_heat_incentive" (
"id" bigserial PRIMARY KEY NOT NULL,
"epc_property_id" bigint NOT NULL,
"space_heating_kwh" real NOT NULL,
"water_heating_kwh" real NOT NULL,
"impact_of_loft_insulation_kwh" real,
"impact_of_cavity_insulation_kwh" real,
"impact_of_solid_wall_insulation_kwh" real,
CONSTRAINT "epc_renewable_heat_incentive_epc_property_id_unique" UNIQUE("epc_property_id")
);
--> statement-breakpoint
CREATE TABLE "property_baseline_performance" (
"id" bigserial PRIMARY KEY NOT NULL,
"property_id" bigint NOT NULL,
"lodged_sap_score" integer NOT NULL,
"lodged_epc_band" "epc" NOT NULL,
"lodged_co2_emissions_t_per_yr" real NOT NULL,
"lodged_primary_energy_intensity_kwh_per_m2_yr" integer NOT NULL,
"effective_sap_score" integer NOT NULL,
"effective_epc_band" "epc" NOT NULL,
"effective_co2_emissions_t_per_yr" real NOT NULL,
"effective_primary_energy_intensity_kwh_per_m2_yr" integer NOT NULL,
"rebaseline_reason" "rebaseline_reason" NOT NULL,
"space_heating_kwh" real NOT NULL,
"water_heating_kwh" real NOT NULL,
"fuel_rates_period" text,
"heating_kwh" real,
"heating_cost_gbp" real,
"hot_water_kwh" real,
"hot_water_cost_gbp" real,
"lighting_kwh" real,
"lighting_cost_gbp" real,
"appliances_kwh" real,
"appliances_cost_gbp" real,
"cooking_kwh" real,
"cooking_cost_gbp" real,
"pumps_fans_kwh" real,
"pumps_fans_cost_gbp" real,
"cooling_kwh" real,
"cooling_cost_gbp" real,
"standing_charges_gbp" real,
"seg_credit_gbp" real,
"total_annual_bill_gbp" real,
CONSTRAINT "property_baseline_performance_property_id_unique" UNIQUE("property_id")
);
--> statement-breakpoint
ALTER TABLE "epc_building_part" ALTER COLUMN "wall_construction"
SET DATA TYPE jsonb
USING (
CASE
WHEN "wall_construction" IS NULL THEN NULL
WHEN "wall_construction" ~ '^-?[0-9]+$' THEN "wall_construction"::jsonb
ELSE to_json("wall_construction")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_building_part" ALTER COLUMN "wall_insulation_type"
SET DATA TYPE jsonb
USING (
CASE
WHEN "wall_insulation_type" IS NULL THEN NULL
WHEN "wall_insulation_type" ~ '^-?[0-9]+$' THEN "wall_insulation_type"::jsonb
ELSE to_json("wall_insulation_type")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_building_part" ALTER COLUMN "party_wall_construction"
SET DATA TYPE jsonb
USING (
CASE
WHEN "party_wall_construction" IS NULL THEN NULL
WHEN "party_wall_construction" ~ '^-?[0-9]+$' THEN "party_wall_construction"::jsonb
ELSE to_json("party_wall_construction")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_building_part" ALTER COLUMN "party_wall_construction" DROP NOT NULL;--> statement-breakpoint
ALTER TABLE "epc_building_part" ALTER COLUMN "flat_roof_insulation_thickness"
SET DATA TYPE jsonb
USING (
CASE
WHEN "flat_roof_insulation_thickness" IS NULL THEN NULL
WHEN "flat_roof_insulation_thickness" ~ '^-?[0-9]+$' THEN "flat_roof_insulation_thickness"::jsonb
ELSE to_json("flat_roof_insulation_thickness")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_building_part" ALTER COLUMN "roof_insulation_location"
SET DATA TYPE jsonb
USING (
CASE
WHEN "roof_insulation_location" IS NULL THEN NULL
WHEN "roof_insulation_location" ~ '^-?[0-9]+$' THEN "roof_insulation_location"::jsonb
ELSE to_json("roof_insulation_location")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_building_part" ALTER COLUMN "roof_insulation_thickness"
SET DATA TYPE jsonb
USING (
CASE
WHEN "roof_insulation_thickness" IS NULL THEN NULL
WHEN "roof_insulation_thickness" ~ '^-?[0-9]+$' THEN "roof_insulation_thickness"::jsonb
ELSE to_json("roof_insulation_thickness")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_main_heating_detail" ALTER COLUMN "main_fuel_type"
SET DATA TYPE jsonb
USING (
CASE
WHEN "main_fuel_type" IS NULL THEN NULL
WHEN "main_fuel_type" ~ '^-?[0-9]+$' THEN "main_fuel_type"::jsonb
ELSE to_json("main_fuel_type")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_main_heating_detail" ALTER COLUMN "heat_emitter_type"
SET DATA TYPE jsonb
USING (
CASE
WHEN "heat_emitter_type" IS NULL THEN NULL
WHEN "heat_emitter_type" ~ '^-?[0-9]+$' THEN "heat_emitter_type"::jsonb
ELSE to_json("heat_emitter_type")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_main_heating_detail" ALTER COLUMN "emitter_temperature"
SET DATA TYPE jsonb
USING (
CASE
WHEN "emitter_temperature" IS NULL THEN NULL
WHEN "emitter_temperature" ~ '^-?[0-9]+$' THEN "emitter_temperature"::jsonb
ELSE to_json("emitter_temperature")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_main_heating_detail" ALTER COLUMN "main_heating_control"
SET DATA TYPE jsonb
USING (
CASE
WHEN "main_heating_control" IS NULL THEN NULL
WHEN "main_heating_control" ~ '^-?[0-9]+$' THEN "main_heating_control"::jsonb
ELSE to_json("main_heating_control")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_property" ALTER COLUMN "energy_pv_connection"
SET DATA TYPE jsonb
USING (
CASE
WHEN "energy_pv_connection" IS NULL THEN NULL
WHEN "energy_pv_connection" ~ '^-?[0-9]+$' THEN "energy_pv_connection"::jsonb
ELSE to_json("energy_pv_connection")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_property" ALTER COLUMN "heating_cylinder_size"
SET DATA TYPE jsonb
USING (
CASE
WHEN "heating_cylinder_size" IS NULL THEN NULL
WHEN "heating_cylinder_size" ~ '^-?[0-9]+$' THEN "heating_cylinder_size"::jsonb
ELSE to_json("heating_cylinder_size")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_property" ALTER COLUMN "heating_immersion_heating_type"
SET DATA TYPE jsonb
USING (
CASE
WHEN "heating_immersion_heating_type" IS NULL THEN NULL
WHEN "heating_immersion_heating_type" ~ '^-?[0-9]+$' THEN "heating_immersion_heating_type"::jsonb
ELSE to_json("heating_immersion_heating_type")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_property" ALTER COLUMN "heating_cylinder_insulation_type"
SET DATA TYPE jsonb
USING (
CASE
WHEN "heating_cylinder_insulation_type" IS NULL THEN NULL
WHEN "heating_cylinder_insulation_type" ~ '^-?[0-9]+$' THEN "heating_cylinder_insulation_type"::jsonb
ELSE to_json("heating_cylinder_insulation_type")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_property" ALTER COLUMN "heating_secondary_heating_type"
SET DATA TYPE jsonb
USING (
CASE
WHEN "heating_secondary_heating_type" IS NULL THEN NULL
WHEN "heating_secondary_heating_type" ~ '^-?[0-9]+$' THEN "heating_secondary_heating_type"::jsonb
ELSE to_json("heating_secondary_heating_type")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_property" ALTER COLUMN "heating_shower_outlet_type"
SET DATA TYPE jsonb
USING (
CASE
WHEN "heating_shower_outlet_type" IS NULL THEN NULL
WHEN "heating_shower_outlet_type" ~ '^-?[0-9]+$' THEN "heating_shower_outlet_type"::jsonb
ELSE to_json("heating_shower_outlet_type")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_window" ALTER COLUMN "glazing_gap"
SET DATA TYPE jsonb
USING (
CASE
WHEN "glazing_gap" IS NULL THEN NULL
WHEN "glazing_gap" ~ '^-?[0-9]+$' THEN "glazing_gap"::jsonb
ELSE to_json("glazing_gap")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_window" ALTER COLUMN "orientation"
SET DATA TYPE jsonb
USING (
CASE
WHEN "orientation" IS NULL THEN NULL
WHEN "orientation" ~ '^-?[0-9]+$' THEN "orientation"::jsonb
ELSE to_json("orientation")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_window" ALTER COLUMN "window_type"
SET DATA TYPE jsonb
USING (
CASE
WHEN "window_type" IS NULL THEN NULL
WHEN "window_type" ~ '^-?[0-9]+$' THEN "window_type"::jsonb
ELSE to_json("window_type")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_window" ALTER COLUMN "glazing_type"
SET DATA TYPE jsonb
USING (
CASE
WHEN "glazing_type" IS NULL THEN NULL
WHEN "glazing_type" ~ '^-?[0-9]+$' THEN "glazing_type"::jsonb
ELSE to_json("glazing_type")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_window" ALTER COLUMN "draught_proofed"
SET DATA TYPE jsonb
USING to_json("draught_proofed")::jsonb;--> statement-breakpoint
ALTER TABLE "epc_window" ALTER COLUMN "window_location"
SET DATA TYPE jsonb
USING (
CASE
WHEN "window_location" IS NULL THEN NULL
WHEN "window_location" ~ '^-?[0-9]+$' THEN "window_location"::jsonb
ELSE to_json("window_location")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_window" ALTER COLUMN "window_wall_type"
SET DATA TYPE jsonb
USING (
CASE
WHEN "window_wall_type" IS NULL THEN NULL
WHEN "window_wall_type" ~ '^-?[0-9]+$' THEN "window_wall_type"::jsonb
ELSE to_json("window_wall_type")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_window" ALTER COLUMN "permanent_shutters_present"
SET DATA TYPE jsonb
USING to_json("permanent_shutters_present")::jsonb;--> statement-breakpoint
ALTER TABLE "epc_window" ALTER COLUMN "transmission_data_source"
SET DATA TYPE jsonb
USING (
CASE
WHEN "transmission_data_source" IS NULL THEN NULL
WHEN "transmission_data_source" ~ '^-?[0-9]+$' THEN "transmission_data_source"::jsonb
ELSE to_json("transmission_data_source")::jsonb
END
);--> statement-breakpoint
ALTER TABLE "epc_building_part" ADD COLUMN "roof_construction_type" text;--> statement-breakpoint
ALTER TABLE "epc_building_part" ADD COLUMN "curtain_wall_age" text;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "mechanical_vent_duct_insulation_level" integer;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "addendum_stone_walls" boolean;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "addendum_system_build" boolean;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "addendum_numbers" jsonb;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "heating_number_baths" integer;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "heating_number_baths_wwhrs" integer;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "heating_electric_shower_count" integer;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "heating_mixer_shower_count" integer;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "ventilation_present" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "ventilation_sheltered_sides" integer;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "ventilation_has_suspended_timber_floor" boolean;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "ventilation_suspended_timber_floor_sealed" boolean;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "ventilation_has_draught_lobby" boolean;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "ventilation_air_permeability_ap4_m3_h_m2" real;--> statement-breakpoint
ALTER TABLE "epc_property" ADD COLUMN "ventilation_mechanical_ventilation_kind" text;--> statement-breakpoint
ALTER TABLE "epc_renewable_heat_incentive" ADD CONSTRAINT "epc_renewable_heat_incentive_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 "property_baseline_performance" ADD CONSTRAINT "property_baseline_performance_property_id_property_id_fk" FOREIGN KEY ("property_id") REFERENCES "public"."property"("id") ON DELETE no action ON UPDATE no action;

File diff suppressed because it is too large Load diff

View file

@ -1541,6 +1541,13 @@
"when": 1780419959831,
"tag": "0219_add_verify_ack",
"breakpoints": true
},
{
"idx": 220,
"version": "7",
"when": 1780483404142,
"tag": "0220_glamorous_phantom_reporter",
"breakpoints": true
}
]
}

View file

@ -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"),
},
);

View file

@ -54,7 +54,9 @@ export const fileType = pgEnum("file_type", [
"improvement_option_evaluation",
"medium_term_improvement_plan",
// Design
"retrofit_design_doc"
"retrofit_design_doc",
// Other
"other"
]);
export const fileSource = pgEnum("file_source", [