From 0a87ef0b1341ba4f6cefe605ce89e35d37fef9d8 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 27 May 2026 10:13:59 +0000 Subject: [PATCH 01/13] save plans for landlord overrid --- docs/wip/landlord-override-frontend-plan.md | 107 ++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 docs/wip/landlord-override-frontend-plan.md diff --git a/docs/wip/landlord-override-frontend-plan.md b/docs/wip/landlord-override-frontend-plan.md new file mode 100644 index 00000000..5a370b35 --- /dev/null +++ b/docs/wip/landlord-override-frontend-plan.md @@ -0,0 +1,107 @@ +# Landlord override frontend — in-flight design notes + +**Status:** Paused mid-grilling (2026-05-27) +**Branch:** `feature/frontend_landlord_overrides` +**Author:** Jun-te (with Claude, via `/grill-me`) + +This is a *design-in-progress* document, not an ADR. It captures decisions made +so far on the landlord-override frontend plan so the conversation can resume +without re-litigating settled questions. Promote to an ADR once the trigger +mechanism (Q4) is resolved — that's the decision worth permanent recording. + +## Goal + +Build the front-end e2e for `landlord_description_override`, starting from the +`bulk_upload` flow. Backend lives at `/workspaces/home/github/Model`. + +## Backstory + +- Four landlord override tables exist in Drizzle + ([src/app/db/schema/landlord_overrides.ts](../../src/app/db/schema/landlord_overrides.ts)): + `landlord_property_type_overrides`, `landlord_built_form_type_overrides`, + `landlord_wall_type_overrides`, `landlord_roof_type_overrides`. Schema + rationale in [ADR-0002](../adr/0002-landlord-override-vocabulary.md). +- Nothing in Next.js reads or writes them yet. +- The Python lambda at + `/workspaces/home/github/Model/applications/landlord_description_overrides/handler.py` + is **not deployed** and **not wired** into the BulkUpload pipeline. It + hardcodes its trigger params (`portfolio_id`, `s3_uri`) and its source column + names (`"Property Type"`, `"Walls"`, `"Roofs"`). +- Note: ADR-0002 says writes come from Next.js POST, but the current backend + writes direct to Postgres. This drift may need to be revisited under Q4. + +## Decided so far + +### Q1 — Scope + +**Trigger + Review/Edit, with classifier non-blocking for address matching.** +Restated by user as: extend the column-mapping UI with optional +landlord-description slots → persist the mapping on the upload → pass mapping +to the lambda → lambda needs edits to work when deployed. + +### Q2 — Categories + +**All four classifier categories** get independent optional slots in +[`INTERNAL_FIELDS`](../../src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/map-columns/MapColumnsClient.tsx#L14-L21): +`property_type`, `built_form_type`, `wall_type`, `roof_type`. + +Rejected alternatives: (a) start with only PT+BF — wasted plumbing churn for +the same migration cost; (c) collapse PT+BF into one UI slot — bakes a +backend coincidence (they read the same CSV column today) into the +user-facing model. + +### Q2.1 — No `autoDetect` for the new slots + +The four new slots default to `"skip"`. The user must explicitly map them. +`autoDetect()` regex patterns are for required address-ish fields only. + +**Why:** Address headers are unambiguous and required, so guessing is safe and +useful. Landlord-description columns are ambiguous (a "type" column could be +PropertyType or BuiltFormType or something else) and they are optional — +auto-detecting them would silently opt the landlord into classifier runs they +didn't intend. + +## Open — resume here + +### Q3 (in flight) — Uniqueness validation on the mapping + +Today validation only checks required fields exist +([MapColumnsClient.tsx:67-68](../../src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/map-columns/MapColumnsClient.tsx#L67-L68)); +two CSV headers can both map to `address_1` silently. + +- (a) Leave it alone — backend last-wins. +- (b) Enforce uniqueness only on the four new slots. +- (c) Enforce uniqueness everywhere except `skip`. **Recommended.** + +### Q4 (queued — biggest) — Trigger mechanism + +How does Next.js invoke the lambda once mapping is complete? SQS message? +Direct lambda invoke? HTTP endpoint? And what's the state-machine integration — +new `BulkUpload` status, or runs orthogonally to address matching? + +This drives both the deployment work and the lambda edits. Likely worth its +own ADR once decided. + +### Q5 (queued) — Persistence of the extended mapping + +Current `bulkAddressUploads.columnMapping` is a `Record` and +naturally accommodates the new slots. Confirm no separate table is needed. + +### Q6 (queued) — Lambda edits + +Handler hardcodes `source_column="Property Type" / "Walls" / "Roofs"`; needs +to read the mapping from the trigger body. +`LandlordDescriptionOverridesTriggerBody` already exists — check what fields +it has vs needs. + +### Q7 (queued) — Review/Edit UI for classified mappings + +Is the per-row review/edit surface in scope for this iteration, or deferred? +User has not addressed yet. ADR-0002 calls this "the future override +frontend" and treats it as deferred work — but "front end e2e from +bulk_upload" could reasonably include it. + +## Resuming + +Re-read this file, then ask Q3. Don't re-litigate Q1/Q2 unless the user +reopens them. From 46fb19ae05716998a8a1e644016b87265b870b9f Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Thu, 28 May 2026 19:32:15 +0000 Subject: [PATCH 02/13] claude completed proof of concept completion just got to check and follow to do list insturcitons in docs/wip --- .claude/settings.json | 20 + docs/wip/landlord-override-frontend-plan.md | 17 +- docs/wip/landlord-override-verification.md | 88 + .../start-address-matching/route.ts | 42 +- .../migrations/0215_invert_column_mapping.sql | 22 + .../migrations/0216_add_subtask_service.sql | 1 + src/app/db/migrations/meta/0215_snapshot.json | 10125 +++++++++++++++ src/app/db/migrations/meta/0216_snapshot.json | 10131 ++++++++++++++++ src/app/db/migrations/meta/_journal.json | 14 + src/app/db/schema/tasks/subtask.ts | 4 + .../[uploadId]/OnboardingProgress.tsx | 29 +- .../map-columns/MapColumnsClient.tsx | 252 +- .../(portfolio)/landlord-overrides/page.tsx | 101 + src/lib/bulkUpload/columnFields.ts | 100 + src/lib/bulkUpload/server.ts | 74 +- src/lib/bulkUpload/types.ts | 15 + src/lib/landlordOverrides/server.ts | 79 + 17 files changed, 20938 insertions(+), 176 deletions(-) create mode 100644 docs/wip/landlord-override-verification.md create mode 100644 src/app/db/migrations/0215_invert_column_mapping.sql create mode 100644 src/app/db/migrations/0216_add_subtask_service.sql create mode 100644 src/app/db/migrations/meta/0215_snapshot.json create mode 100644 src/app/db/migrations/meta/0216_snapshot.json create mode 100644 src/app/portfolio/[slug]/(portfolio)/landlord-overrides/page.tsx create mode 100644 src/lib/bulkUpload/columnFields.ts create mode 100644 src/lib/landlordOverrides/server.ts diff --git a/.claude/settings.json b/.claude/settings.json index 7b99cc77..878b2d3b 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,8 +1,28 @@ { "permissions": { + "allow": [ + "Read(//home/vscode/.claude/skills/grill-me/**)", + "Bash(grep \"\\\\.py$\")", + "Bash(git fetch *)", + "Bash(git add *)", + "Bash(git commit *)", + "Bash(git merge *)", + "Read(//home/vscode/.claude/skills/to-issues/**)", + "Read(//home/vscode/.claude/skills/triage/**)", + "Read(//home/vscode/.claude/skills/**)", + "Bash(echo \"tsc exit: $?\")", + "Bash(npm install *)", + "Bash(npx drizzle-kit *)", + "Bash(echo \"frontend tsc exit: $?\")" + ], "deny": [ "Bash(npx drizzle-kit generate)", "Bash(npx drizzle-kit push)" + ], + "additionalDirectories": [ + "/workspaces/home/github/Model/backend/app/bulk_uploads", + "/workspaces/home/github/Model/applications/landlord_description_overrides", + "/workspaces/home/github/Model/orchestration" ] } } diff --git a/docs/wip/landlord-override-frontend-plan.md b/docs/wip/landlord-override-frontend-plan.md index 9cf4a362..a67f666d 100644 --- a/docs/wip/landlord-override-frontend-plan.md +++ b/docs/wip/landlord-override-frontend-plan.md @@ -118,10 +118,13 @@ framing, which was wrong once classifier header-sharing became a requirement. POST-back. - **State machine:** the classifier runs as a **subtask under the same address task** (not a separate task, not a new `BulkUpload` status). Both subtasks - fire together at the **"Start address matching"** action. Safe: the combiner - globs S3 `ara_raw_outputs/{task_id}/`, which the classifier never writes to, - so the combined address output is not affected; `subtask_handler` does no - sibling-completion gating. + fire together at the **"Start address matching"** action. The combined address + *output* is not affected (the classifier writes Postgres, not + `ara_raw_outputs/{task_id}/`), **but** the parent Task *status* IS recomputed + from all subtasks (`TaskOrchestrator._cascade`), so a classifier failure fails + the onboarding task and gates the combiner. This coupling was knowingly + accepted (2026-05-28), superseding Q1 non-blocking — see ADR-0003's Amended + note. (Corrects an earlier wrong claim that the envelope does no gating.) - **Progress honesty:** add a nullable **`service`/`kind` discriminator to `sub_task`** (existing rows = address/legacy) so the progress view shows address batches vs classification separately and attributes failures @@ -164,9 +167,9 @@ ADR-0002 anticipates. Grilling is complete (Q1–Q7). Suggested follow-ups: -1. Promote Q4 (trigger + state-machine integration) to a **frontend ADR-0003**; - cross-link the backend's ADR-0003 (which already superseded ADR-0002's - Next.js-writes clause). +1. **Done** — Q4 promoted to + [ADR-0003](../adr/0003-classifier-triggers-as-address-subtask.md) (trigger + + state-machine integration), cross-linking the backend's ADR-0003. 2. Break the work into issues (`/to-issues`): (a) invert the mapping shape + UI + migration + validation; (b) `sub_task` discriminator + progress view; (c) classifier trigger (new FastAPI endpoint + payload, fire at "Start"); diff --git a/docs/wip/landlord-override-verification.md b/docs/wip/landlord-override-verification.md new file mode 100644 index 00000000..82bcff08 --- /dev/null +++ b/docs/wip/landlord-override-verification.md @@ -0,0 +1,88 @@ +# Landlord override e2e — verification & deploy checklist + +**Created:** 2026-05-28 +**Branch:** `feature/frontend_landlord_overrides` (this repo) + `feature/landlord_data` (Model repo) +**Plan:** [landlord-override-frontend-plan.md](./landlord-override-frontend-plan.md) · **ADR:** [0003-classifier-triggers-as-address-subtask.md](../adr/0003-classifier-triggers-as-address-subtask.md) + +## Context for picking this up cold + +The landlord-classifier e2e is **implemented across both repos but uncommitted**, and +**statically verified only** (frontend `tsc` 0 errors, `next lint` clean, backend +`py_compile` clean). It has **not** been run live — that needs the steps below +(migrations applied, SQS queue + env, FastAPI endpoint + lambda deployed with +OpenAI/S3/Postgres access). Two migration files are **generated but not applied**: +`0215_invert_column_mapping.sql` (data) and `0216_add_subtask_service.sql` (schema). + +Work through the sections in order — each step's prerequisites come first. + +--- + +## A. Before you start +- [ ] Use dev/preview first, not prod. +- [ ] Confirm `.env.local` DB creds (`DB_HOST/PORT/USERNAME/PASSWORD/NAME`) point at the target DB. +- [ ] **Back up** `column_mapping` — the 0215 inversion is one-shot/irreversible: + ```sql + CREATE TABLE _bak_bulk_mapping AS + SELECT id, column_mapping FROM bulk_address_uploads WHERE column_mapping IS NOT NULL; + ``` + +## B. Database migrations ⚠️ read the gotcha +`0215` = data (inverts `header→field` → `field→header`); `0216` = schema (`ADD COLUMN sub_task.service`). + +⚠️ package.json only has `migration:push` (`drizzle-kit push`). **`push` diffs schema and will NOT run the 0215 data `UPDATE`** — it would add the column but silently skip the inversion. Use `migrate`: +- [ ] ```bash + npx drizzle-kit migrate # runs 0215 then 0216 in order + ``` + (If the team only uses `push`: run `push` for 0216, then execute `0215`'s SQL manually.) +- [ ] ⚠️ Run **0215 exactly once, on old-shape data**. Re-running re-inverts and corrupts. `migrate` guards via the journal; manual runs don't. +- [ ] Verify 0215 — values should now be headers: + ```sql + SELECT column_mapping FROM bulk_address_uploads WHERE column_mapping IS NOT NULL LIMIT 5; + -- expect {"address_1":"Addr 1","postcode":"PCode", ...} + ``` +- [ ] Verify 0216: + ```sql + SELECT 1 FROM information_schema.columns + WHERE table_name='sub_task' AND column_name='service'; + ``` + +## C. Backend deploy (Model service) +- [ ] Create an SQS queue for the classifier (e.g. `landlord-description-overrides`). +- [ ] Set **`LANDLORD_OVERRIDES_SQS_URL`** in the FastAPI env to that queue. +- [ ] Deploy FastAPI so `/v1/bulk-uploads/trigger-landlord-overrides` is live. +- [ ] Deploy the lambda (`applications/landlord_description_overrides`) + event-source mapping queue → lambda. +- [ ] Lambda env/IAM: `OPENAI_API_KEY`, Postgres creds, **S3 read on the original-upload bucket** (it reads `upload.s3Bucket/s3Key`, not `retrofit-data-dev`). + +## D. Frontend deploy +- [ ] Deploy `assessment-model` with the new code (`FASTAPI_API_URL` / `FASTAPI_API_KEY` already set). + +## E. Verify the UI (Column Remapper) +- [ ] Map-columns page shows **one row per field with a header dropdown**, split into **Address fields** + **Landlord description fields**. +- [ ] Leaving Address 1 / Postcode unset blocks submit. +- [ ] Two **address** fields → one column is blocked; the **same** column → Property Type + Built Form is allowed. +- [ ] Existing `mapping_complete` uploads open with their mapping intact (confirms 0215). + +## F. Verify trigger → classify → persist +- [ ] Map ≥1 landlord-description field, click **Start address matching**. +- [ ] `sub_task` has two rows under the task: `service='address2uprn'` and `service='landlord_description_overrides'`. +- [ ] SQS message enqueued + lambda ran (CloudWatch). +- [ ] Rows appear with `source='classifier'`: + ```sql + SELECT description, value FROM landlord_property_type_overrides WHERE portfolio_id = LIMIT 10; + ``` + +## G. Verify the results view +- [ ] `/portfolio//landlord-overrides` lists `description → value` per category with a "classifier" badge. (No nav link yet — reach by URL.) + +## H. Regression — address matching unaffected +- [ ] The same upload's address pipeline still emits the canonical CSV (`Address 1`/`postcode`) and combines normally. + +## I. Watch-out (by design — ADR-0003 "accepted coupling") +- [ ] If the classifier subtask **fails**, the shared onboarding task goes FAILED and **"Run Combiner" is blocked**; the task only COMPLETEs once classification finishes. If painful, switch to the separate-task design in the ADR. + +--- + +## Still open / not done +- [ ] Commit the work (this repo + Model repo, separately) — currently uncommitted. +- [ ] Nav link to `/portfolio//landlord-overrides` (reachable by URL only). +- [ ] User-edit write-back for overrides (deferred — Q7 "read-only this iteration"). diff --git a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/start-address-matching/route.ts b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/start-address-matching/route.ts index 5fd282fa..14441ce2 100644 --- a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/start-address-matching/route.ts +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/start-address-matching/route.ts @@ -3,20 +3,13 @@ import { getServerSession } from "next-auth"; import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { createS3Client, createRetrofitDataS3Client, retrofitDataS3Bucket } from "@/app/utils/s3"; import * as XLSX from "xlsx"; -import { loadForAddressMatching, triggerAddressMatching } from "@/lib/bulkUpload/server"; +import { loadForAddressMatching, triggerAddressMatching, triggerClassifier } from "@/lib/bulkUpload/server"; import { readSessionToken } from "@/lib/session"; - -const FIELD_RENAME: Record = { - address_1: "Address 1", - address_2: "Address 2", - address_3: "Address 3", - postcode: "postcode", - internal_reference: "Internal Reference", -}; +import { ADDRESS_FIELDS } from "@/lib/bulkUpload/columnFields"; function transformFile( buffer: Buffer, - columnMapping: Record + columnMapping: Record // field → source header ): { csv: string; error?: never } | { csv?: never; error: string } { const wb = XLSX.read(buffer, { type: "buffer" }); const sheet = wb.Sheets[wb.SheetNames[0]]; @@ -24,16 +17,13 @@ function transformFile( if (rows.length === 0) return { error: "Empty file" }; - const sourceHeaders = Object.keys(rows[0]); const outputHeaders: string[] = []; - const sourceToOutput: Record = {}; - - for (const src of sourceHeaders) { - const mapped = columnMapping[src]; - if (!mapped || mapped === "skip") continue; - const renamed = FIELD_RENAME[mapped] ?? mapped; - outputHeaders.push(renamed); - sourceToOutput[src] = renamed; + const outputToSource: Record = {}; + for (const field of ADDRESS_FIELDS) { + const src = columnMapping[field.value]; + if (!src || !field.outputHeader) continue; + outputHeaders.push(field.outputHeader); + outputToSource[field.outputHeader] = src; } if (!outputHeaders.includes("Address 1")) @@ -43,8 +33,8 @@ function transformFile( const outputRows = rows.map((row) => { const out: Record = {}; - for (const [src, renamed] of Object.entries(sourceToOutput)) { - out[renamed] = row[src] ?? ""; + for (const [outName, src] of Object.entries(outputToSource)) { + out[outName] = row[src] ?? ""; } return out; }); @@ -112,13 +102,13 @@ export async function POST( const s3Uri = `s3://${outputBucket}/${transformedKey}`; - const trigger = await triggerAddressMatching({ - uploadId, - s3Uri, - sessionToken: readSessionToken(request), - }); + const sessionToken = readSessionToken(request); + const trigger = await triggerAddressMatching({ uploadId, s3Uri, sessionToken }); if (trigger.kind === "trigger_failed") return NextResponse.json({ error: trigger.message }, { status: trigger.status }); + // Co-fire the landlord classifier (non-blocking) under the same task. + await triggerClassifier({ taskId: trigger.taskId, uploadId, sessionToken }); + return NextResponse.json({ taskId: trigger.taskId }, { status: 200 }); } diff --git a/src/app/db/migrations/0215_invert_column_mapping.sql b/src/app/db/migrations/0215_invert_column_mapping.sql new file mode 100644 index 00000000..00c9b64b --- /dev/null +++ b/src/app/db/migrations/0215_invert_column_mapping.sql @@ -0,0 +1,22 @@ +-- One-shot inversion of bulk_address_uploads.column_mapping. +-- +-- Old shape: { "": "" } (header -> field), with +-- unmapped columns stored as "
": "skip". +-- New shape: { "": "" } (field -> header), with +-- unmapped fields simply absent. See ADR-0003 and the WIP plan (Q2.2/Q5). +-- +-- 'skip' entries are dropped. On a legacy duplicate (two headers -> one field), +-- jsonb_object_agg keeps the last header — the new address-uniqueness rule +-- forbids that going forward anyway. No-op on NULL/empty mappings, so this is +-- safe regardless of data volume. One-shot: assumes rows are still old-shape. +UPDATE "bulk_address_uploads" +SET "column_mapping" = COALESCE( + ( + SELECT jsonb_object_agg(elem.value, elem.key) + FROM jsonb_each_text("column_mapping") AS elem + WHERE elem.value <> 'skip' + ), + '{}'::jsonb +) +WHERE "column_mapping" IS NOT NULL + AND "column_mapping" <> '{}'::jsonb; diff --git a/src/app/db/migrations/0216_add_subtask_service.sql b/src/app/db/migrations/0216_add_subtask_service.sql new file mode 100644 index 00000000..62502a20 --- /dev/null +++ b/src/app/db/migrations/0216_add_subtask_service.sql @@ -0,0 +1 @@ +ALTER TABLE "sub_task" ADD COLUMN "service" text; \ No newline at end of file diff --git a/src/app/db/migrations/meta/0215_snapshot.json b/src/app/db/migrations/meta/0215_snapshot.json new file mode 100644 index 00000000..c9e46a1c --- /dev/null +++ b/src/app/db/migrations/meta/0215_snapshot.json @@ -0,0 +1,10125 @@ +{ + "id": "25c3ba3e-0d41-48ac-803e-5af7e50e052f", + "prevId": "2afd2da4-e5c1-4901-abc1-10da26265c48", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.postcode_search": { + "name": "postcode_search", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result_data": { + "name": "result_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_updated_at": { + "name": "last_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postcode_search_postcode_unique": { + "name": "postcode_search_postcode_unique", + "columns": [ + "postcode" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deal_measure_approval_events": { + "name": "deal_measure_approval_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "measure_name": { + "name": "measure_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acted_by": { + "name": "acted_by", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "acted_at": { + "name": "acted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deal_measure_events_deal_id": { + "name": "idx_deal_measure_events_deal_id", + "columns": [ + { + "expression": "hubspot_deal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_deal_measure_events_acted_at": { + "name": "idx_deal_measure_events_acted_at", + "columns": [ + { + "expression": "acted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "deal_measure_approval_events_acted_by_user_id_fk": { + "name": "deal_measure_approval_events_acted_by_user_id_fk", + "tableFrom": "deal_measure_approval_events", + "columnsFrom": [ + "acted_by" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deal_measure_approvals": { + "name": "deal_measure_approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "measure_name": { + "name": "measure_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_approved": { + "name": "is_approved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "approved_by": { + "name": "approved_by", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deal_measure_approvals_deal_id": { + "name": "idx_deal_measure_approvals_deal_id", + "columns": [ + { + "expression": "hubspot_deal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "deal_measure_approvals_approved_by_user_id_fk": { + "name": "deal_measure_approvals_approved_by_user_id_fk", + "tableFrom": "deal_measure_approvals", + "columnsFrom": [ + "approved_by" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_deal_measure": { + "name": "uq_deal_measure", + "columns": [ + "hubspot_deal_id", + "measure_name" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bulk_address_uploads": { + "name": "bulk_address_uploads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "s3_bucket": { + "name": "s3_bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "s3_key": { + "name": "s3_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ready_for_processing'" + }, + "source_headers": { + "name": "source_headers", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "column_mapping": { + "name": "column_mapping", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "combined_output_s3_uri": { + "name": "combined_output_s3_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.aspect_condition": { + "name": "aspect_condition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "element_id": { + "name": "element_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "aspect_type": { + "name": "aspect_type", + "type": "aspect_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "aspect_instance": { + "name": "aspect_instance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "install_date": { + "name": "install_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "renewal_year": { + "name": "renewal_year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "comments": { + "name": "comments", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "aspect_condition_element_id_element_id_fk": { + "name": "aspect_condition_element_id_element_id_fk", + "tableFrom": "aspect_condition", + "columnsFrom": [ + "element_id" + ], + "tableTo": "element", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.element": { + "name": "element", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "survey_id": { + "name": "survey_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "element_type": { + "name": "element_type", + "type": "element_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "element_instance": { + "name": "element_instance", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "element_survey_id_property_condition_survey_id_fk": { + "name": "element_survey_id_property_condition_survey_id_fk", + "tableFrom": "element", + "columnsFrom": [ + "survey_id" + ], + "tableTo": "property_condition_survey", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_condition_survey": { + "name": "property_condition_survey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hubspot_company_data": { + "name": "hubspot_company_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hubspot_deal_data": { + "name": "hubspot_deal_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deal_id": { + "name": "deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dealname": { + "name": "dealname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dealstage": { + "name": "dealstage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_code": { + "name": "project_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "landlord_property_id": { + "name": "landlord_property_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "listing_id": { + "name": "listing_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uprn": { + "name": "uprn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_notes": { + "name": "outcome_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "major_condition_issue_description": { + "name": "major_condition_issue_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "major_condition_issue_photos": { + "name": "major_condition_issue_photos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "major_condition_issue_evidence_s3_url": { + "name": "major_condition_issue_evidence_s3_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coordination_status": { + "name": "coordination_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "design_status": { + "name": "design_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "booking_status": { + "name": "booking_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pashub_link": { + "name": "pashub_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sharepoint_link": { + "name": "sharepoint_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dampmould_growth": { + "name": "dampmould_growth", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pre_sap": { + "name": "pre_sap", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coordinator": { + "name": "coordinator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mtp_completion_date": { + "name": "mtp_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "mtp_re_model_completion_date": { + "name": "mtp_re_model_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "ioe_v3_completion_date": { + "name": "ioe_v3_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "proposed_measures": { + "name": "proposed_measures", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_package": { + "name": "approved_package", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "designer": { + "name": "designer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "design_type": { + "name": "design_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "design_completion_date": { + "name": "design_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "actual_measures_installed": { + "name": "actual_measures_installed", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installer": { + "name": "installer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installer_handover": { + "name": "installer_handover", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lodgement_status": { + "name": "lodgement_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "measures_lodgement_date": { + "name": "measures_lodgement_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "lodgement_date": { + "name": "lodgement_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "expected_commencement_date": { + "name": "expected_commencement_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "coordination_comments": { + "name": "coordination_comments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surveyor": { + "name": "surveyor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "damp_mould_and_repairs_comments": { + "name": "damp_mould_and_repairs_comments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "batch": { + "name": "batch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "batch_description": { + "name": "batch_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_reference": { + "name": "block_reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nonfunded_measures": { + "name": "nonfunded_measures", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_prn": { + "name": "epc_prn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "potential_post_sap_score_dropdown": { + "name": "potential_post_sap_score_dropdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ei_score": { + "name": "ei_score", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ei_score__potential_": { + "name": "ei_score__potential_", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_sap_score": { + "name": "epc_sap_score", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_sap_score__potential_": { + "name": "epc_sap_score__potential_", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmed_survey_date": { + "name": "confirmed_survey_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmed_survey_time": { + "name": "confirmed_survey_time", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surveyed_date": { + "name": "surveyed_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "survey_type": { + "name": "survey_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "measures_for_pibi_ordered": { + "name": "measures_for_pibi_ordered", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pibi_order_date": { + "name": "pibi_order_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "pibi_completed_date": { + "name": "pibi_completed_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "property_halted_date": { + "name": "property_halted_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "property_halted_reason": { + "name": "property_halted_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "technical_approved_measures_for_install": { + "name": "technical_approved_measures_for_install", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_to_installer_for_pricing": { + "name": "sent_to_installer_for_pricing", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "domna_survey_required": { + "name": "domna_survey_required", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "domna_survey_type": { + "name": "domna_survey_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domna_survey_date": { + "name": "domna_survey_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hubspot_projects_data": { + "name": "hubspot_projects_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hubspot_projects_data_project_id_unique": { + "name": "hubspot_projects_data_project_id_unique", + "columns": [ + "project_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hubspot_users": { + "name": "hubspot_users", + "schema": "", + "columns": { + "hubspot_owner_id": { + "name": "hubspot_owner_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_status_tracker": { + "name": "property_status_tracker", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "property_status_tracker_property_id_property_id_fk": { + "name": "property_status_tracker_property_id_property_id_fk", + "tableFrom": "property_status_tracker", + "columnsFrom": [ + "property_id" + ], + "tableTo": "property", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "property_status_tracker_portfolio_id_portfolio_id_fk": { + "name": "property_status_tracker_portfolio_id_portfolio_id_fk", + "tableFrom": "property_status_tracker", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.energy_assessments": { + "name": "energy_assessments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "uprn_source": { + "name": "uprn_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "building_reference_number": { + "name": "building_reference_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_energy_efficiency": { + "name": "current_energy_efficiency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_energy_rating": { + "name": "current_energy_rating", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address1": { + "name": "address1", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address2": { + "name": "address2", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address3": { + "name": "address3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "posttown": { + "name": "posttown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "county": { + "name": "county", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency": { + "name": "constituency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency_label": { + "name": "constituency_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "low_energy_fixed_light_count": { + "name": "low_energy_fixed_light_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "construction_age_band": { + "name": "construction_age_band", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheat_energy_eff": { + "name": "mainheat_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "windows_env_eff": { + "name": "windows_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_energy_eff": { + "name": "lighting_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_impact_potential": { + "name": "environment_impact_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheatcont_description": { + "name": "mainheatcont_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sheating_energy_eff": { + "name": "sheating_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "local_authority": { + "name": "local_authority", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "local_authority_label": { + "name": "local_authority_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fixed_lighting_outlets_count": { + "name": "fixed_lighting_outlets_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_tariff": { + "name": "energy_tariff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mechanical_ventilation": { + "name": "mechanical_ventilation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "solar_water_heating_flag": { + "name": "solar_water_heating_flag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "co2_emissions_potential": { + "name": "co2_emissions_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_heated_rooms": { + "name": "number_heated_rooms", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_description": { + "name": "floor_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_consumption_potential": { + "name": "energy_consumption_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "built_form": { + "name": "built_form", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_open_fireplaces": { + "name": "number_open_fireplaces", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "windows_description": { + "name": "windows_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "glazed_area": { + "name": "glazed_area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inspection_date": { + "name": "inspection_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true + }, + "mains_gas_flag": { + "name": "mains_gas_flag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "co2_emiss_curr_per_floor_area": { + "name": "co2_emiss_curr_per_floor_area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heat_loss_corridor": { + "name": "heat_loss_corridor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unheated_corridor_length": { + "name": "unheated_corridor_length", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "flat_storey_count": { + "name": "flat_storey_count", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "roof_energy_eff": { + "name": "roof_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_floor_area": { + "name": "total_floor_area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_impact_current": { + "name": "environment_impact_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "roof_description": { + "name": "roof_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_energy_eff": { + "name": "floor_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_habitable_rooms": { + "name": "number_habitable_rooms", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_env_eff": { + "name": "hot_water_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheatc_energy_eff": { + "name": "mainheatc_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "main_fuel": { + "name": "main_fuel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_env_eff": { + "name": "lighting_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "windows_energy_eff": { + "name": "windows_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_env_eff": { + "name": "floor_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sheating_env_eff": { + "name": "sheating_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_description": { + "name": "lighting_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "roof_env_eff": { + "name": "roof_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walls_energy_eff": { + "name": "walls_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "photo_supply": { + "name": "photo_supply", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_cost_potential": { + "name": "lighting_cost_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheat_env_eff": { + "name": "mainheat_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "multi_glaze_proportion": { + "name": "multi_glaze_proportion", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "main_heating_controls": { + "name": "main_heating_controls", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "flat_top_storey": { + "name": "flat_top_storey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secondheat_description": { + "name": "secondheat_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walls_env_eff": { + "name": "walls_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_type": { + "name": "transaction_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "extension_count": { + "name": "extension_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheatc_env_eff": { + "name": "mainheatc_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lmk_key": { + "name": "lmk_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wind_turbine_count": { + "name": "wind_turbine_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenure": { + "name": "tenure", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_level": { + "name": "floor_level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "potential_energy_efficiency": { + "name": "potential_energy_efficiency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "potential_energy_rating": { + "name": "potential_energy_rating", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_energy_eff": { + "name": "hot_water_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "low_energy_lighting": { + "name": "low_energy_lighting", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walls_description": { + "name": "walls_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hotwater_description": { + "name": "hotwater_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "co2_emissions_current": { + "name": "co2_emissions_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heating_cost_current": { + "name": "heating_cost_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heating_cost_potential": { + "name": "heating_cost_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_cost_current": { + "name": "hot_water_cost_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_cost_potential": { + "name": "hot_water_cost_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_cost_current": { + "name": "lighting_cost_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_consumption_current": { + "name": "energy_consumption_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lodgement_date": { + "name": "lodgement_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "lodgement_datetime": { + "name": "lodgement_datetime", + "type": "timestamp (6)", + "primaryKey": false, + "notNull": true + }, + "mainheat_description": { + "name": "mainheat_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_height": { + "name": "floor_height", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "glazed_type": { + "name": "glazed_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_location": { + "name": "file_location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surveyor_name": { + "name": "surveyor_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surveyor_company": { + "name": "surveyor_company", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_heating_kwh": { + "name": "space_heating_kwh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "water_heating_kwh": { + "name": "water_heating_kwh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_of_doors": { + "name": "number_of_doors", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number_of_insulated_doors": { + "name": "number_of_insulated_doors", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number_of_floors": { + "name": "number_of_floors", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "insulation_wall_area": { + "name": "insulation_wall_area", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "heat_loss_perimeter": { + "name": "heat_loss_perimeter", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "party_wall_length": { + "name": "party_wall_length", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "perimeter": { + "name": "perimeter", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "rooms_with_bath_and_or_shower": { + "name": "rooms_with_bath_and_or_shower", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rooms_with_mixer_shower_no_bath": { + "name": "rooms_with_mixer_shower_no_bath", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "room_with_bath_and_mixer_shower": { + "name": "room_with_bath_and_mixer_shower", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "percent_draftproofed": { + "name": "percent_draftproofed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "has_hot_water_cylinder": { + "name": "has_hot_water_cylinder", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cylinder_insulation_type": { + "name": "cylinder_insulation_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cylinder_insulation_thickness": { + "name": "cylinder_insulation_thickness", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cylinder_thermostat": { + "name": "cylinder_thermostat", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "main_dwelling_ground_floor_area": { + "name": "main_dwelling_ground_floor_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_windows": { + "name": "number_of_windows", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "windows_area": { + "name": "windows_area", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.energy_assessment_documents": { + "name": "energy_assessment_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "energy_assessment_id": { + "name": "energy_assessment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "document_type": { + "name": "document_type", + "type": "document_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "document_location": { + "name": "document_location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "scenario_id": { + "name": "scenario_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "energy_assessment_documents_energy_assessment_id_energy_assessments_id_fk": { + "name": "energy_assessment_documents_energy_assessment_id_energy_assessments_id_fk", + "tableFrom": "energy_assessment_documents", + "columnsFrom": [ + "energy_assessment_id" + ], + "tableTo": "energy_assessments", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "energy_assessment_documents_scenario_id_energy_assessment_scenarios_id_fk": { + "name": "energy_assessment_documents_scenario_id_energy_assessment_scenarios_id_fk", + "tableFrom": "energy_assessment_documents", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "energy_assessment_scenarios", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.energy_assessment_scenarios": { + "name": "energy_assessment_scenarios", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "scenario_name": { + "name": "scenario_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_assessment_id": { + "name": "energy_assessment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "energy_assessment_scenarios_energy_assessment_id_energy_assessments_id_fk": { + "name": "energy_assessment_scenarios_energy_assessment_id_energy_assessments_id_fk", + "tableFrom": "energy_assessment_scenarios", + "columnsFrom": [ + "energy_assessment_id" + ], + "tableTo": "energy_assessments", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_store": { + "name": "epc_store", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "epc_api_created_at": { + "name": "epc_api_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "epc_api": { + "name": "epc_api", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "epc_page_created_at": { + "name": "epc_page_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "epc_page": { + "name": "epc_page", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_page_rrn": { + "name": "epc_page_rrn", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_epc_store_uprn": { + "name": "uq_epc_store_uprn", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files_from_surveyor": { + "name": "files_from_surveyor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "s3_json_url": { + "name": "s3_json_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "files_from_surveyor_portfolio_id_portfolio_id_fk": { + "name": "files_from_surveyor_portfolio_id_portfolio_id_fk", + "tableFrom": "files_from_surveyor", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "files_from_surveyor_property_id_property_id_fk": { + "name": "files_from_surveyor_property_id_property_id_fk", + "tableFrom": "files_from_surveyor", + "columnsFrom": [ + "property_id" + ], + "tableTo": "property", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.funding_package": { + "name": "funding_package", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "scheme": { + "name": "scheme", + "type": "scheme", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "project_funding": { + "name": "project_funding", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_uplift": { + "name": "total_uplift", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "full_project_score": { + "name": "full_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "partial_project_score": { + "name": "partial_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "uplift_project_score": { + "name": "uplift_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "funding_package_plan_id_plan_id_fk": { + "name": "funding_package_plan_id_plan_id_fk", + "tableFrom": "funding_package", + "columnsFrom": [ + "plan_id" + ], + "tableTo": "plan", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.funding_package_measures": { + "name": "funding_package_measures", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "funding_package_id": { + "name": "funding_package_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "measure": { + "name": "measure", + "type": "type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "innovation_uplift": { + "name": "innovation_uplift", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "partial_project_score": { + "name": "partial_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "uplift_project_score": { + "name": "uplift_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "funding_package_measures_funding_package_id_funding_package_id_fk": { + "name": "funding_package_measures_funding_package_id_funding_package_id_fk", + "tableFrom": "funding_package_measures", + "columnsFrom": [ + "funding_package_id" + ], + "tableTo": "funding_package", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "funding_package_measures_material_id_material_id_fk": { + "name": "funding_package_measures_material_id_material_id_fk", + "tableFrom": "funding_package_measures", + "columnsFrom": [ + "material_id" + ], + "tableTo": "material", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inspections": { + "name": "inspections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "archetype": { + "name": "archetype", + "type": "inspection_archetype", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "archetype_2": { + "name": "archetype_2", + "type": "inspection_archetype_2", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "wall_construction": { + "name": "wall_construction", + "type": "inspections_wall_construction", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "insulation": { + "name": "insulation", + "type": "inspections_wall_insulation", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "insulation_material": { + "name": "insulation_material", + "type": "inspections_insulation_material", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "borescoped": { + "name": "borescoped", + "type": "inspection_borescoped", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "roof_orientation": { + "name": "roof_orientation", + "type": "inspections_roof_orientation", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "tile_hung": { + "name": "tile_hung", + "type": "inspections_tile_hung", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "rendered": { + "name": "rendered", + "type": "inspections_rendered", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "cladding": { + "name": "cladding", + "type": "inspections_cladding", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "access_issues": { + "name": "access_issues", + "type": "inspections_access_issues", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "surveyor_name": { + "name": "surveyor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "inspections_property_id_property_id_fk": { + "name": "inspections_property_id_property_id_fk", + "tableFrom": "inspections", + "columnsFrom": [ + "property_id" + ], + "tableTo": "property", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.landlord_built_form_type_overrides": { + "name": "landlord_built_form_type_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "built_form_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "override_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "landlord_built_form_type_overrides_portfolio_id_portfolio_id_fk": { + "name": "landlord_built_form_type_overrides_portfolio_id_portfolio_id_fk", + "tableFrom": "landlord_built_form_type_overrides", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "landlord_built_form_type_overrides_portfolio_description_unique": { + "name": "landlord_built_form_type_overrides_portfolio_description_unique", + "columns": [ + "portfolio_id", + "description" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.landlord_property_type_overrides": { + "name": "landlord_property_type_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "property_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "override_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "landlord_property_type_overrides_portfolio_id_portfolio_id_fk": { + "name": "landlord_property_type_overrides_portfolio_id_portfolio_id_fk", + "tableFrom": "landlord_property_type_overrides", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "landlord_property_type_overrides_portfolio_description_unique": { + "name": "landlord_property_type_overrides_portfolio_description_unique", + "columns": [ + "portfolio_id", + "description" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.landlord_roof_type_overrides": { + "name": "landlord_roof_type_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "roof_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "override_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "landlord_roof_type_overrides_portfolio_id_portfolio_id_fk": { + "name": "landlord_roof_type_overrides_portfolio_id_portfolio_id_fk", + "tableFrom": "landlord_roof_type_overrides", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "landlord_roof_type_overrides_portfolio_description_unique": { + "name": "landlord_roof_type_overrides_portfolio_description_unique", + "columns": [ + "portfolio_id", + "description" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.landlord_wall_type_overrides": { + "name": "landlord_wall_type_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "wall_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "override_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "landlord_wall_type_overrides_portfolio_id_portfolio_id_fk": { + "name": "landlord_wall_type_overrides_portfolio_id_portfolio_id_fk", + "tableFrom": "landlord_wall_type_overrides", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "landlord_wall_type_overrides_portfolio_description_unique": { + "name": "landlord_wall_type_overrides_portfolio_description_unique", + "columns": [ + "portfolio_id", + "description" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.magic_plan_door": { + "name": "magic_plan_door", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "magic_plan_room_id": { + "name": "magic_plan_room_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "width_mm": { + "name": "width_mm", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "magic_plan_door_magic_plan_room_id_magic_plan_room_id_fk": { + "name": "magic_plan_door_magic_plan_room_id_magic_plan_room_id_fk", + "tableFrom": "magic_plan_door", + "columnsFrom": [ + "magic_plan_room_id" + ], + "tableTo": "magic_plan_room", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.magic_plan_floor": { + "name": "magic_plan_floor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "magic_plan_plan_id": { + "name": "magic_plan_plan_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "magic_plan_floor_magic_plan_plan_id_magic_plan_plan_id_fk": { + "name": "magic_plan_floor_magic_plan_plan_id_magic_plan_plan_id_fk", + "tableFrom": "magic_plan_floor", + "columnsFrom": [ + "magic_plan_plan_id" + ], + "tableTo": "magic_plan_plan", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.magic_plan_plan": { + "name": "magic_plan_plan", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "magic_plan_uid": { + "name": "magic_plan_uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_file_id": { + "name": "uploaded_file_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "magic_plan_plan_uploaded_file_id_uploaded_files_id_fk": { + "name": "magic_plan_plan_uploaded_file_id_uploaded_files_id_fk", + "tableFrom": "magic_plan_plan", + "columnsFrom": [ + "uploaded_file_id" + ], + "tableTo": "uploaded_files", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "magic_plan_plan_magic_plan_uid_unique": { + "name": "magic_plan_plan_magic_plan_uid_unique", + "columns": [ + "magic_plan_uid" + ], + "nullsNotDistinct": false + }, + "magic_plan_plan_uploaded_file_id_unique": { + "name": "magic_plan_plan_uploaded_file_id_unique", + "columns": [ + "uploaded_file_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.magic_plan_room": { + "name": "magic_plan_room", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "magic_plan_floor_id": { + "name": "magic_plan_floor_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "width_m": { + "name": "width_m", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "length_m": { + "name": "length_m", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "area_m2": { + "name": "area_m2", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "magic_plan_room_magic_plan_floor_id_magic_plan_floor_id_fk": { + "name": "magic_plan_room_magic_plan_floor_id_magic_plan_floor_id_fk", + "tableFrom": "magic_plan_room", + "columnsFrom": [ + "magic_plan_floor_id" + ], + "tableTo": "magic_plan_floor", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.magic_plan_window": { + "name": "magic_plan_window", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "magic_plan_room_id": { + "name": "magic_plan_room_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "width_m": { + "name": "width_m", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "height_m": { + "name": "height_m", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "area_m2": { + "name": "area_m2", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "opening_type": { + "name": "opening_type", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "magic_plan_window_magic_plan_room_id_magic_plan_room_id_fk": { + "name": "magic_plan_window_magic_plan_room_id_magic_plan_room_id_fk", + "tableFrom": "magic_plan_window", + "columnsFrom": [ + "magic_plan_room_id" + ], + "tableTo": "magic_plan_room", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.material": { + "name": "material", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depth": { + "name": "depth", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "depth_unit": { + "name": "depth_unit", + "type": "depth_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "cost_unit": { + "name": "cost_unit", + "type": "cost_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "r_value_per_mm": { + "name": "r_value_per_mm", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "r_value_unit": { + "name": "r_value_unit", + "type": "r_value_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "thermal_conductivity": { + "name": "thermal_conductivity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "thermal_conductivity_unit": { + "name": "thermal_conductivity_unit", + "type": "thermal_conductivity_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "prime_material_cost": { + "name": "prime_material_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "material_cost": { + "name": "material_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_cost": { + "name": "labour_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_hours_per_unit": { + "name": "labour_hours_per_unit", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "plant_cost": { + "name": "plant_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_cost": { + "name": "total_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_installer_quote": { + "name": "is_installer_quote", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "innovation_rate": { + "name": "innovation_rate", + "type": "real", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "size": { + "name": "size", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "size_unit": { + "name": "size_unit", + "type": "size_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "includes_scaffolding": { + "name": "includes_scaffolding", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "includes_battery": { + "name": "includes_battery", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "battery_size": { + "name": "battery_size", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organisation": { + "name": "organisation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "hubspot_company_id": { + "name": "hubspot_company_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pibi_requests": { + "name": "pibi_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "measure_name": { + "name": "measure_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordered_at": { + "name": "ordered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_pibi_requests_deal_id": { + "name": "idx_pibi_requests_deal_id", + "columns": [ + { + "expression": "hubspot_deal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_pibi_requests_portfolio_id": { + "name": "idx_pibi_requests_portfolio_id", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "pibi_requests_portfolio_id_portfolio_id_fk": { + "name": "pibi_requests_portfolio_id_portfolio_id_fk", + "tableFrom": "pibi_requests", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "pibi_requests_created_by_user_id_user_id_fk": { + "name": "pibi_requests_created_by_user_id_user_id_fk", + "tableFrom": "pibi_requests", + "columnsFrom": [ + "created_by_user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolio_organisation": { + "name": "portfolio_organisation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolio_organisation_portfolio_id_portfolio_id_fk": { + "name": "portfolio_organisation_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolio_organisation", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "portfolio_organisation_organisation_id_organisation_id_fk": { + "name": "portfolio_organisation_organisation_id_organisation_id_fk", + "tableFrom": "portfolio_organisation", + "columnsFrom": [ + "organisation_id" + ], + "tableTo": "organisation", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "portfolio_organisation_portfolio_id_organisation_id_unique": { + "name": "portfolio_organisation_portfolio_id_organisation_id_unique", + "columns": [ + "portfolio_id", + "organisation_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolio": { + "name": "portfolio", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "budget": { + "name": "budget", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "goal", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_properties": { + "name": "number_of_properties", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rental_yield_increase": { + "name": "rental_yield_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_days": { + "name": "labour_days", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "epc_breakdown_pre_retrofit": { + "name": "epc_breakdown_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_breakdown_post_retrofit": { + "name": "epc_breakdown_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "n_units_to_retrofit": { + "name": "n_units_to_retrofit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_pre_retrofit": { + "name": "co2_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_post_retrofit": { + "name": "co2_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_pre_retrofit": { + "name": "energy_bill_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_post_retrofit": { + "name": "energy_bill_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_pre_retrofit": { + "name": "energy_consumption_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_post_retrofit": { + "name": "energy_consumption_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_improvement_per_unit": { + "name": "valuation_improvement_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_unit": { + "name": "cost_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_co2_saved": { + "name": "cost_per_co2_saved", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_sap_point": { + "name": "cost_per_sap_point", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_return_on_investment": { + "name": "valuation_return_on_investment", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolio_capabilities": { + "name": "portfolio_capabilities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "portfolio_capability", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolio_capabilities_user_id_user_id_fk": { + "name": "portfolio_capabilities_user_id_user_id_fk", + "tableFrom": "portfolio_capabilities", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "portfolio_capabilities_portfolio_id_portfolio_id_fk": { + "name": "portfolio_capabilities_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolio_capabilities", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "portfolio_capabilities_user_id_portfolio_id_capability_unique": { + "name": "portfolio_capabilities_user_id_portfolio_id_capability_unique", + "columns": [ + "user_id", + "portfolio_id", + "capability" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolioInvitations": { + "name": "portfolioInvitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolioInvitations_portfolio_id_portfolio_id_fk": { + "name": "portfolioInvitations_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolioInvitations", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "portfolioInvitations_invited_by_user_id_user_id_fk": { + "name": "portfolioInvitations_invited_by_user_id_user_id_fk", + "tableFrom": "portfolioInvitations", + "columnsFrom": [ + "invited_by_user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "portfolio_invitations_portfolio_email_unique": { + "name": "portfolio_invitations_portfolio_email_unique", + "columns": [ + "portfolio_id", + "email" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolioUsers": { + "name": "portfolioUsers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolioUsers_user_id_user_id_fk": { + "name": "portfolioUsers_user_id_user_id_fk", + "tableFrom": "portfolioUsers", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "portfolioUsers_portfolio_id_portfolio_id_fk": { + "name": "portfolioUsers_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolioUsers", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_building_part": { + "name": "epc_building_part", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "epc_property_id": { + "name": "epc_property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "construction_age_band": { + "name": "construction_age_band", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wall_construction": { + "name": "wall_construction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wall_insulation_type": { + "name": "wall_insulation_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wall_thickness_measured": { + "name": "wall_thickness_measured", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "party_wall_construction": { + "name": "party_wall_construction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "building_part_number": { + "name": "building_part_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "wall_dry_lined": { + "name": "wall_dry_lined", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "wall_thickness_mm": { + "name": "wall_thickness_mm", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "wall_insulation_thickness": { + "name": "wall_insulation_thickness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_heat_loss": { + "name": "floor_heat_loss", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "floor_insulation_thickness": { + "name": "floor_insulation_thickness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "flat_roof_insulation_thickness": { + "name": "flat_roof_insulation_thickness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_type": { + "name": "floor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_construction_type": { + "name": "floor_construction_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_insulation_type_str": { + "name": "floor_insulation_type_str", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_u_value_known": { + "name": "floor_u_value_known", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "roof_construction": { + "name": "roof_construction", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "roof_insulation_location": { + "name": "roof_insulation_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "roof_insulation_thickness": { + "name": "roof_insulation_thickness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "room_in_roof_floor_area": { + "name": "room_in_roof_floor_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "room_in_roof_construction_age_band": { + "name": "room_in_roof_construction_age_band", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alt_wall_1_area": { + "name": "alt_wall_1_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "alt_wall_1_dry_lined": { + "name": "alt_wall_1_dry_lined", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alt_wall_1_construction": { + "name": "alt_wall_1_construction", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "alt_wall_1_insulation_type": { + "name": "alt_wall_1_insulation_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "alt_wall_1_thickness_measured": { + "name": "alt_wall_1_thickness_measured", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alt_wall_1_insulation_thickness": { + "name": "alt_wall_1_insulation_thickness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alt_wall_2_area": { + "name": "alt_wall_2_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "alt_wall_2_dry_lined": { + "name": "alt_wall_2_dry_lined", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alt_wall_2_construction": { + "name": "alt_wall_2_construction", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "alt_wall_2_insulation_type": { + "name": "alt_wall_2_insulation_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "alt_wall_2_thickness_measured": { + "name": "alt_wall_2_thickness_measured", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alt_wall_2_insulation_thickness": { + "name": "alt_wall_2_insulation_thickness", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "epc_building_part_epc_property_id_epc_property_id_fk": { + "name": "epc_building_part_epc_property_id_epc_property_id_fk", + "tableFrom": "epc_building_part", + "columnsFrom": [ + "epc_property_id" + ], + "tableTo": "epc_property", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_energy_element": { + "name": "epc_energy_element", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "epc_property_id": { + "name": "epc_property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "element_type": { + "name": "element_type", + "type": "energy_element_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_efficiency_rating": { + "name": "energy_efficiency_rating", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environmental_efficiency_rating": { + "name": "environmental_efficiency_rating", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "epc_energy_element_epc_property_id_epc_property_id_fk": { + "name": "epc_energy_element_epc_property_id_epc_property_id_fk", + "tableFrom": "epc_energy_element", + "columnsFrom": [ + "epc_property_id" + ], + "tableTo": "epc_property", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_flat_details": { + "name": "epc_flat_details", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "epc_property_id": { + "name": "epc_property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "top_storey": { + "name": "top_storey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "flat_location": { + "name": "flat_location", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "heat_loss_corridor": { + "name": "heat_loss_corridor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "storey_count": { + "name": "storey_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "unheated_corridor_length_m": { + "name": "unheated_corridor_length_m", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "epc_flat_details_epc_property_id_epc_property_id_fk": { + "name": "epc_flat_details_epc_property_id_epc_property_id_fk", + "tableFrom": "epc_flat_details", + "columnsFrom": [ + "epc_property_id" + ], + "tableTo": "epc_property", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "epc_flat_details_epc_property_id_unique": { + "name": "epc_flat_details_epc_property_id_unique", + "columns": [ + "epc_property_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_floor_dimension": { + "name": "epc_floor_dimension", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "epc_building_part_id": { + "name": "epc_building_part_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "floor": { + "name": "floor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "room_height_m": { + "name": "room_height_m", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "total_floor_area_m2": { + "name": "total_floor_area_m2", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "party_wall_length_m": { + "name": "party_wall_length_m", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "heat_loss_perimeter_m": { + "name": "heat_loss_perimeter_m", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "floor_insulation": { + "name": "floor_insulation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "floor_construction": { + "name": "floor_construction", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "epc_floor_dimension_epc_building_part_id_epc_building_part_id_fk": { + "name": "epc_floor_dimension_epc_building_part_id_epc_building_part_id_fk", + "tableFrom": "epc_floor_dimension", + "columnsFrom": [ + "epc_building_part_id" + ], + "tableTo": "epc_building_part", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_main_heating_detail": { + "name": "epc_main_heating_detail", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "epc_property_id": { + "name": "epc_property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "has_fghrs": { + "name": "has_fghrs", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "main_fuel_type": { + "name": "main_fuel_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heat_emitter_type": { + "name": "heat_emitter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emitter_temperature": { + "name": "emitter_temperature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "main_heating_control": { + "name": "main_heating_control", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fan_flue_present": { + "name": "fan_flue_present", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boiler_flue_type": { + "name": "boiler_flue_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "boiler_ignition_type": { + "name": "boiler_ignition_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "central_heating_pump_age": { + "name": "central_heating_pump_age", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "central_heating_pump_age_str": { + "name": "central_heating_pump_age_str", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "main_heating_index_number": { + "name": "main_heating_index_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sap_main_heating_code": { + "name": "sap_main_heating_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "main_heating_number": { + "name": "main_heating_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "main_heating_category": { + "name": "main_heating_category", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "main_heating_fraction": { + "name": "main_heating_fraction", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "main_heating_data_source": { + "name": "main_heating_data_source", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "condensing": { + "name": "condensing", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "weather_compensator": { + "name": "weather_compensator", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "epc_main_heating_detail_epc_property_id_epc_property_id_fk": { + "name": "epc_main_heating_detail_epc_property_id_epc_property_id_fk", + "tableFrom": "epc_main_heating_detail", + "columnsFrom": [ + "epc_property_id" + ], + "tableTo": "epc_property", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_property": { + "name": "epc_property", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "uploaded_file_id": { + "name": "uploaded_file_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "uprn_source": { + "name": "uprn_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "report_reference": { + "name": "report_reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "report_type": { + "name": "report_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assessment_type": { + "name": "assessment_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sap_version": { + "name": "sap_version", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "schema_type": { + "name": "schema_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_versions_original": { + "name": "schema_versions_original", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "calculation_software_version": { + "name": "calculation_software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line_1": { + "name": "address_line_1", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address_line_2": { + "name": "address_line_2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "post_town": { + "name": "post_town", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region_code": { + "name": "region_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dwelling_type": { + "name": "dwelling_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "built_form": { + "name": "built_form", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenure": { + "name": "tenure", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_type": { + "name": "transaction_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inspection_date": { + "name": "inspection_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completion_date": { + "name": "completion_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "registration_date": { + "name": "registration_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_floor_area_m2": { + "name": "total_floor_area_m2", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "measurement_type": { + "name": "measurement_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "solar_water_heating": { + "name": "solar_water_heating", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "has_hot_water_cylinder": { + "name": "has_hot_water_cylinder", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "has_fixed_air_conditioning": { + "name": "has_fixed_air_conditioning", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "has_conservatory": { + "name": "has_conservatory", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_heated_separate_conservatory": { + "name": "has_heated_separate_conservatory", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "conservatory_type": { + "name": "conservatory_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "door_count": { + "name": "door_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "wet_rooms_count": { + "name": "wet_rooms_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "extensions_count": { + "name": "extensions_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "heated_rooms_count": { + "name": "heated_rooms_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "open_chimneys_count": { + "name": "open_chimneys_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "habitable_rooms_count": { + "name": "habitable_rooms_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "insulated_door_count": { + "name": "insulated_door_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cfl_fixed_lighting_bulbs_count": { + "name": "cfl_fixed_lighting_bulbs_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "led_fixed_lighting_bulbs_count": { + "name": "led_fixed_lighting_bulbs_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "incandescent_fixed_lighting_bulbs_count": { + "name": "incandescent_fixed_lighting_bulbs_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "blocked_chimneys_count": { + "name": "blocked_chimneys_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "draughtproofed_door_count": { + "name": "draughtproofed_door_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "energy_rating_average": { + "name": "energy_rating_average", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "low_energy_fixed_lighting_bulbs_count": { + "name": "low_energy_fixed_lighting_bulbs_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fixed_lighting_outlets_count": { + "name": "fixed_lighting_outlets_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "low_energy_fixed_lighting_outlets_count": { + "name": "low_energy_fixed_lighting_outlets_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_storeys": { + "name": "number_of_storeys", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "any_unheated_rooms": { + "name": "any_unheated_rooms", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hydro": { + "name": "hydro", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "photovoltaic_array": { + "name": "photovoltaic_array", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "waste_water_heat_recovery": { + "name": "waste_water_heat_recovery", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pressure_test": { + "name": "pressure_test", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pressure_test_certificate_number": { + "name": "pressure_test_certificate_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "percent_draughtproofed": { + "name": "percent_draughtproofed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "insulated_door_u_value": { + "name": "insulated_door_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "multiple_glazed_proportion": { + "name": "multiple_glazed_proportion", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "windows_transmission_u_value": { + "name": "windows_transmission_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "windows_transmission_data_source": { + "name": "windows_transmission_data_source", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "windows_transmission_solar_transmittance": { + "name": "windows_transmission_solar_transmittance", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_mains_gas": { + "name": "energy_mains_gas", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "energy_meter_type": { + "name": "energy_meter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_pv_battery_count": { + "name": "energy_pv_battery_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "energy_wind_turbines_count": { + "name": "energy_wind_turbines_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "energy_gas_smart_meter_present": { + "name": "energy_gas_smart_meter_present", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "energy_is_dwelling_export_capable": { + "name": "energy_is_dwelling_export_capable", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "energy_wind_turbines_terrain_type": { + "name": "energy_wind_turbines_terrain_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_electricity_smart_meter_present": { + "name": "energy_electricity_smart_meter_present", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "energy_pv_connection": { + "name": "energy_pv_connection", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_pv_percent_roof_area": { + "name": "energy_pv_percent_roof_area", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "energy_pv_battery_capacity": { + "name": "energy_pv_battery_capacity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_wind_turbine_hub_height": { + "name": "energy_wind_turbine_hub_height", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_wind_turbine_rotor_diameter": { + "name": "energy_wind_turbine_rotor_diameter", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "heating_cylinder_size": { + "name": "heating_cylinder_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_water_heating_code": { + "name": "heating_water_heating_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_water_heating_fuel": { + "name": "heating_water_heating_fuel", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_immersion_heating_type": { + "name": "heating_immersion_heating_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_cylinder_insulation_type": { + "name": "heating_cylinder_insulation_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_cylinder_thermostat": { + "name": "heating_cylinder_thermostat", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_secondary_fuel_type": { + "name": "heating_secondary_fuel_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_secondary_heating_type": { + "name": "heating_secondary_heating_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_cylinder_insulation_thickness_mm": { + "name": "heating_cylinder_insulation_thickness_mm", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_wwhrs_index_number_1": { + "name": "heating_wwhrs_index_number_1", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_wwhrs_index_number_2": { + "name": "heating_wwhrs_index_number_2", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_shower_outlet_type": { + "name": "heating_shower_outlet_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_shower_wwhrs": { + "name": "heating_shower_wwhrs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_type": { + "name": "ventilation_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ventilation_draught_lobby": { + "name": "ventilation_draught_lobby", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ventilation_pressure_test": { + "name": "ventilation_pressure_test", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ventilation_open_flues_count": { + "name": "ventilation_open_flues_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_closed_flues_count": { + "name": "ventilation_closed_flues_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_boiler_flues_count": { + "name": "ventilation_boiler_flues_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_other_flues_count": { + "name": "ventilation_other_flues_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_extract_fans_count": { + "name": "ventilation_extract_fans_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_passive_vents_count": { + "name": "ventilation_passive_vents_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_flueless_gas_fires_count": { + "name": "ventilation_flueless_gas_fires_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_in_pcdf_database": { + "name": "ventilation_in_pcdf_database", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "mechanical_ventilation": { + "name": "mechanical_ventilation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mechanical_vent_duct_type": { + "name": "mechanical_vent_duct_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mechanical_vent_duct_placement": { + "name": "mechanical_vent_duct_placement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mechanical_vent_duct_insulation": { + "name": "mechanical_vent_duct_insulation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mechanical_ventilation_index_number": { + "name": "mechanical_ventilation_index_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mechanical_vent_measured_installation": { + "name": "mechanical_vent_measured_installation", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_epc_property_property_portfolio": { + "name": "uq_epc_property_property_portfolio", + "columns": [ + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "epc_property_property_id_property_id_fk": { + "name": "epc_property_property_id_property_id_fk", + "tableFrom": "epc_property", + "columnsFrom": [ + "property_id" + ], + "tableTo": "property", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "epc_property_portfolio_id_portfolio_id_fk": { + "name": "epc_property_portfolio_id_portfolio_id_fk", + "tableFrom": "epc_property", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "epc_property_uploaded_file_id_uploaded_files_id_fk": { + "name": "epc_property_uploaded_file_id_uploaded_files_id_fk", + "tableFrom": "epc_property", + "columnsFrom": [ + "uploaded_file_id" + ], + "tableTo": "uploaded_files", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "epc_property_uploaded_file_id_unique": { + "name": "epc_property_uploaded_file_id_unique", + "columns": [ + "uploaded_file_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_property_energy_performance": { + "name": "epc_property_energy_performance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "epc_property_id": { + "name": "epc_property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "energy_rating_current": { + "name": "energy_rating_current", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_current": { + "name": "energy_consumption_current", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "environmental_impact_current": { + "name": "environmental_impact_current", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_cost_current": { + "name": "heating_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lighting_cost_current": { + "name": "lighting_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "hot_water_cost_current": { + "name": "hot_water_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_emissions_current": { + "name": "co2_emissions_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_emissions_current_per_floor_area": { + "name": "co2_emissions_current_per_floor_area", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "current_energy_efficiency_band": { + "name": "current_energy_efficiency_band", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_rating_potential": { + "name": "energy_rating_potential", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_potential": { + "name": "energy_consumption_potential", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "environmental_impact_potential": { + "name": "environmental_impact_potential", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_cost_potential": { + "name": "heating_cost_potential", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lighting_cost_potential": { + "name": "lighting_cost_potential", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "hot_water_cost_potential": { + "name": "hot_water_cost_potential", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_emissions_potential": { + "name": "co2_emissions_potential", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "potential_energy_efficiency_band": { + "name": "potential_energy_efficiency_band", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "epc_property_energy_performance_epc_property_id_epc_property_id_fk": { + "name": "epc_property_energy_performance_epc_property_id_epc_property_id_fk", + "tableFrom": "epc_property_energy_performance", + "columnsFrom": [ + "epc_property_id" + ], + "tableTo": "epc_property", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "epc_property_energy_performance_epc_property_id_unique": { + "name": "epc_property_energy_performance_epc_property_id_unique", + "columns": [ + "epc_property_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_window": { + "name": "epc_window", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "epc_property_id": { + "name": "epc_property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "glazing_gap": { + "name": "glazing_gap", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "orientation": { + "name": "orientation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_type": { + "name": "window_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "glazing_type": { + "name": "glazing_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_width": { + "name": "window_width", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "window_height": { + "name": "window_height", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "draught_proofed": { + "name": "draught_proofed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "window_location": { + "name": "window_location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_wall_type": { + "name": "window_wall_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permanent_shutters_present": { + "name": "permanent_shutters_present", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "frame_material": { + "name": "frame_material", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frame_factor": { + "name": "frame_factor", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "permanent_shutters_insulated": { + "name": "permanent_shutters_insulated", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transmission_u_value": { + "name": "transmission_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "transmission_data_source": { + "name": "transmission_data_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transmission_solar_transmittance": { + "name": "transmission_solar_transmittance", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "epc_window_epc_property_id_epc_property_id_fk": { + "name": "epc_window_epc_property_id_epc_property_id_fk", + "tableFrom": "epc_window", + "columnsFrom": [ + "epc_property_id" + ], + "tableTo": "epc_property", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.non_intrusive_survey": { + "name": "non_intrusive_survey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "survey_date": { + "name": "survey_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "surveyor": { + "name": "surveyor", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.non_intrusive_survey_notes": { + "name": "non_intrusive_survey_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "survey_id": { + "name": "survey_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "non_intrusive_survey_notes_survey_id_non_intrusive_survey_id_fk": { + "name": "non_intrusive_survey_notes_survey_id_non_intrusive_survey_id_fk", + "tableFrom": "non_intrusive_survey_notes", + "columnsFrom": [ + "survey_id" + ], + "tableTo": "non_intrusive_survey", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property": { + "name": "property", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "creation_status": { + "name": "creation_status", + "type": "creation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "landlord_property_id": { + "name": "landlord_property_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "building_reference_number": { + "name": "building_reference_number", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_inputted_address": { + "name": "user_inputted_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_inputted_postcode": { + "name": "user_inputted_postcode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lexiscore": { + "name": "lexiscore", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "has_pre_condition_report": { + "name": "has_pre_condition_report", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_recommendations": { + "name": "has_recommendations", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "built_form": { + "name": "built_form", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_authority": { + "name": "local_authority", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency": { + "name": "constituency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number_of_rooms": { + "name": "number_of_rooms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "year_built": { + "name": "year_built", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenure": { + "name": "tenure", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_epc_rating": { + "name": "current_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "current_sap_points": { + "name": "current_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "current_valuation": { + "name": "current_valuation", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_sap_point_adjustment": { + "name": "installed_measures_sap_point_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_sap_points_adjusted_for_installed_measures": { + "name": "is_sap_points_adjusted_for_installed_measures", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "original_sap_points": { + "name": "original_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lodged_sap_points": { + "name": "lodged_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lodged_epc_rating": { + "name": "lodged_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_property_portfolio_uprn": { + "name": "uq_property_portfolio_uprn", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "where": "\"property\".\"uprn\" IS NOT NULL", + "concurrently": false + } + }, + "foreignKeys": { + "property_portfolio_id_portfolio_id_fk": { + "name": "property_portfolio_id_portfolio_id_fk", + "tableFrom": "property", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_details_epc": { + "name": "property_details_epc", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "full_address": { + "name": "full_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lodgement_date": { + "name": "lodgement_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_expired": { + "name": "is_expired", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "total_floor_area": { + "name": "total_floor_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "walls": { + "name": "walls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "walls_rating": { + "name": "walls_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "roof": { + "name": "roof", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "roof_rating": { + "name": "roof_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "floor": { + "name": "floor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_rating": { + "name": "floor_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "windows": { + "name": "windows", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "windows_rating": { + "name": "windows_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "heating": { + "name": "heating", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_rating": { + "name": "heating_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "heating_controls": { + "name": "heating_controls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_controls_rating": { + "name": "heating_controls_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "hot_water": { + "name": "hot_water", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hot_water_rating": { + "name": "hot_water_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "lighting": { + "name": "lighting", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lighting_rating": { + "name": "lighting_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "mainfuel": { + "name": "mainfuel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ventilation": { + "name": "ventilation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "solar_pv": { + "name": "solar_pv", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "solar_hot_water": { + "name": "solar_hot_water", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "wind_turbine": { + "name": "wind_turbine", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "floor_height": { + "name": "floor_height", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_heated_rooms": { + "name": "number_heated_rooms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heat_loss_corridor": { + "name": "heat_loss_corridor", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "unheated_corridor_length": { + "name": "unheated_corridor_length", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_open_fireplaces": { + "name": "number_of_open_fireplaces", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_extensions": { + "name": "number_of_extensions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_storeys": { + "name": "number_of_storeys", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mains_gas": { + "name": "mains_gas", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "energy_tariff": { + "name": "energy_tariff", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "primary_energy_consumption": { + "name": "primary_energy_consumption", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_emissions": { + "name": "co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "current_energy_demand": { + "name": "current_energy_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "current_energy_demand_heating_hotwater": { + "name": "current_energy_demand_heating_hotwater", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sap_05_overwritten": { + "name": "sap_05_overwritten", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sap_05_score": { + "name": "sap_05_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "sap_05_epc_rating": { + "name": "sap_05_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "heating_cost_current": { + "name": "heating_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "hot_water_cost_current": { + "name": "hot_water_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lighting_cost_current": { + "name": "lighting_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "appliances_cost_current": { + "name": "appliances_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "gas_standing_charge": { + "name": "gas_standing_charge", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "electricity_standing_charge": { + "name": "electricity_standing_charge", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_co2_emissions": { + "name": "original_co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_primary_energy_consumption": { + "name": "original_primary_energy_consumption", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_current_energy_demand": { + "name": "original_current_energy_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_current_energy_demand_heating_hotwater": { + "name": "original_current_energy_demand_heating_hotwater", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_co2_adjustment": { + "name": "installed_measures_co2_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_energy_demand_adjustment": { + "name": "installed_measures_energy_demand_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_total_energy_bill_adjustment": { + "name": "installed_measures_total_energy_bill_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_heat_demand_adjustment": { + "name": "installed_measures_heat_demand_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_epc_adjusted_for_installed_measures": { + "name": "is_epc_adjusted_for_installed_measures", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "lodged_co2_emissions": { + "name": "lodged_co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lodged_heat_demand": { + "name": "lodged_heat_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "has_been_remodelled": { + "name": "has_been_remodelled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "environment_impact_current": { + "name": "environment_impact_current", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_property_details_epc_property_portfolio": { + "name": "uq_property_details_epc_property_portfolio", + "columns": [ + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "property_details_epc_property_id_property_id_fk": { + "name": "property_details_epc_property_id_property_id_fk", + "tableFrom": "property_details_epc", + "columnsFrom": [ + "property_id" + ], + "tableTo": "property", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "property_details_epc_portfolio_id_portfolio_id_fk": { + "name": "property_details_epc_portfolio_id_portfolio_id_fk", + "tableFrom": "property_details_epc", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_details_meter": { + "name": "property_details_meter", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "energy_supplier": { + "name": "energy_supplier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gas_supplier": { + "name": "gas_supplier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meter_reading_total": { + "name": "meter_reading_total", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "meter_reading_electricity": { + "name": "meter_reading_electricity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "meter_reading_gas": { + "name": "meter_reading_gas", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_details_spatial": { + "name": "property_details_spatial", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "x_coordinate": { + "name": "x_coordinate", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "y_coordinate": { + "name": "y_coordinate", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "conservation_status": { + "name": "conservation_status", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_listed_building": { + "name": "is_listed_building", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_heritage_building": { + "name": "is_heritage_building", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_property_details_spatial_uprn": { + "name": "uq_property_details_spatial_uprn", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_targets": { + "name": "property_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "epc": { + "name": "epc", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "heat_demand": { + "name": "heat_demand", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "property_targets_property_id_property_id_fk": { + "name": "property_targets_property_id_property_id_fk", + "tableFrom": "property_targets", + "columnsFrom": [ + "property_id" + ], + "tableTo": "property", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "property_targets_portfolio_id_portfolio_id_fk": { + "name": "property_targets_portfolio_id_portfolio_id_fk", + "tableFrom": "property_targets", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.installed_measure": { + "name": "installed_measure", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "measure_type": { + "name": "measure_type", + "type": "measure_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "sap_points": { + "name": "sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "carbon_savings": { + "name": "carbon_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "kwh_savings": { + "name": "kwh_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "bill_savings": { + "name": "bill_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "heat_demand_savings": { + "name": "heat_demand_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_installed_measure_uprn": { + "name": "idx_installed_measure_uprn", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_installed_measure_uprn_active": { + "name": "idx_installed_measure_uprn_active", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"installed_measure\".\"is_active\" = true", + "concurrently": false + }, + "idx_installed_measure_measure_type": { + "name": "idx_installed_measure_measure_type", + "columns": [ + { + "expression": "measure_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_installed_measure_uprn_measure": { + "name": "idx_installed_measure_uprn_measure", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "measure_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"installed_measure\".\"is_active\" = true", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plan": { + "name": "plan", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "scenario_id": { + "name": "scenario_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "valuation_increase_lower_bound": { + "name": "valuation_increase_lower_bound", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_increase_upper_bound": { + "name": "valuation_increase_upper_bound", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_increase_average": { + "name": "valuation_increase_average", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_sap_points": { + "name": "post_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_epc_rating": { + "name": "post_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "post_co2_emissions": { + "name": "post_co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_savings": { + "name": "co2_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_energy_bill": { + "name": "post_energy_bill", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_bill_savings": { + "name": "energy_bill_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_energy_consumption": { + "name": "post_energy_consumption", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_savings": { + "name": "energy_consumption_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_post_retrofit": { + "name": "valuation_post_retrofit", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_increase": { + "name": "valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cost_of_works": { + "name": "cost_of_works", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "contingency_cost": { + "name": "contingency_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "plan_type": { + "name": "plan_type", + "type": "plan_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_plan_portfolio_scenario": { + "name": "idx_plan_portfolio_scenario", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scenario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_plan_latest_per_property": { + "name": "idx_plan_latest_per_property", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scenario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "plan_portfolio_id_portfolio_id_fk": { + "name": "plan_portfolio_id_portfolio_id_fk", + "tableFrom": "plan", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "plan_property_id_property_id_fk": { + "name": "plan_property_id_property_id_fk", + "tableFrom": "plan", + "columnsFrom": [ + "property_id" + ], + "tableTo": "property", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "plan_scenario_id_scenario_id_fk": { + "name": "plan_scenario_id_scenario_id_fk", + "tableFrom": "plan", + "columnsFrom": [ + "scenario_id" + ], + "tableTo": "scenario", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plan_recommendations": { + "name": "plan_recommendations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "recommendation_id": { + "name": "recommendation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_plan_recommendations_plan_id": { + "name": "idx_plan_recommendations_plan_id", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_plan_recommendations_plan_rec": { + "name": "idx_plan_recommendations_plan_rec", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recommendation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "plan_recommendations_plan_id_plan_id_fk": { + "name": "plan_recommendations_plan_id_plan_id_fk", + "tableFrom": "plan_recommendations", + "columnsFrom": [ + "plan_id" + ], + "tableTo": "plan", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "plan_recommendations_recommendation_id_recommendation_id_fk": { + "name": "plan_recommendations_recommendation_id_recommendation_id_fk", + "tableFrom": "plan_recommendations", + "columnsFrom": [ + "recommendation_id" + ], + "tableTo": "recommendation", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recommendation": { + "name": "recommendation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "measure_type": { + "name": "measure_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "estimated_cost": { + "name": "estimated_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "contingency_cost": { + "name": "contingency_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "starting_u_value": { + "name": "starting_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "new_u_value": { + "name": "new_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "sap_points": { + "name": "sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "heat_demand": { + "name": "heat_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "kwh_savings": { + "name": "kwh_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rental_yield_increase": { + "name": "rental_yield_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_days": { + "name": "labour_days", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "already_installed": { + "name": "already_installed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "recommendation_property_id_idx": { + "name": "recommendation_property_id_idx", + "columns": [ + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_recommendation_active_defaults": { + "name": "idx_recommendation_active_defaults", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"recommendation\".\"default\" = true AND \"recommendation\".\"already_installed\" = false", + "concurrently": false + }, + "idx_recommendation_active_id_property": { + "name": "idx_recommendation_active_id_property", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "where": "\"recommendation\".\"default\" = true AND \"recommendation\".\"already_installed\" = false", + "concurrently": false + } + }, + "foreignKeys": { + "recommendation_property_id_property_id_fk": { + "name": "recommendation_property_id_property_id_fk", + "tableFrom": "recommendation", + "columnsFrom": [ + "property_id" + ], + "tableTo": "property", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recommendation_materials": { + "name": "recommendation_materials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "recommendation_id": { + "name": "recommendation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "depth": { + "name": "depth", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "quantity_unit": { + "name": "quantity_unit", + "type": "unit_quantity", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "estimated_cost": { + "name": "estimated_cost", + "type": "real", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "recommendation_materials_recommendation_id_idx": { + "name": "recommendation_materials_recommendation_id_idx", + "columns": [ + { + "expression": "recommendation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "recommendation_materials_recommendation_id_recommendation_id_fk": { + "name": "recommendation_materials_recommendation_id_recommendation_id_fk", + "tableFrom": "recommendation_materials", + "columnsFrom": [ + "recommendation_id" + ], + "tableTo": "recommendation", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "recommendation_materials_material_id_material_id_fk": { + "name": "recommendation_materials_material_id_material_id_fk", + "tableFrom": "recommendation_materials", + "columnsFrom": [ + "material_id" + ], + "tableTo": "material", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenario": { + "name": "scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "budget": { + "name": "budget", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "housing_type": { + "name": "housing_type", + "type": "housing_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "goal", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "goal_value": { + "name": "goal_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ashp_cop": { + "name": "ashp_cop", + "type": "real", + "primaryKey": false, + "notNull": false, + "default": 2.8 + }, + "trigger_file_path": { + "name": "trigger_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "already_installed_file_path": { + "name": "already_installed_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "patches_file_path": { + "name": "patches_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "non_invasive_recommendations_file_path": { + "name": "non_invasive_recommendations_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exclusions": { + "name": "exclusions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "multi_plan": { + "name": "multi_plan", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "contingency": { + "name": "contingency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "funding": { + "name": "funding", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_days": { + "name": "labour_days", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "epc_breakdown_pre_retrofit": { + "name": "epc_breakdown_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_breakdown_post_retrofit": { + "name": "epc_breakdown_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number_of_properties": { + "name": "number_of_properties", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "n_units_to_retrofit": { + "name": "n_units_to_retrofit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_pre_retrofit": { + "name": "co2_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_post_retrofit": { + "name": "co2_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_pre_retrofit": { + "name": "energy_bill_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_post_retrofit": { + "name": "energy_bill_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_pre_retrofit": { + "name": "energy_consumption_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_post_retrofit": { + "name": "energy_consumption_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_improvement_per_unit": { + "name": "valuation_improvement_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_unit": { + "name": "cost_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_co2_saved": { + "name": "cost_per_co2_saved", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_sap_point": { + "name": "cost_per_sap_point", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_return_on_investment": { + "name": "valuation_return_on_investment", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_portfolio_id_portfolio_id_fk": { + "name": "scenario_portfolio_id_portfolio_id_fk", + "tableFrom": "scenario", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_removal_requests": { + "name": "property_removal_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'removal'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "requested_by": { + "name": "requested_by", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "original_batch": { + "name": "original_batch", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_removal_requests_deal_id": { + "name": "idx_removal_requests_deal_id", + "columns": [ + { + "expression": "hubspot_deal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_removal_requests_portfolio_id": { + "name": "idx_removal_requests_portfolio_id", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "property_removal_requests_portfolio_id_portfolio_id_fk": { + "name": "property_removal_requests_portfolio_id_portfolio_id_fk", + "tableFrom": "property_removal_requests", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "property_removal_requests_requested_by_user_id_fk": { + "name": "property_removal_requests_requested_by_user_id_fk", + "tableFrom": "property_removal_requests", + "columnsFrom": [ + "requested_by" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "property_removal_requests_reviewed_by_user_id_fk": { + "name": "property_removal_requests_reviewed_by_user_id_fk", + "tableFrom": "property_removal_requests", + "columnsFrom": [ + "reviewed_by" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.solar": { + "name": "solar", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "latitude": { + "name": "latitude", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "google_api_response": { + "name": "google_api_response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.solar_scenario": { + "name": "solar_scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "solar_id": { + "name": "solar_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "scenario_type": { + "name": "scenario_type", + "type": "scenario_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "number_panels": { + "name": "number_panels", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "array_kwhp": { + "name": "array_kwhp", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "lifetime_dc_kwh": { + "name": "lifetime_dc_kwh", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "yearly_dc_kwh": { + "name": "yearly_dc_kwh", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "lifetime_ac_kwh": { + "name": "lifetime_ac_kwh", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "yearly_ac_kwh": { + "name": "yearly_ac_kwh", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "expected_payback_years": { + "name": "expected_payback_years", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "panelled_roof_area": { + "name": "panelled_roof_area", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "solar_scenario_solar_id_solar_id_fk": { + "name": "solar_scenario_solar_id_solar_id_fk", + "tableFrom": "solar_scenario", + "columnsFrom": [ + "solar_id" + ], + "tableTo": "solar", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.survey_requests": { + "name": "survey_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "survey_type": { + "name": "survey_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "requested_by": { + "name": "requested_by", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "fulfilled_at": { + "name": "fulfilled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_survey_requests_deal_id": { + "name": "idx_survey_requests_deal_id", + "columns": [ + { + "expression": "hubspot_deal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_survey_requests_portfolio_id": { + "name": "idx_survey_requests_portfolio_id", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "survey_requests_portfolio_id_portfolio_id_fk": { + "name": "survey_requests_portfolio_id_portfolio_id_fk", + "tableFrom": "survey_requests", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "survey_requests_requested_by_user_id_fk": { + "name": "survey_requests_requested_by_user_id_fk", + "tableFrom": "survey_requests", + "columnsFrom": [ + "requested_by" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sub_task": { + "name": "sub_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_started": { + "name": "job_started", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "job_completed": { + "name": "job_completed", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'In Progress'" + }, + "inputs": { + "name": "inputs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_logs_url": { + "name": "cloud_logs_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sub_task_task_id_tasks_id_fk": { + "name": "sub_task_task_id_tasks_id_fk", + "tableFrom": "sub_task", + "columnsFrom": [ + "task_id" + ], + "tableTo": "tasks", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_source": { + "name": "task_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_started": { + "name": "job_started", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "job_completed": { + "name": "job_completed", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'In Progress'" + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team": { + "name": "team", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_org_id_organisation_id_fk": { + "name": "team_org_id_organisation_id_fk", + "tableFrom": "team", + "columnsFrom": [ + "org_id" + ], + "tableTo": "organisation", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_members_user_id_user_id_fk": { + "name": "team_members_user_id_user_id_fk", + "tableFrom": "team_members", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "team_members_team_id_team_id_fk": { + "name": "team_members_team_id_team_id_fk", + "tableFrom": "team_members", + "columnsFrom": [ + "team_id" + ], + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_portfolio_permissions": { + "name": "team_portfolio_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_portfolio_permissions_team_id_team_id_fk": { + "name": "team_portfolio_permissions_team_id_team_id_fk", + "tableFrom": "team_portfolio_permissions", + "columnsFrom": [ + "team_id" + ], + "tableTo": "team", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + }, + "team_portfolio_permissions_portfolio_id_portfolio_id_fk": { + "name": "team_portfolio_permissions_portfolio_id_portfolio_id_fk", + "tableFrom": "team_portfolio_permissions", + "columnsFrom": [ + "portfolio_id" + ], + "tableTo": "portfolio", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.uploaded_files": { + "name": "uploaded_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "s3_file_bucket": { + "name": "s3_file_bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "s3_file_key": { + "name": "s3_file_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "s3_upload_timestamp": { + "name": "s3_upload_timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "landlord_property_id": { + "name": "landlord_property_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hubspot_listing_id": { + "name": "hubspot_listing_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "file_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "file_source": { + "name": "file_source", + "type": "file_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "measure_name": { + "name": "measure_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "uploaded_files_uploaded_by_user_id_fk": { + "name": "uploaded_files_uploaded_by_user_id_fk", + "tableFrom": "uploaded_files", + "columnsFrom": [ + "uploaded_by" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_defined_deal_measures": { + "name": "user_defined_deal_measures", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "measure_name": { + "name": "measure_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "user_defined_deal_measure_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmed_in_hubspot_at": { + "name": "confirmed_in_hubspot_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_user_defined_deal_measures_deal_id": { + "name": "idx_user_defined_deal_measures_deal_id", + "columns": [ + { + "expression": "hubspot_deal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + }, + "idx_user_defined_deal_measures_source": { + "name": "idx_user_defined_deal_measures_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "user_defined_deal_measures_created_by_user_id_user_id_fk": { + "name": "user_defined_deal_measures_created_by_user_id_user_id_fk", + "tableFrom": "user_defined_deal_measures", + "columnsFrom": [ + "created_by_user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "columnsFrom": [ + "userId" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.authRateLimits": { + "name": "authRateLimits", + "schema": "", + "columns": { + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "window_start": { + "name": "window_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "authRateLimits_scope_key_pk": { + "name": "authRateLimits_scope_key_pk", + "columns": [ + "scope", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "columnsFrom": [ + "userId" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "oauth_id": { + "name": "oauth_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_provider": { + "name": "oauth_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "onboarded": { + "name": "onboarded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_login": { + "name": "last_login", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_profiles": { + "name": "user_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_type": { + "name": "user_type", + "type": "user_profiles_user_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "property_count": { + "name": "property_count", + "type": "user_profiles_property_count", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "goals": { + "name": "goals", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "referral_source": { + "name": "referral_source", + "type": "user_profiles_referral_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "nrla_membership_id": { + "name": "nrla_membership_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "accepted_privacy": { + "name": "accepted_privacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accepted_privacy_at": { + "name": "accepted_privacy_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "marketing_opt_in": { + "name": "marketing_opt_in", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "marketing_opt_in_at": { + "name": "marketing_opt_in_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_profiles_user_id_user_id_fk": { + "name": "user_profiles_user_id_user_id_fk", + "tableFrom": "user_profiles", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.whlg": { + "name": "whlg", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.aspect_type": { + "name": "aspect_type", + "schema": "public", + "values": [ + "material", + "condition", + "type", + "area", + "configuration", + "presence", + "risk", + "severity", + "location", + "finish", + "insulation", + "pointing", + "spalling", + "lintels", + "cladding", + "category", + "quantity", + "adequacy", + "rating", + "strategy", + "extent", + "distribution", + "structure", + "covering", + "fire_rating", + "external_decoration", + "work_required", + "age_band", + "construction_type", + "classification", + "system" + ] + }, + "public.element_type": { + "name": "element_type", + "schema": "public", + "values": [ + "property", + "property_construction_type", + "property_classification", + "property_age_band", + "storey_count", + "floor_level", + "floor_level_front_door", + "accessible_housing_register", + "asbestos", + "quality_standard", + "ccu", + "passenger_lift", + "stairlift", + "disabled_hoist_tracking", + "disabled_facilities", + "steps_to_front_door", + "roof", + "pitched_roof_covering", + "flat_roof_covering", + "rainwater_goods", + "loft_insulation", + "porch_canopy", + "chimney", + "fascia", + "soffit", + "fascia_soffit_bargeboards", + "gutters", + "store_roof", + "garage_roof", + "garage_and_store_roof", + "external_wall", + "external_noise_insulation", + "primary_wall", + "secondary_wall", + "downpipes", + "external_decoration", + "cladding", + "spandrel_panels", + "garage_walls", + "party_wall_fire_break", + "external_brickwork_pointing", + "internal_downpipes_external_area", + "external_windows", + "communal_windows", + "secondary_glazing", + "store_windows", + "garage_windows", + "garage_and_store_windows", + "external_door", + "front_door", + "rear_door", + "store_door", + "garage_door", + "garage_and_store_door", + "communal_entrance_door", + "main_door", + "block_entrance_door", + "lintel", + "patio_french_door", + "door_entry_handset", + "paths_and_hardstandings", + "parking_areas", + "boundary_walls", + "front_fencing", + "rear_fencing", + "side_fencing", + "rear_gate", + "front_gate", + "gates", + "retaining_walls", + "private_balcony", + "balcony_balustrade", + "outbuildings", + "garage_structure", + "paving", + "roads", + "soil_and_vent", + "solar_thermals", + "drop_kerb", + "outbuilding_overhaul", + "external_structural_defects", + "access_ramp", + "kitchen", + "kitchen_space_layout", + "tenant_installed_kitchen", + "kitchen_extractor_fan", + "bathroom", + "secondary_bathroom", + "secondary_toilet", + "bathroom_extractor_fan", + "additional_wc_or_whb", + "bathroom_remaining_life_source", + "kitchen_remaining_life_source", + "central_heating", + "heating_boiler", + "heating_distribution", + "secondary_heating", + "hot_water_system", + "cold_water_storage", + "heating_system", + "boiler_fuel", + "water_heating", + "programmable_heating", + "community_heating", + "gas_available", + "heat_recovery_units", + "heating_improvements", + "electrical_wiring", + "consumer_unit", + "smoke_detection", + "heat_detection", + "carbon_monoxide_detection", + "fire_door_rating", + "fire_risk_assessment", + "internal_wiring", + "electrics", + "communal_heating", + "communal_boiler", + "communal_electrics", + "communal_fire_alarm", + "communal_emergency_lighting", + "communal_door_entry", + "communal_cctv", + "communal_bin_store", + "communal_bin_store_doors", + "communal_bin_store_walls", + "communal_bin_store_roof", + "communal_refuse_chute", + "communal_floor_covering", + "communal_kitchen", + "communal_bathroom", + "communal_toilets", + "communal_gates", + "communal_lift", + "communal_passenger_lift", + "communal_balcony_walkway", + "communal_entrance", + "communal_internal_decorations", + "communal_internal_floor", + "communal_walkways", + "communal_external_doors", + "communal_stairs", + "communal_aerial", + "communal_aov", + "communal_internal_doors", + "communal_lateral_mains", + "communal_lighting", + "communal_lighting_conductor", + "communal_store_roof", + "communal_store_walls", + "communal_store_doors", + "communal_warden_call_system", + "communal_bms", + "communal_booster_pump", + "communal_dry_riser", + "communal_wet_riser", + "communal_cold_water_storage", + "communal_sprinkler", + "communal_plug_sockets", + "communal_circulation_space", + "ffhh_damp", + "ffhh_hold_and_cold_water", + "ffhh_drainage_lavatories", + "ffhh_neglected", + "ffhh_natural_light", + "ffhh_ventilation", + "ffhh_food_prep_and_washup", + "ffhh_unsafe_layout", + "ffhh_unstable_building", + "hhsrs_damp_and_mould", + "hhsrs_excess_cold", + "hhsrs_excess_heat", + "hhsrs_asbestos_and_mmf", + "hhsrs_biocides", + "hhsrs_carbon_monoxide", + "hhsrs_lead", + "hhsrs_radiation", + "hhsrs_uncombusted_fuel_gas", + "hhsrs_volatile_organic_compounds", + "hhsrs_crowding_and_space", + "hhsrs_entry_by_intruders", + "hhsrs_lighting", + "hhsrs_noise", + "hhsrs_domestic_hygiene_pests_refuse", + "hhsrs_food_safety", + "hhsrs_personal_hygiene_sanitation", + "hhsrs_water_supply", + "hhsrs_falls_associated_with_baths", + "hhsrs_falls_on_level_surfaces", + "hhsrs_falls_on_stairs", + "hhsrs_falls_between_levels", + "hhsrs_electrical_hazards", + "hhsrs_fire", + "hhsrs_flames_hot_surfaces", + "hhsrs_collision_and_entrapment", + "hhsrs_collision_hazards_low_headroom", + "hhsrs_explosions", + "hhsrs_ergonomics", + "hhsrs_structural_collapse", + "hhsrs_amenities" + ] + }, + "public.document_type": { + "name": "document_type", + "schema": "public", + "values": [ + "EPR", + "Condition Report", + "Evidence Report", + "Summary Information", + "Floor Plan", + "Scenario Draft EPC", + "Scenario Site Notes" + ] + }, + "public.scheme": { + "name": "scheme", + "schema": "public", + "values": [ + "eco4", + "gbis", + "whlg", + "none" + ] + }, + "public.inspection_archetype_2": { + "name": "inspection_archetype_2", + "schema": "public", + "values": [ + "detached", + "mid-terrace", + "enclosed mid-terrace", + "end-terrace", + "enclosed end-terrace", + "semi-detached" + ] + }, + "public.inspection_archetype": { + "name": "inspection_archetype", + "schema": "public", + "values": [ + "Bungalow", + "Flat", + "Maisonette", + "House", + "non-domestic" + ] + }, + "public.inspection_borescoped": { + "name": "inspection_borescoped", + "schema": "public", + "values": [ + "yes", + "no", + "refused" + ] + }, + "public.inspections_access_issues": { + "name": "inspections_access_issues", + "schema": "public", + "values": [ + "see notes", + "damp issues", + "foliage on walls", + "bushes against wall", + "trees around/anove property", + "high rise block flats/maisonettes", + "conservatory", + "lean-to", + "garage", + "extension", + "decking", + "shed against wall" + ] + }, + "public.inspections_cladding": { + "name": "inspections_cladding", + "schema": "public", + "values": [ + "none", + "cladded with “sufficient space to fill the wall”", + "cladded with “insufficient space to fill the wall”" + ] + }, + "public.inspections_insulation_material": { + "name": "inspections_insulation_material", + "schema": "public", + "values": [ + "empty 50-90", + "empty 100+", + "empty 30-40", + "empty less than 30", + "loose fibre/wool", + "eps/celo/king", + "fibre batts - with cavity", + "fibre batts - no cavity", + "loose bead", + "glued bead", + "formaldehyde", + "bubble wrap", + "poly chunks" + ] + }, + "public.inspections_rendered": { + "name": "inspections_rendered", + "schema": "public", + "values": [ + "no render", + "rendered with “insufficient” space between dpc and render", + "rendered with “sufficient” space between dpc and render" + ] + }, + "public.inspections_roof_orientation": { + "name": "inspections_roof_orientation", + "schema": "public", + "values": [ + "north", + "east", + "south", + "west", + "north-east", + "north-west", + "south-east", + "south-west", + "n/s split", + "e/w split", + "ne/sw split", + "nw/se split", + "flat roof", + "no roof", + "roof too small", + "already has solar pv" + ] + }, + "public.inspections_tile_hung": { + "name": "inspections_tile_hung", + "schema": "public", + "values": [ + "yes", + "no", + "first floor flats are tile hung" + ] + }, + "public.inspections_wall_construction": { + "name": "inspections_wall_construction", + "schema": "public", + "values": [ + "cavity", + "solid", + "system built", + "timber framed", + "steel framed", + "re-walled cavity", + "mansard pre-fab", + "mansard ewi", + "mansard re-walled" + ] + }, + "public.inspections_wall_insulation": { + "name": "inspections_wall_insulation", + "schema": "public", + "values": [ + "empty cavity", + "filled at build", + "partial", + "retro drilled", + "ewi", + "iwi", + "solid non-cavity", + "system built", + "timber framed", + "steel framed" + ] + }, + "public.built_form_type": { + "name": "built_form_type", + "schema": "public", + "values": [ + "Detached", + "Semi-Detached", + "Mid-Terrace", + "End-Terrace", + "Enclosed Mid-Terrace", + "Enclosed End-Terrace", + "Not Recorded", + "Unknown" + ] + }, + "public.override_source": { + "name": "override_source", + "schema": "public", + "values": [ + "classifier", + "user" + ] + }, + "public.property_type": { + "name": "property_type", + "schema": "public", + "values": [ + "House", + "Bungalow", + "Flat", + "Maisonette", + "Park home", + "Unknown" + ] + }, + "public.roof_type": { + "name": "roof_type", + "schema": "public", + "values": [ + "Flat, insulated", + "Flat, insulated (assumed)", + "Flat, limited insulation", + "Flat, limited insulation (assumed)", + "Flat, no insulation", + "Flat, no insulation (assumed)", + "Pitched, insulated", + "Pitched, insulated (assumed)", + "Pitched, insulated at rafters", + "Pitched, limited insulation", + "Pitched, limited insulation (assumed)", + "Pitched, no insulation", + "Pitched, no insulation (assumed)", + "Pitched, Unknown loft insulation", + "Pitched, 0 mm loft insulation", + "Pitched, 12 mm loft insulation", + "Pitched, 25 mm loft insulation", + "Pitched, 50 mm loft insulation", + "Pitched, 75 mm loft insulation", + "Pitched, 100 mm loft insulation", + "Pitched, 125 mm loft insulation", + "Pitched, 150 mm loft insulation", + "Pitched, 175 mm loft insulation", + "Pitched, 200 mm loft insulation", + "Pitched, 225 mm loft insulation", + "Pitched, 250 mm loft insulation", + "Pitched, 270 mm loft insulation", + "Pitched, 300 mm loft insulation", + "Pitched, 350 mm loft insulation", + "Pitched, 400 mm loft insulation", + "Pitched, 400+ mm loft insulation", + "Roof room(s), insulated", + "Roof room(s), insulated (assumed)", + "Roof room(s), limited insulation", + "Roof room(s), limited insulation (assumed)", + "Roof room(s), no insulation", + "Roof room(s), no insulation (assumed)", + "Roof room(s), ceiling insulated", + "Roof room(s), thatched", + "Roof room(s), thatched with additional insulation", + "Thatched", + "Thatched, with additional insulation", + "(another dwelling above)", + "(same dwelling above)", + "(other premises above)", + "(another premises above)", + "Another Premises Above", + "Unknown" + ] + }, + "public.wall_type": { + "name": "wall_type", + "schema": "public", + "values": [ + "Cavity wall, filled cavity", + "Cavity wall, as built, insulated (assumed)", + "Cavity wall, as built, no insulation (assumed)", + "Cavity wall, as built, partial insulation (assumed)", + "Cavity wall, with internal insulation", + "Cavity wall, with external insulation", + "Cavity wall, filled cavity and internal insulation", + "Cavity wall, filled cavity and external insulation", + "Solid brick, as built, no insulation (assumed)", + "Solid brick, as built, insulated (assumed)", + "Solid brick, as built, partial insulation (assumed)", + "Solid brick, with internal insulation", + "Solid brick, with external insulation", + "Timber frame, as built, no insulation (assumed)", + "Timber frame, as built, insulated (assumed)", + "Timber frame, as built, partial insulation (assumed)", + "Timber frame, with additional insulation", + "Sandstone, as built, no insulation (assumed)", + "Sandstone, as built, insulated (assumed)", + "Sandstone, as built, partial insulation (assumed)", + "Sandstone, with internal insulation", + "Sandstone, with external insulation", + "Granite or whin, as built, no insulation (assumed)", + "Granite or whin, as built, insulated (assumed)", + "Granite or whin, as built, partial insulation (assumed)", + "Granite or whin, with internal insulation", + "Granite or whin, with external insulation", + "System built, as built, no insulation (assumed)", + "System built, as built, insulated (assumed)", + "System built, as built, partial insulation (assumed)", + "System built, with internal insulation", + "System built, with external insulation", + "Park home wall, as built", + "Park home wall, with internal insulation", + "Park home wall, with external insulation", + "Cob, as built", + "Cob, with internal insulation", + "Cob, with external insulation", + "Curtain wall", + "Curtain Wall, as built, no insulation (assumed)", + "Curtain Wall, as built, insulated (assumed)", + "Curtain Wall, filled cavity", + "Curtain Wall, with internal insulation", + "Basement wall", + "Basement wall, as built", + "Unknown" + ] + }, + "public.cost_unit": { + "name": "cost_unit", + "schema": "public", + "values": [ + "gbp_sq_meter", + "gbp_per_unit", + "gbp_per_m2", + "gbp_per_m" + ] + }, + "public.depth_unit": { + "name": "depth_unit", + "schema": "public", + "values": [ + "mm" + ] + }, + "public.type": { + "name": "type", + "schema": "public", + "values": [ + "suspended_floor_insulation", + "solid_floor_insulation", + "external_wall_insulation", + "internal_wall_insulation", + "cavity_wall_insulation", + "mechanical_ventilation", + "loft_insulation", + "exposed_floor_insulation", + "flat_roof_insulation", + "room_roof_insulation", + "cavity_wall_extraction", + "iwi_wall_demolition", + "iwi_vapour_barrier", + "iwi_redecoration", + "suspended_floor_demolition", + "suspended_floor_redecoration", + "suspended_floor_vapour_barrier", + "solid_floor_demolition", + "solid_floor_preparation", + "solid_floor_vapour_barrier", + "solid_floor_redecoration", + "ewi_wall_demolition", + "ewi_wall_preparation", + "ewi_wall_redecoration", + "low_energy_lighting_installation", + "flat_roof_preparation", + "flat_roof_vapour_barrier", + "flat_roof_waterproofing", + "windows_glazing", + "secondary_glazing", + "double_glazing", + "trickle_vent", + "door_undercut", + "solar_pv", + "solar_battery", + "scaffolding", + "high_heat_retention_storage_heaters", + "air_source_heat_pump", + "boiler_upgrade", + "roomstat_programmer_trvs", + "time_temperature_zone_control", + "sealing_fireplace" + ] + }, + "public.r_value_unit": { + "name": "r_value_unit", + "schema": "public", + "values": [ + "square_meter_kelvin_per_watt" + ] + }, + "public.size_unit": { + "name": "size_unit", + "schema": "public", + "values": [ + "kWp", + "kW", + "watt", + "storey" + ] + }, + "public.thermal_conductivity_unit": { + "name": "thermal_conductivity_unit", + "schema": "public", + "values": [ + "watt_per_meter_kelvin" + ] + }, + "public.goal": { + "name": "goal", + "schema": "public", + "values": [ + "Valuation Improvement", + "Increasing EPC", + "Reducing CO2 emissions", + "Energy Savings", + "None" + ] + }, + "public.portfolio_capability": { + "name": "portfolio_capability", + "schema": "public", + "values": [ + "approver", + "contractor" + ] + }, + "public.role": { + "name": "role", + "schema": "public", + "values": [ + "creator", + "admin", + "read", + "write" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "scoping", + "survey", + "assessment", + "tendering", + "project underway", + "completion; status: on track", + "completion; status: delayed", + "completion; status: at risk", + "completion; status: completed", + "needs review" + ] + }, + "public.energy_element_type": { + "name": "energy_element_type", + "schema": "public", + "values": [ + "roof", + "wall", + "floor", + "main_heating", + "window", + "lighting", + "hot_water", + "secondary_heating", + "main_heating_controls" + ] + }, + "public.epc": { + "name": "epc", + "schema": "public", + "values": [ + "A", + "B", + "C", + "D", + "E", + "F", + "G" + ] + }, + "public.creation_status": { + "name": "creation_status", + "schema": "public", + "values": [ + "LOADING", + "READY", + "ERROR" + ] + }, + "public.housing_type": { + "name": "housing_type", + "schema": "public", + "values": [ + "Private", + "Social" + ] + }, + "public.measure_type": { + "name": "measure_type", + "schema": "public", + "values": [ + "air_source_heat_pump", + "boiler_upgrade", + "high_heat_retention_storage_heaters", + "secondary_heating", + "roomstat_programmer_trvs", + "time_temperature_zone_control", + "cylinder_thermostat", + "cavity_wall_insulation", + "extension_cavity_wall_insulation", + "external_wall_insulation", + "internal_wall_insulation", + "loft_insulation", + "flat_roof_insulation", + "room_roof_insulation", + "solid_floor_insulation", + "suspended_floor_insulation", + "double_glazing", + "secondary_glazing", + "draught_proofing", + "mechanical_ventilation", + "low_energy_lighting", + "solar_pv", + "hot_water_tank_insulation", + "sealing_open_fireplace" + ] + }, + "public.plan_type": { + "name": "plan_type", + "schema": "public", + "values": [ + "solar_eco4", + "solar_hhrsh_eco4", + "empty_cavity_eco", + "partial_cavity_eco", + "extraction_eco" + ] + }, + "public.unit_quantity": { + "name": "unit_quantity", + "schema": "public", + "values": [ + "m2", + "part", + "kwp" + ] + }, + "public.scenario_type": { + "name": "scenario_type", + "schema": "public", + "values": [ + "unit", + "building" + ] + }, + "public.source": { + "name": "source", + "schema": "public", + "values": [ + "portfolio_id", + "hubspot_deal_id" + ] + }, + "public.file_source": { + "name": "file_source", + "schema": "public", + "values": [ + "pas hub", + "sharepoint", + "hubspot", + "ecmk", + "contractor", + "magic_plan", + "coordination_hub" + ] + }, + "public.file_type": { + "name": "file_type", + "schema": "public", + "values": [ + "photo_pack", + "site_note", + "rd_sap_site_note", + "pas_2023_ventilation", + "pas_2023_condition", + "pas_significance", + "par_photo_pack", + "pas_2023_property", + "pas_2023_occupancy", + "ecmk_site_note", + "ecmk_rd_sap_site_note", + "ecmk_survey_xml", + "pre_photo", + "mid_photo", + "post_photo", + "loft_hatch_photo", + "dmev_photos", + "door_undercut_photos", + "trickle_vent_photos", + "pre_installation_building_inspection", + "point_of_work_risk_assessment", + "claim_of_compliance", + "mcs_compliance_certificate", + "certificate_of_conformity", + "minor_works_electrical_certificate", + "trustmark_licence_numbers", + "operative_competency", + "ventilation_assessment_checklist", + "anemometer_readings", + "commissioning_records", + "part_f_ventilation_document", + "handover_pack", + "insurance_guarantee", + "workmanship_warranty", + "g98_notification", + "installer_qualifications", + "installer_feedback", + "contractor_other", + "magic_plan_json", + "improvement_option_evaluation", + "medium_term_improvement_plan", + "retrofit_design_doc" + ] + }, + "public.user_defined_deal_measure_source": { + "name": "user_defined_deal_measure_source", + "schema": "public", + "values": [ + "instructed", + "pibi_ordered" + ] + }, + "public.user_profiles_property_count": { + "name": "user_profiles_property_count", + "schema": "public", + "values": [ + "1", + "2–5", + "6–20", + "21+", + "1–50", + "51–100", + "101–300", + "301–1000", + "1000+" + ] + }, + "public.user_profiles_referral_source": { + "name": "user_profiles_referral_source", + "schema": "public", + "values": [ + "search", + "social_media", + "NRLA", + "partner", + "word_of_mouth", + "other" + ] + }, + "public.user_profiles_user_type": { + "name": "user_profiles_user_type", + "schema": "public", + "values": [ + "private_landlord", + "private_tenant", + "social_landlord", + "social_tenant", + "homeowner", + "other" + ] + } + }, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/app/db/migrations/meta/0216_snapshot.json b/src/app/db/migrations/meta/0216_snapshot.json new file mode 100644 index 00000000..7cb686e6 --- /dev/null +++ b/src/app/db/migrations/meta/0216_snapshot.json @@ -0,0 +1,10131 @@ +{ + "id": "a8bd66c7-30ab-40bf-ae3d-ea083d04b522", + "prevId": "25c3ba3e-0d41-48ac-803e-5af7e50e052f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.postcode_search": { + "name": "postcode_search", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "result_data": { + "name": "result_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_updated_at": { + "name": "last_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postcode_search_postcode_unique": { + "name": "postcode_search_postcode_unique", + "nullsNotDistinct": false, + "columns": [ + "postcode" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deal_measure_approval_events": { + "name": "deal_measure_approval_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "measure_name": { + "name": "measure_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acted_by": { + "name": "acted_by", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "acted_at": { + "name": "acted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deal_measure_events_deal_id": { + "name": "idx_deal_measure_events_deal_id", + "columns": [ + { + "expression": "hubspot_deal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deal_measure_events_acted_at": { + "name": "idx_deal_measure_events_acted_at", + "columns": [ + { + "expression": "acted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deal_measure_approval_events_acted_by_user_id_fk": { + "name": "deal_measure_approval_events_acted_by_user_id_fk", + "tableFrom": "deal_measure_approval_events", + "tableTo": "user", + "columnsFrom": [ + "acted_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deal_measure_approvals": { + "name": "deal_measure_approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "measure_name": { + "name": "measure_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_approved": { + "name": "is_approved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "approved_by": { + "name": "approved_by", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deal_measure_approvals_deal_id": { + "name": "idx_deal_measure_approvals_deal_id", + "columns": [ + { + "expression": "hubspot_deal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deal_measure_approvals_approved_by_user_id_fk": { + "name": "deal_measure_approvals_approved_by_user_id_fk", + "tableFrom": "deal_measure_approvals", + "tableTo": "user", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_deal_measure": { + "name": "uq_deal_measure", + "nullsNotDistinct": false, + "columns": [ + "hubspot_deal_id", + "measure_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bulk_address_uploads": { + "name": "bulk_address_uploads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "s3_bucket": { + "name": "s3_bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "s3_key": { + "name": "s3_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ready_for_processing'" + }, + "source_headers": { + "name": "source_headers", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "column_mapping": { + "name": "column_mapping", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "combined_output_s3_uri": { + "name": "combined_output_s3_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.aspect_condition": { + "name": "aspect_condition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "element_id": { + "name": "element_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "aspect_type": { + "name": "aspect_type", + "type": "aspect_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "aspect_instance": { + "name": "aspect_instance", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "install_date": { + "name": "install_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "renewal_year": { + "name": "renewal_year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "comments": { + "name": "comments", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "aspect_condition_element_id_element_id_fk": { + "name": "aspect_condition_element_id_element_id_fk", + "tableFrom": "aspect_condition", + "tableTo": "element", + "columnsFrom": [ + "element_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.element": { + "name": "element", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "survey_id": { + "name": "survey_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "element_type": { + "name": "element_type", + "type": "element_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "element_instance": { + "name": "element_instance", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "element_survey_id_property_condition_survey_id_fk": { + "name": "element_survey_id_property_condition_survey_id_fk", + "tableFrom": "element", + "tableTo": "property_condition_survey", + "columnsFrom": [ + "survey_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_condition_survey": { + "name": "property_condition_survey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hubspot_company_data": { + "name": "hubspot_company_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hubspot_deal_data": { + "name": "hubspot_deal_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "deal_id": { + "name": "deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dealname": { + "name": "dealname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dealstage": { + "name": "dealstage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company_id": { + "name": "company_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_code": { + "name": "project_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "landlord_property_id": { + "name": "landlord_property_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "listing_id": { + "name": "listing_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uprn": { + "name": "uprn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_notes": { + "name": "outcome_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "major_condition_issue_description": { + "name": "major_condition_issue_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "major_condition_issue_photos": { + "name": "major_condition_issue_photos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "major_condition_issue_evidence_s3_url": { + "name": "major_condition_issue_evidence_s3_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coordination_status": { + "name": "coordination_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "design_status": { + "name": "design_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "booking_status": { + "name": "booking_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pashub_link": { + "name": "pashub_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sharepoint_link": { + "name": "sharepoint_link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dampmould_growth": { + "name": "dampmould_growth", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pre_sap": { + "name": "pre_sap", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "coordinator": { + "name": "coordinator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mtp_completion_date": { + "name": "mtp_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "mtp_re_model_completion_date": { + "name": "mtp_re_model_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "ioe_v3_completion_date": { + "name": "ioe_v3_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "proposed_measures": { + "name": "proposed_measures", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_package": { + "name": "approved_package", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "designer": { + "name": "designer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "design_type": { + "name": "design_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "design_completion_date": { + "name": "design_completion_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "actual_measures_installed": { + "name": "actual_measures_installed", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installer": { + "name": "installer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installer_handover": { + "name": "installer_handover", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lodgement_status": { + "name": "lodgement_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "measures_lodgement_date": { + "name": "measures_lodgement_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "lodgement_date": { + "name": "lodgement_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "expected_commencement_date": { + "name": "expected_commencement_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "coordination_comments": { + "name": "coordination_comments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surveyor": { + "name": "surveyor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "damp_mould_and_repairs_comments": { + "name": "damp_mould_and_repairs_comments", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "batch": { + "name": "batch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "batch_description": { + "name": "batch_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_reference": { + "name": "block_reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nonfunded_measures": { + "name": "nonfunded_measures", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_prn": { + "name": "epc_prn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "potential_post_sap_score_dropdown": { + "name": "potential_post_sap_score_dropdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ei_score": { + "name": "ei_score", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ei_score__potential_": { + "name": "ei_score__potential_", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_sap_score": { + "name": "epc_sap_score", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_sap_score__potential_": { + "name": "epc_sap_score__potential_", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmed_survey_date": { + "name": "confirmed_survey_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmed_survey_time": { + "name": "confirmed_survey_time", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surveyed_date": { + "name": "surveyed_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "survey_type": { + "name": "survey_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "measures_for_pibi_ordered": { + "name": "measures_for_pibi_ordered", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pibi_order_date": { + "name": "pibi_order_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "pibi_completed_date": { + "name": "pibi_completed_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "property_halted_date": { + "name": "property_halted_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "property_halted_reason": { + "name": "property_halted_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "technical_approved_measures_for_install": { + "name": "technical_approved_measures_for_install", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_to_installer_for_pricing": { + "name": "sent_to_installer_for_pricing", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "domna_survey_required": { + "name": "domna_survey_required", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "domna_survey_type": { + "name": "domna_survey_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domna_survey_date": { + "name": "domna_survey_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hubspot_projects_data": { + "name": "hubspot_projects_data", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hubspot_projects_data_project_id_unique": { + "name": "hubspot_projects_data_project_id_unique", + "nullsNotDistinct": false, + "columns": [ + "project_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hubspot_users": { + "name": "hubspot_users", + "schema": "", + "columns": { + "hubspot_owner_id": { + "name": "hubspot_owner_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_status_tracker": { + "name": "property_status_tracker", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "property_status_tracker_property_id_property_id_fk": { + "name": "property_status_tracker_property_id_property_id_fk", + "tableFrom": "property_status_tracker", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "property_status_tracker_portfolio_id_portfolio_id_fk": { + "name": "property_status_tracker_portfolio_id_portfolio_id_fk", + "tableFrom": "property_status_tracker", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.energy_assessments": { + "name": "energy_assessments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "uprn_source": { + "name": "uprn_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "building_reference_number": { + "name": "building_reference_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_energy_efficiency": { + "name": "current_energy_efficiency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_energy_rating": { + "name": "current_energy_rating", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address1": { + "name": "address1", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address2": { + "name": "address2", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address3": { + "name": "address3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "posttown": { + "name": "posttown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "county": { + "name": "county", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency": { + "name": "constituency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency_label": { + "name": "constituency_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "low_energy_fixed_light_count": { + "name": "low_energy_fixed_light_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "construction_age_band": { + "name": "construction_age_band", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheat_energy_eff": { + "name": "mainheat_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "windows_env_eff": { + "name": "windows_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_energy_eff": { + "name": "lighting_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_impact_potential": { + "name": "environment_impact_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheatcont_description": { + "name": "mainheatcont_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sheating_energy_eff": { + "name": "sheating_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "local_authority": { + "name": "local_authority", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "local_authority_label": { + "name": "local_authority_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fixed_lighting_outlets_count": { + "name": "fixed_lighting_outlets_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_tariff": { + "name": "energy_tariff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mechanical_ventilation": { + "name": "mechanical_ventilation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "solar_water_heating_flag": { + "name": "solar_water_heating_flag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "co2_emissions_potential": { + "name": "co2_emissions_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_heated_rooms": { + "name": "number_heated_rooms", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_description": { + "name": "floor_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_consumption_potential": { + "name": "energy_consumption_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "built_form": { + "name": "built_form", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_open_fireplaces": { + "name": "number_open_fireplaces", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "windows_description": { + "name": "windows_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "glazed_area": { + "name": "glazed_area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inspection_date": { + "name": "inspection_date", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true + }, + "mains_gas_flag": { + "name": "mains_gas_flag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "co2_emiss_curr_per_floor_area": { + "name": "co2_emiss_curr_per_floor_area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heat_loss_corridor": { + "name": "heat_loss_corridor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unheated_corridor_length": { + "name": "unheated_corridor_length", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "flat_storey_count": { + "name": "flat_storey_count", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "roof_energy_eff": { + "name": "roof_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_floor_area": { + "name": "total_floor_area", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_impact_current": { + "name": "environment_impact_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "roof_description": { + "name": "roof_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_energy_eff": { + "name": "floor_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_habitable_rooms": { + "name": "number_habitable_rooms", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_env_eff": { + "name": "hot_water_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheatc_energy_eff": { + "name": "mainheatc_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "main_fuel": { + "name": "main_fuel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_env_eff": { + "name": "lighting_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "windows_energy_eff": { + "name": "windows_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_env_eff": { + "name": "floor_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sheating_env_eff": { + "name": "sheating_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_description": { + "name": "lighting_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "roof_env_eff": { + "name": "roof_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walls_energy_eff": { + "name": "walls_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "photo_supply": { + "name": "photo_supply", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_cost_potential": { + "name": "lighting_cost_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheat_env_eff": { + "name": "mainheat_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "multi_glaze_proportion": { + "name": "multi_glaze_proportion", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "main_heating_controls": { + "name": "main_heating_controls", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "flat_top_storey": { + "name": "flat_top_storey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secondheat_description": { + "name": "secondheat_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walls_env_eff": { + "name": "walls_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_type": { + "name": "transaction_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "extension_count": { + "name": "extension_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mainheatc_env_eff": { + "name": "mainheatc_env_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lmk_key": { + "name": "lmk_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wind_turbine_count": { + "name": "wind_turbine_count", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenure": { + "name": "tenure", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_level": { + "name": "floor_level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "potential_energy_efficiency": { + "name": "potential_energy_efficiency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "potential_energy_rating": { + "name": "potential_energy_rating", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_energy_eff": { + "name": "hot_water_energy_eff", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "low_energy_lighting": { + "name": "low_energy_lighting", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "walls_description": { + "name": "walls_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hotwater_description": { + "name": "hotwater_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "co2_emissions_current": { + "name": "co2_emissions_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heating_cost_current": { + "name": "heating_cost_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heating_cost_potential": { + "name": "heating_cost_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_cost_current": { + "name": "hot_water_cost_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hot_water_cost_potential": { + "name": "hot_water_cost_potential", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lighting_cost_current": { + "name": "lighting_cost_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_consumption_current": { + "name": "energy_consumption_current", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lodgement_date": { + "name": "lodgement_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "lodgement_datetime": { + "name": "lodgement_datetime", + "type": "timestamp (6)", + "primaryKey": false, + "notNull": true + }, + "mainheat_description": { + "name": "mainheat_description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor_height": { + "name": "floor_height", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "glazed_type": { + "name": "glazed_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_location": { + "name": "file_location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surveyor_name": { + "name": "surveyor_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "surveyor_company": { + "name": "surveyor_company", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_heating_kwh": { + "name": "space_heating_kwh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "water_heating_kwh": { + "name": "water_heating_kwh", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_of_doors": { + "name": "number_of_doors", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number_of_insulated_doors": { + "name": "number_of_insulated_doors", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number_of_floors": { + "name": "number_of_floors", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "insulation_wall_area": { + "name": "insulation_wall_area", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "heat_loss_perimeter": { + "name": "heat_loss_perimeter", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "party_wall_length": { + "name": "party_wall_length", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "perimeter": { + "name": "perimeter", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "rooms_with_bath_and_or_shower": { + "name": "rooms_with_bath_and_or_shower", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rooms_with_mixer_shower_no_bath": { + "name": "rooms_with_mixer_shower_no_bath", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "room_with_bath_and_mixer_shower": { + "name": "room_with_bath_and_mixer_shower", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "percent_draftproofed": { + "name": "percent_draftproofed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "has_hot_water_cylinder": { + "name": "has_hot_water_cylinder", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cylinder_insulation_type": { + "name": "cylinder_insulation_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cylinder_insulation_thickness": { + "name": "cylinder_insulation_thickness", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cylinder_thermostat": { + "name": "cylinder_thermostat", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "main_dwelling_ground_floor_area": { + "name": "main_dwelling_ground_floor_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_windows": { + "name": "number_of_windows", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "windows_area": { + "name": "windows_area", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.energy_assessment_documents": { + "name": "energy_assessment_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "energy_assessment_id": { + "name": "energy_assessment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "document_type": { + "name": "document_type", + "type": "document_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "document_location": { + "name": "document_location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "scenario_id": { + "name": "scenario_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "energy_assessment_documents_energy_assessment_id_energy_assessments_id_fk": { + "name": "energy_assessment_documents_energy_assessment_id_energy_assessments_id_fk", + "tableFrom": "energy_assessment_documents", + "tableTo": "energy_assessments", + "columnsFrom": [ + "energy_assessment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "energy_assessment_documents_scenario_id_energy_assessment_scenarios_id_fk": { + "name": "energy_assessment_documents_scenario_id_energy_assessment_scenarios_id_fk", + "tableFrom": "energy_assessment_documents", + "tableTo": "energy_assessment_scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.energy_assessment_scenarios": { + "name": "energy_assessment_scenarios", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "scenario_name": { + "name": "scenario_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_assessment_id": { + "name": "energy_assessment_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "energy_assessment_scenarios_energy_assessment_id_energy_assessments_id_fk": { + "name": "energy_assessment_scenarios_energy_assessment_id_energy_assessments_id_fk", + "tableFrom": "energy_assessment_scenarios", + "tableTo": "energy_assessments", + "columnsFrom": [ + "energy_assessment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_store": { + "name": "epc_store", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "epc_api_created_at": { + "name": "epc_api_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "epc_api": { + "name": "epc_api", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "epc_page_created_at": { + "name": "epc_page_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "epc_page": { + "name": "epc_page", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_page_rrn": { + "name": "epc_page_rrn", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_epc_store_uprn": { + "name": "uq_epc_store_uprn", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files_from_surveyor": { + "name": "files_from_surveyor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "s3_json_url": { + "name": "s3_json_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "files_from_surveyor_portfolio_id_portfolio_id_fk": { + "name": "files_from_surveyor_portfolio_id_portfolio_id_fk", + "tableFrom": "files_from_surveyor", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_from_surveyor_property_id_property_id_fk": { + "name": "files_from_surveyor_property_id_property_id_fk", + "tableFrom": "files_from_surveyor", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.funding_package": { + "name": "funding_package", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "scheme": { + "name": "scheme", + "type": "scheme", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "project_funding": { + "name": "project_funding", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_uplift": { + "name": "total_uplift", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "full_project_score": { + "name": "full_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "partial_project_score": { + "name": "partial_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "uplift_project_score": { + "name": "uplift_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "funding_package_plan_id_plan_id_fk": { + "name": "funding_package_plan_id_plan_id_fk", + "tableFrom": "funding_package", + "tableTo": "plan", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.funding_package_measures": { + "name": "funding_package_measures", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "funding_package_id": { + "name": "funding_package_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "measure": { + "name": "measure", + "type": "type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "innovation_uplift": { + "name": "innovation_uplift", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "partial_project_score": { + "name": "partial_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "uplift_project_score": { + "name": "uplift_project_score", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "funding_package_measures_funding_package_id_funding_package_id_fk": { + "name": "funding_package_measures_funding_package_id_funding_package_id_fk", + "tableFrom": "funding_package_measures", + "tableTo": "funding_package", + "columnsFrom": [ + "funding_package_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "funding_package_measures_material_id_material_id_fk": { + "name": "funding_package_measures_material_id_material_id_fk", + "tableFrom": "funding_package_measures", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inspections": { + "name": "inspections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "archetype": { + "name": "archetype", + "type": "inspection_archetype", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "archetype_2": { + "name": "archetype_2", + "type": "inspection_archetype_2", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "wall_construction": { + "name": "wall_construction", + "type": "inspections_wall_construction", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "insulation": { + "name": "insulation", + "type": "inspections_wall_insulation", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "insulation_material": { + "name": "insulation_material", + "type": "inspections_insulation_material", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "borescoped": { + "name": "borescoped", + "type": "inspection_borescoped", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "roof_orientation": { + "name": "roof_orientation", + "type": "inspections_roof_orientation", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "tile_hung": { + "name": "tile_hung", + "type": "inspections_tile_hung", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "rendered": { + "name": "rendered", + "type": "inspections_rendered", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "cladding": { + "name": "cladding", + "type": "inspections_cladding", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "access_issues": { + "name": "access_issues", + "type": "inspections_access_issues", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "surveyor_name": { + "name": "surveyor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "inspections_property_id_property_id_fk": { + "name": "inspections_property_id_property_id_fk", + "tableFrom": "inspections", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.landlord_built_form_type_overrides": { + "name": "landlord_built_form_type_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "built_form_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "override_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "landlord_built_form_type_overrides_portfolio_id_portfolio_id_fk": { + "name": "landlord_built_form_type_overrides_portfolio_id_portfolio_id_fk", + "tableFrom": "landlord_built_form_type_overrides", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "landlord_built_form_type_overrides_portfolio_description_unique": { + "name": "landlord_built_form_type_overrides_portfolio_description_unique", + "nullsNotDistinct": false, + "columns": [ + "portfolio_id", + "description" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.landlord_property_type_overrides": { + "name": "landlord_property_type_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "property_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "override_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "landlord_property_type_overrides_portfolio_id_portfolio_id_fk": { + "name": "landlord_property_type_overrides_portfolio_id_portfolio_id_fk", + "tableFrom": "landlord_property_type_overrides", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "landlord_property_type_overrides_portfolio_description_unique": { + "name": "landlord_property_type_overrides_portfolio_description_unique", + "nullsNotDistinct": false, + "columns": [ + "portfolio_id", + "description" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.landlord_roof_type_overrides": { + "name": "landlord_roof_type_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "roof_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "override_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "landlord_roof_type_overrides_portfolio_id_portfolio_id_fk": { + "name": "landlord_roof_type_overrides_portfolio_id_portfolio_id_fk", + "tableFrom": "landlord_roof_type_overrides", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "landlord_roof_type_overrides_portfolio_description_unique": { + "name": "landlord_roof_type_overrides_portfolio_description_unique", + "nullsNotDistinct": false, + "columns": [ + "portfolio_id", + "description" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.landlord_wall_type_overrides": { + "name": "landlord_wall_type_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "wall_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "override_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "landlord_wall_type_overrides_portfolio_id_portfolio_id_fk": { + "name": "landlord_wall_type_overrides_portfolio_id_portfolio_id_fk", + "tableFrom": "landlord_wall_type_overrides", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "landlord_wall_type_overrides_portfolio_description_unique": { + "name": "landlord_wall_type_overrides_portfolio_description_unique", + "nullsNotDistinct": false, + "columns": [ + "portfolio_id", + "description" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.magic_plan_door": { + "name": "magic_plan_door", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "magic_plan_room_id": { + "name": "magic_plan_room_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "width_mm": { + "name": "width_mm", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "magic_plan_door_magic_plan_room_id_magic_plan_room_id_fk": { + "name": "magic_plan_door_magic_plan_room_id_magic_plan_room_id_fk", + "tableFrom": "magic_plan_door", + "tableTo": "magic_plan_room", + "columnsFrom": [ + "magic_plan_room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.magic_plan_floor": { + "name": "magic_plan_floor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "magic_plan_plan_id": { + "name": "magic_plan_plan_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "magic_plan_floor_magic_plan_plan_id_magic_plan_plan_id_fk": { + "name": "magic_plan_floor_magic_plan_plan_id_magic_plan_plan_id_fk", + "tableFrom": "magic_plan_floor", + "tableTo": "magic_plan_plan", + "columnsFrom": [ + "magic_plan_plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.magic_plan_plan": { + "name": "magic_plan_plan", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "magic_plan_uid": { + "name": "magic_plan_uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_file_id": { + "name": "uploaded_file_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "magic_plan_plan_uploaded_file_id_uploaded_files_id_fk": { + "name": "magic_plan_plan_uploaded_file_id_uploaded_files_id_fk", + "tableFrom": "magic_plan_plan", + "tableTo": "uploaded_files", + "columnsFrom": [ + "uploaded_file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "magic_plan_plan_magic_plan_uid_unique": { + "name": "magic_plan_plan_magic_plan_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "magic_plan_uid" + ] + }, + "magic_plan_plan_uploaded_file_id_unique": { + "name": "magic_plan_plan_uploaded_file_id_unique", + "nullsNotDistinct": false, + "columns": [ + "uploaded_file_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.magic_plan_room": { + "name": "magic_plan_room", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "magic_plan_floor_id": { + "name": "magic_plan_floor_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "width_m": { + "name": "width_m", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "length_m": { + "name": "length_m", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "area_m2": { + "name": "area_m2", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "magic_plan_room_magic_plan_floor_id_magic_plan_floor_id_fk": { + "name": "magic_plan_room_magic_plan_floor_id_magic_plan_floor_id_fk", + "tableFrom": "magic_plan_room", + "tableTo": "magic_plan_floor", + "columnsFrom": [ + "magic_plan_floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.magic_plan_window": { + "name": "magic_plan_window", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "magic_plan_room_id": { + "name": "magic_plan_room_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "width_m": { + "name": "width_m", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "height_m": { + "name": "height_m", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "area_m2": { + "name": "area_m2", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "opening_type": { + "name": "opening_type", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "magic_plan_window_magic_plan_room_id_magic_plan_room_id_fk": { + "name": "magic_plan_window_magic_plan_room_id_magic_plan_room_id_fk", + "tableFrom": "magic_plan_window", + "tableTo": "magic_plan_room", + "columnsFrom": [ + "magic_plan_room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.material": { + "name": "material", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "depth": { + "name": "depth", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "depth_unit": { + "name": "depth_unit", + "type": "depth_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "cost_unit": { + "name": "cost_unit", + "type": "cost_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "r_value_per_mm": { + "name": "r_value_per_mm", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "r_value_unit": { + "name": "r_value_unit", + "type": "r_value_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "thermal_conductivity": { + "name": "thermal_conductivity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "thermal_conductivity_unit": { + "name": "thermal_conductivity_unit", + "type": "thermal_conductivity_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "prime_material_cost": { + "name": "prime_material_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "material_cost": { + "name": "material_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_cost": { + "name": "labour_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_hours_per_unit": { + "name": "labour_hours_per_unit", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "plant_cost": { + "name": "plant_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_cost": { + "name": "total_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_installer_quote": { + "name": "is_installer_quote", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "innovation_rate": { + "name": "innovation_rate", + "type": "real", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "size": { + "name": "size", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "size_unit": { + "name": "size_unit", + "type": "size_unit", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "includes_scaffolding": { + "name": "includes_scaffolding", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "includes_battery": { + "name": "includes_battery", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "battery_size": { + "name": "battery_size", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organisation": { + "name": "organisation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "hubspot_company_id": { + "name": "hubspot_company_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pibi_requests": { + "name": "pibi_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "measure_name": { + "name": "measure_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordered_at": { + "name": "ordered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_pibi_requests_deal_id": { + "name": "idx_pibi_requests_deal_id", + "columns": [ + { + "expression": "hubspot_deal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pibi_requests_portfolio_id": { + "name": "idx_pibi_requests_portfolio_id", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pibi_requests_portfolio_id_portfolio_id_fk": { + "name": "pibi_requests_portfolio_id_portfolio_id_fk", + "tableFrom": "pibi_requests", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pibi_requests_created_by_user_id_user_id_fk": { + "name": "pibi_requests_created_by_user_id_user_id_fk", + "tableFrom": "pibi_requests", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolio_organisation": { + "name": "portfolio_organisation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "organisation_id": { + "name": "organisation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolio_organisation_portfolio_id_portfolio_id_fk": { + "name": "portfolio_organisation_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolio_organisation", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "portfolio_organisation_organisation_id_organisation_id_fk": { + "name": "portfolio_organisation_organisation_id_organisation_id_fk", + "tableFrom": "portfolio_organisation", + "tableTo": "organisation", + "columnsFrom": [ + "organisation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "portfolio_organisation_portfolio_id_organisation_id_unique": { + "name": "portfolio_organisation_portfolio_id_organisation_id_unique", + "nullsNotDistinct": false, + "columns": [ + "portfolio_id", + "organisation_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolio": { + "name": "portfolio", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "budget": { + "name": "budget", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "goal", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_properties": { + "name": "number_of_properties", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rental_yield_increase": { + "name": "rental_yield_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_days": { + "name": "labour_days", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "epc_breakdown_pre_retrofit": { + "name": "epc_breakdown_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_breakdown_post_retrofit": { + "name": "epc_breakdown_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "n_units_to_retrofit": { + "name": "n_units_to_retrofit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_pre_retrofit": { + "name": "co2_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_post_retrofit": { + "name": "co2_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_pre_retrofit": { + "name": "energy_bill_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_post_retrofit": { + "name": "energy_bill_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_pre_retrofit": { + "name": "energy_consumption_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_post_retrofit": { + "name": "energy_consumption_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_improvement_per_unit": { + "name": "valuation_improvement_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_unit": { + "name": "cost_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_co2_saved": { + "name": "cost_per_co2_saved", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_sap_point": { + "name": "cost_per_sap_point", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_return_on_investment": { + "name": "valuation_return_on_investment", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolio_capabilities": { + "name": "portfolio_capabilities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "capability": { + "name": "capability", + "type": "portfolio_capability", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolio_capabilities_user_id_user_id_fk": { + "name": "portfolio_capabilities_user_id_user_id_fk", + "tableFrom": "portfolio_capabilities", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "portfolio_capabilities_portfolio_id_portfolio_id_fk": { + "name": "portfolio_capabilities_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolio_capabilities", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "portfolio_capabilities_user_id_portfolio_id_capability_unique": { + "name": "portfolio_capabilities_user_id_portfolio_id_capability_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "portfolio_id", + "capability" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolioInvitations": { + "name": "portfolioInvitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolioInvitations_portfolio_id_portfolio_id_fk": { + "name": "portfolioInvitations_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolioInvitations", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "portfolioInvitations_invited_by_user_id_user_id_fk": { + "name": "portfolioInvitations_invited_by_user_id_user_id_fk", + "tableFrom": "portfolioInvitations", + "tableTo": "user", + "columnsFrom": [ + "invited_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "portfolio_invitations_portfolio_email_unique": { + "name": "portfolio_invitations_portfolio_email_unique", + "nullsNotDistinct": false, + "columns": [ + "portfolio_id", + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolioUsers": { + "name": "portfolioUsers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "portfolioUsers_user_id_user_id_fk": { + "name": "portfolioUsers_user_id_user_id_fk", + "tableFrom": "portfolioUsers", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "portfolioUsers_portfolio_id_portfolio_id_fk": { + "name": "portfolioUsers_portfolio_id_portfolio_id_fk", + "tableFrom": "portfolioUsers", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_building_part": { + "name": "epc_building_part", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "epc_property_id": { + "name": "epc_property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "construction_age_band": { + "name": "construction_age_band", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wall_construction": { + "name": "wall_construction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wall_insulation_type": { + "name": "wall_insulation_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "wall_thickness_measured": { + "name": "wall_thickness_measured", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "party_wall_construction": { + "name": "party_wall_construction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "building_part_number": { + "name": "building_part_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "wall_dry_lined": { + "name": "wall_dry_lined", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "wall_thickness_mm": { + "name": "wall_thickness_mm", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "wall_insulation_thickness": { + "name": "wall_insulation_thickness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_heat_loss": { + "name": "floor_heat_loss", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "floor_insulation_thickness": { + "name": "floor_insulation_thickness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "flat_roof_insulation_thickness": { + "name": "flat_roof_insulation_thickness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_type": { + "name": "floor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_construction_type": { + "name": "floor_construction_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_insulation_type_str": { + "name": "floor_insulation_type_str", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_u_value_known": { + "name": "floor_u_value_known", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "roof_construction": { + "name": "roof_construction", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "roof_insulation_location": { + "name": "roof_insulation_location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "roof_insulation_thickness": { + "name": "roof_insulation_thickness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "room_in_roof_floor_area": { + "name": "room_in_roof_floor_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "room_in_roof_construction_age_band": { + "name": "room_in_roof_construction_age_band", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alt_wall_1_area": { + "name": "alt_wall_1_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "alt_wall_1_dry_lined": { + "name": "alt_wall_1_dry_lined", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alt_wall_1_construction": { + "name": "alt_wall_1_construction", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "alt_wall_1_insulation_type": { + "name": "alt_wall_1_insulation_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "alt_wall_1_thickness_measured": { + "name": "alt_wall_1_thickness_measured", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alt_wall_1_insulation_thickness": { + "name": "alt_wall_1_insulation_thickness", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alt_wall_2_area": { + "name": "alt_wall_2_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "alt_wall_2_dry_lined": { + "name": "alt_wall_2_dry_lined", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alt_wall_2_construction": { + "name": "alt_wall_2_construction", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "alt_wall_2_insulation_type": { + "name": "alt_wall_2_insulation_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "alt_wall_2_thickness_measured": { + "name": "alt_wall_2_thickness_measured", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alt_wall_2_insulation_thickness": { + "name": "alt_wall_2_insulation_thickness", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "epc_building_part_epc_property_id_epc_property_id_fk": { + "name": "epc_building_part_epc_property_id_epc_property_id_fk", + "tableFrom": "epc_building_part", + "tableTo": "epc_property", + "columnsFrom": [ + "epc_property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_energy_element": { + "name": "epc_energy_element", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "epc_property_id": { + "name": "epc_property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "element_type": { + "name": "element_type", + "type": "energy_element_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_efficiency_rating": { + "name": "energy_efficiency_rating", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environmental_efficiency_rating": { + "name": "environmental_efficiency_rating", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "epc_energy_element_epc_property_id_epc_property_id_fk": { + "name": "epc_energy_element_epc_property_id_epc_property_id_fk", + "tableFrom": "epc_energy_element", + "tableTo": "epc_property", + "columnsFrom": [ + "epc_property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_flat_details": { + "name": "epc_flat_details", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "epc_property_id": { + "name": "epc_property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "top_storey": { + "name": "top_storey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "flat_location": { + "name": "flat_location", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "heat_loss_corridor": { + "name": "heat_loss_corridor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "storey_count": { + "name": "storey_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "unheated_corridor_length_m": { + "name": "unheated_corridor_length_m", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "epc_flat_details_epc_property_id_epc_property_id_fk": { + "name": "epc_flat_details_epc_property_id_epc_property_id_fk", + "tableFrom": "epc_flat_details", + "tableTo": "epc_property", + "columnsFrom": [ + "epc_property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "epc_flat_details_epc_property_id_unique": { + "name": "epc_flat_details_epc_property_id_unique", + "nullsNotDistinct": false, + "columns": [ + "epc_property_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_floor_dimension": { + "name": "epc_floor_dimension", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "epc_building_part_id": { + "name": "epc_building_part_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "floor": { + "name": "floor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "room_height_m": { + "name": "room_height_m", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "total_floor_area_m2": { + "name": "total_floor_area_m2", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "party_wall_length_m": { + "name": "party_wall_length_m", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "heat_loss_perimeter_m": { + "name": "heat_loss_perimeter_m", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "floor_insulation": { + "name": "floor_insulation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "floor_construction": { + "name": "floor_construction", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "epc_floor_dimension_epc_building_part_id_epc_building_part_id_fk": { + "name": "epc_floor_dimension_epc_building_part_id_epc_building_part_id_fk", + "tableFrom": "epc_floor_dimension", + "tableTo": "epc_building_part", + "columnsFrom": [ + "epc_building_part_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_main_heating_detail": { + "name": "epc_main_heating_detail", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "epc_property_id": { + "name": "epc_property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "has_fghrs": { + "name": "has_fghrs", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "main_fuel_type": { + "name": "main_fuel_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heat_emitter_type": { + "name": "heat_emitter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emitter_temperature": { + "name": "emitter_temperature", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "main_heating_control": { + "name": "main_heating_control", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fan_flue_present": { + "name": "fan_flue_present", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boiler_flue_type": { + "name": "boiler_flue_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "boiler_ignition_type": { + "name": "boiler_ignition_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "central_heating_pump_age": { + "name": "central_heating_pump_age", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "central_heating_pump_age_str": { + "name": "central_heating_pump_age_str", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "main_heating_index_number": { + "name": "main_heating_index_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sap_main_heating_code": { + "name": "sap_main_heating_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "main_heating_number": { + "name": "main_heating_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "main_heating_category": { + "name": "main_heating_category", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "main_heating_fraction": { + "name": "main_heating_fraction", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "main_heating_data_source": { + "name": "main_heating_data_source", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "condensing": { + "name": "condensing", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "weather_compensator": { + "name": "weather_compensator", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "epc_main_heating_detail_epc_property_id_epc_property_id_fk": { + "name": "epc_main_heating_detail_epc_property_id_epc_property_id_fk", + "tableFrom": "epc_main_heating_detail", + "tableTo": "epc_property", + "columnsFrom": [ + "epc_property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_property": { + "name": "epc_property", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "uploaded_file_id": { + "name": "uploaded_file_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "uprn_source": { + "name": "uprn_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "report_reference": { + "name": "report_reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "report_type": { + "name": "report_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assessment_type": { + "name": "assessment_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sap_version": { + "name": "sap_version", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "schema_type": { + "name": "schema_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_versions_original": { + "name": "schema_versions_original", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "calculation_software_version": { + "name": "calculation_software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_line_1": { + "name": "address_line_1", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address_line_2": { + "name": "address_line_2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "post_town": { + "name": "post_town", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region_code": { + "name": "region_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dwelling_type": { + "name": "dwelling_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "built_form": { + "name": "built_form", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenure": { + "name": "tenure", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transaction_type": { + "name": "transaction_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inspection_date": { + "name": "inspection_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completion_date": { + "name": "completion_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "registration_date": { + "name": "registration_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_floor_area_m2": { + "name": "total_floor_area_m2", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "measurement_type": { + "name": "measurement_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "solar_water_heating": { + "name": "solar_water_heating", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "has_hot_water_cylinder": { + "name": "has_hot_water_cylinder", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "has_fixed_air_conditioning": { + "name": "has_fixed_air_conditioning", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "has_conservatory": { + "name": "has_conservatory", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_heated_separate_conservatory": { + "name": "has_heated_separate_conservatory", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "conservatory_type": { + "name": "conservatory_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "door_count": { + "name": "door_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "wet_rooms_count": { + "name": "wet_rooms_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "extensions_count": { + "name": "extensions_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "heated_rooms_count": { + "name": "heated_rooms_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "open_chimneys_count": { + "name": "open_chimneys_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "habitable_rooms_count": { + "name": "habitable_rooms_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "insulated_door_count": { + "name": "insulated_door_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cfl_fixed_lighting_bulbs_count": { + "name": "cfl_fixed_lighting_bulbs_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "led_fixed_lighting_bulbs_count": { + "name": "led_fixed_lighting_bulbs_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "incandescent_fixed_lighting_bulbs_count": { + "name": "incandescent_fixed_lighting_bulbs_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "blocked_chimneys_count": { + "name": "blocked_chimneys_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "draughtproofed_door_count": { + "name": "draughtproofed_door_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "energy_rating_average": { + "name": "energy_rating_average", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "low_energy_fixed_lighting_bulbs_count": { + "name": "low_energy_fixed_lighting_bulbs_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fixed_lighting_outlets_count": { + "name": "fixed_lighting_outlets_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "low_energy_fixed_lighting_outlets_count": { + "name": "low_energy_fixed_lighting_outlets_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_storeys": { + "name": "number_of_storeys", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "any_unheated_rooms": { + "name": "any_unheated_rooms", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "hydro": { + "name": "hydro", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "photovoltaic_array": { + "name": "photovoltaic_array", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "waste_water_heat_recovery": { + "name": "waste_water_heat_recovery", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pressure_test": { + "name": "pressure_test", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pressure_test_certificate_number": { + "name": "pressure_test_certificate_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "percent_draughtproofed": { + "name": "percent_draughtproofed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "insulated_door_u_value": { + "name": "insulated_door_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "multiple_glazed_proportion": { + "name": "multiple_glazed_proportion", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "windows_transmission_u_value": { + "name": "windows_transmission_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "windows_transmission_data_source": { + "name": "windows_transmission_data_source", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "windows_transmission_solar_transmittance": { + "name": "windows_transmission_solar_transmittance", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_mains_gas": { + "name": "energy_mains_gas", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "energy_meter_type": { + "name": "energy_meter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_pv_battery_count": { + "name": "energy_pv_battery_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "energy_wind_turbines_count": { + "name": "energy_wind_turbines_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "energy_gas_smart_meter_present": { + "name": "energy_gas_smart_meter_present", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "energy_is_dwelling_export_capable": { + "name": "energy_is_dwelling_export_capable", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "energy_wind_turbines_terrain_type": { + "name": "energy_wind_turbines_terrain_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "energy_electricity_smart_meter_present": { + "name": "energy_electricity_smart_meter_present", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "energy_pv_connection": { + "name": "energy_pv_connection", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_pv_percent_roof_area": { + "name": "energy_pv_percent_roof_area", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "energy_pv_battery_capacity": { + "name": "energy_pv_battery_capacity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_wind_turbine_hub_height": { + "name": "energy_wind_turbine_hub_height", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_wind_turbine_rotor_diameter": { + "name": "energy_wind_turbine_rotor_diameter", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "heating_cylinder_size": { + "name": "heating_cylinder_size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_water_heating_code": { + "name": "heating_water_heating_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_water_heating_fuel": { + "name": "heating_water_heating_fuel", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_immersion_heating_type": { + "name": "heating_immersion_heating_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_cylinder_insulation_type": { + "name": "heating_cylinder_insulation_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_cylinder_thermostat": { + "name": "heating_cylinder_thermostat", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_secondary_fuel_type": { + "name": "heating_secondary_fuel_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_secondary_heating_type": { + "name": "heating_secondary_heating_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_cylinder_insulation_thickness_mm": { + "name": "heating_cylinder_insulation_thickness_mm", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_wwhrs_index_number_1": { + "name": "heating_wwhrs_index_number_1", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_wwhrs_index_number_2": { + "name": "heating_wwhrs_index_number_2", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_shower_outlet_type": { + "name": "heating_shower_outlet_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_shower_wwhrs": { + "name": "heating_shower_wwhrs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_type": { + "name": "ventilation_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ventilation_draught_lobby": { + "name": "ventilation_draught_lobby", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ventilation_pressure_test": { + "name": "ventilation_pressure_test", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ventilation_open_flues_count": { + "name": "ventilation_open_flues_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_closed_flues_count": { + "name": "ventilation_closed_flues_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_boiler_flues_count": { + "name": "ventilation_boiler_flues_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_other_flues_count": { + "name": "ventilation_other_flues_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_extract_fans_count": { + "name": "ventilation_extract_fans_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_passive_vents_count": { + "name": "ventilation_passive_vents_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_flueless_gas_fires_count": { + "name": "ventilation_flueless_gas_fires_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ventilation_in_pcdf_database": { + "name": "ventilation_in_pcdf_database", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "mechanical_ventilation": { + "name": "mechanical_ventilation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mechanical_vent_duct_type": { + "name": "mechanical_vent_duct_type", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mechanical_vent_duct_placement": { + "name": "mechanical_vent_duct_placement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mechanical_vent_duct_insulation": { + "name": "mechanical_vent_duct_insulation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mechanical_ventilation_index_number": { + "name": "mechanical_ventilation_index_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mechanical_vent_measured_installation": { + "name": "mechanical_vent_measured_installation", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_epc_property_property_portfolio": { + "name": "uq_epc_property_property_portfolio", + "columns": [ + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "epc_property_property_id_property_id_fk": { + "name": "epc_property_property_id_property_id_fk", + "tableFrom": "epc_property", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "epc_property_portfolio_id_portfolio_id_fk": { + "name": "epc_property_portfolio_id_portfolio_id_fk", + "tableFrom": "epc_property", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "epc_property_uploaded_file_id_uploaded_files_id_fk": { + "name": "epc_property_uploaded_file_id_uploaded_files_id_fk", + "tableFrom": "epc_property", + "tableTo": "uploaded_files", + "columnsFrom": [ + "uploaded_file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "epc_property_uploaded_file_id_unique": { + "name": "epc_property_uploaded_file_id_unique", + "nullsNotDistinct": false, + "columns": [ + "uploaded_file_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_property_energy_performance": { + "name": "epc_property_energy_performance", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "epc_property_id": { + "name": "epc_property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "energy_rating_current": { + "name": "energy_rating_current", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_current": { + "name": "energy_consumption_current", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "environmental_impact_current": { + "name": "environmental_impact_current", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_cost_current": { + "name": "heating_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lighting_cost_current": { + "name": "lighting_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "hot_water_cost_current": { + "name": "hot_water_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_emissions_current": { + "name": "co2_emissions_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_emissions_current_per_floor_area": { + "name": "co2_emissions_current_per_floor_area", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "current_energy_efficiency_band": { + "name": "current_energy_efficiency_band", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_rating_potential": { + "name": "energy_rating_potential", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_potential": { + "name": "energy_consumption_potential", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "environmental_impact_potential": { + "name": "environmental_impact_potential", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heating_cost_potential": { + "name": "heating_cost_potential", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lighting_cost_potential": { + "name": "lighting_cost_potential", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "hot_water_cost_potential": { + "name": "hot_water_cost_potential", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_emissions_potential": { + "name": "co2_emissions_potential", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "potential_energy_efficiency_band": { + "name": "potential_energy_efficiency_band", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "epc_property_energy_performance_epc_property_id_epc_property_id_fk": { + "name": "epc_property_energy_performance_epc_property_id_epc_property_id_fk", + "tableFrom": "epc_property_energy_performance", + "tableTo": "epc_property", + "columnsFrom": [ + "epc_property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "epc_property_energy_performance_epc_property_id_unique": { + "name": "epc_property_energy_performance_epc_property_id_unique", + "nullsNotDistinct": false, + "columns": [ + "epc_property_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.epc_window": { + "name": "epc_window", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "epc_property_id": { + "name": "epc_property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "glazing_gap": { + "name": "glazing_gap", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "orientation": { + "name": "orientation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_type": { + "name": "window_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "glazing_type": { + "name": "glazing_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_width": { + "name": "window_width", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "window_height": { + "name": "window_height", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "draught_proofed": { + "name": "draught_proofed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "window_location": { + "name": "window_location", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_wall_type": { + "name": "window_wall_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permanent_shutters_present": { + "name": "permanent_shutters_present", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "frame_material": { + "name": "frame_material", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "frame_factor": { + "name": "frame_factor", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "permanent_shutters_insulated": { + "name": "permanent_shutters_insulated", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transmission_u_value": { + "name": "transmission_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "transmission_data_source": { + "name": "transmission_data_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transmission_solar_transmittance": { + "name": "transmission_solar_transmittance", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "epc_window_epc_property_id_epc_property_id_fk": { + "name": "epc_window_epc_property_id_epc_property_id_fk", + "tableFrom": "epc_window", + "tableTo": "epc_property", + "columnsFrom": [ + "epc_property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.non_intrusive_survey": { + "name": "non_intrusive_survey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "survey_date": { + "name": "survey_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "surveyor": { + "name": "surveyor", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.non_intrusive_survey_notes": { + "name": "non_intrusive_survey_notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "survey_id": { + "name": "survey_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "non_intrusive_survey_notes_survey_id_non_intrusive_survey_id_fk": { + "name": "non_intrusive_survey_notes_survey_id_non_intrusive_survey_id_fk", + "tableFrom": "non_intrusive_survey_notes", + "tableTo": "non_intrusive_survey", + "columnsFrom": [ + "survey_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property": { + "name": "property", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "creation_status": { + "name": "creation_status", + "type": "creation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "landlord_property_id": { + "name": "landlord_property_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "building_reference_number": { + "name": "building_reference_number", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_inputted_address": { + "name": "user_inputted_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_inputted_postcode": { + "name": "user_inputted_postcode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lexiscore": { + "name": "lexiscore", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "has_pre_condition_report": { + "name": "has_pre_condition_report", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "has_recommendations": { + "name": "has_recommendations", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "built_form": { + "name": "built_form", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_authority": { + "name": "local_authority", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "constituency": { + "name": "constituency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number_of_rooms": { + "name": "number_of_rooms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "year_built": { + "name": "year_built", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenure": { + "name": "tenure", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_epc_rating": { + "name": "current_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "current_sap_points": { + "name": "current_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "current_valuation": { + "name": "current_valuation", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_sap_point_adjustment": { + "name": "installed_measures_sap_point_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_sap_points_adjusted_for_installed_measures": { + "name": "is_sap_points_adjusted_for_installed_measures", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "original_sap_points": { + "name": "original_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lodged_sap_points": { + "name": "lodged_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lodged_epc_rating": { + "name": "lodged_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_property_portfolio_uprn": { + "name": "uq_property_portfolio_uprn", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"property\".\"uprn\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "property_portfolio_id_portfolio_id_fk": { + "name": "property_portfolio_id_portfolio_id_fk", + "tableFrom": "property", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_details_epc": { + "name": "property_details_epc", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "full_address": { + "name": "full_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lodgement_date": { + "name": "lodgement_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_expired": { + "name": "is_expired", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "total_floor_area": { + "name": "total_floor_area", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "walls": { + "name": "walls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "walls_rating": { + "name": "walls_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "roof": { + "name": "roof", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "roof_rating": { + "name": "roof_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "floor": { + "name": "floor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "floor_rating": { + "name": "floor_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "windows": { + "name": "windows", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "windows_rating": { + "name": "windows_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "heating": { + "name": "heating", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_rating": { + "name": "heating_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "heating_controls": { + "name": "heating_controls", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heating_controls_rating": { + "name": "heating_controls_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "hot_water": { + "name": "hot_water", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hot_water_rating": { + "name": "hot_water_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "lighting": { + "name": "lighting", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lighting_rating": { + "name": "lighting_rating", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "mainfuel": { + "name": "mainfuel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ventilation": { + "name": "ventilation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "solar_pv": { + "name": "solar_pv", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "solar_hot_water": { + "name": "solar_hot_water", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "wind_turbine": { + "name": "wind_turbine", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "floor_height": { + "name": "floor_height", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_heated_rooms": { + "name": "number_heated_rooms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "heat_loss_corridor": { + "name": "heat_loss_corridor", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "unheated_corridor_length": { + "name": "unheated_corridor_length", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "number_of_open_fireplaces": { + "name": "number_of_open_fireplaces", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_extensions": { + "name": "number_of_extensions", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "number_of_storeys": { + "name": "number_of_storeys", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mains_gas": { + "name": "mains_gas", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "energy_tariff": { + "name": "energy_tariff", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "primary_energy_consumption": { + "name": "primary_energy_consumption", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_emissions": { + "name": "co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "current_energy_demand": { + "name": "current_energy_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "current_energy_demand_heating_hotwater": { + "name": "current_energy_demand_heating_hotwater", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sap_05_overwritten": { + "name": "sap_05_overwritten", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sap_05_score": { + "name": "sap_05_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "sap_05_epc_rating": { + "name": "sap_05_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "heating_cost_current": { + "name": "heating_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "hot_water_cost_current": { + "name": "hot_water_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lighting_cost_current": { + "name": "lighting_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "appliances_cost_current": { + "name": "appliances_cost_current", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "gas_standing_charge": { + "name": "gas_standing_charge", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "electricity_standing_charge": { + "name": "electricity_standing_charge", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_co2_emissions": { + "name": "original_co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_primary_energy_consumption": { + "name": "original_primary_energy_consumption", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_current_energy_demand": { + "name": "original_current_energy_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "original_current_energy_demand_heating_hotwater": { + "name": "original_current_energy_demand_heating_hotwater", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_co2_adjustment": { + "name": "installed_measures_co2_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_energy_demand_adjustment": { + "name": "installed_measures_energy_demand_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_total_energy_bill_adjustment": { + "name": "installed_measures_total_energy_bill_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "installed_measures_heat_demand_adjustment": { + "name": "installed_measures_heat_demand_adjustment", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "is_epc_adjusted_for_installed_measures": { + "name": "is_epc_adjusted_for_installed_measures", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "lodged_co2_emissions": { + "name": "lodged_co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "lodged_heat_demand": { + "name": "lodged_heat_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "has_been_remodelled": { + "name": "has_been_remodelled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "environment_impact_current": { + "name": "environment_impact_current", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_property_details_epc_property_portfolio": { + "name": "uq_property_details_epc_property_portfolio", + "columns": [ + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "property_details_epc_property_id_property_id_fk": { + "name": "property_details_epc_property_id_property_id_fk", + "tableFrom": "property_details_epc", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "property_details_epc_portfolio_id_portfolio_id_fk": { + "name": "property_details_epc_portfolio_id_portfolio_id_fk", + "tableFrom": "property_details_epc", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_details_meter": { + "name": "property_details_meter", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "energy_supplier": { + "name": "energy_supplier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gas_supplier": { + "name": "gas_supplier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meter_reading_total": { + "name": "meter_reading_total", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "meter_reading_electricity": { + "name": "meter_reading_electricity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "meter_reading_gas": { + "name": "meter_reading_gas", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_details_spatial": { + "name": "property_details_spatial", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "x_coordinate": { + "name": "x_coordinate", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "y_coordinate": { + "name": "y_coordinate", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "longitude": { + "name": "longitude", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "conservation_status": { + "name": "conservation_status", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_listed_building": { + "name": "is_listed_building", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_heritage_building": { + "name": "is_heritage_building", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_property_details_spatial_uprn": { + "name": "uq_property_details_spatial_uprn", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_targets": { + "name": "property_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "epc": { + "name": "epc", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "heat_demand": { + "name": "heat_demand", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "property_targets_property_id_property_id_fk": { + "name": "property_targets_property_id_property_id_fk", + "tableFrom": "property_targets", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "property_targets_portfolio_id_portfolio_id_fk": { + "name": "property_targets_portfolio_id_portfolio_id_fk", + "tableFrom": "property_targets", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.installed_measure": { + "name": "installed_measure", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "measure_type": { + "name": "measure_type", + "type": "measure_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "sap_points": { + "name": "sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "carbon_savings": { + "name": "carbon_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "kwh_savings": { + "name": "kwh_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "bill_savings": { + "name": "bill_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "heat_demand_savings": { + "name": "heat_demand_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "idx_installed_measure_uprn": { + "name": "idx_installed_measure_uprn", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_installed_measure_uprn_active": { + "name": "idx_installed_measure_uprn_active", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"installed_measure\".\"is_active\" = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_installed_measure_measure_type": { + "name": "idx_installed_measure_measure_type", + "columns": [ + { + "expression": "measure_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_installed_measure_uprn_measure": { + "name": "idx_installed_measure_uprn_measure", + "columns": [ + { + "expression": "uprn", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "measure_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"installed_measure\".\"is_active\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plan": { + "name": "plan", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "scenario_id": { + "name": "scenario_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "valuation_increase_lower_bound": { + "name": "valuation_increase_lower_bound", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_increase_upper_bound": { + "name": "valuation_increase_upper_bound", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_increase_average": { + "name": "valuation_increase_average", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_sap_points": { + "name": "post_sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_epc_rating": { + "name": "post_epc_rating", + "type": "epc", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "post_co2_emissions": { + "name": "post_co2_emissions", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_savings": { + "name": "co2_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_energy_bill": { + "name": "post_energy_bill", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_bill_savings": { + "name": "energy_bill_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "post_energy_consumption": { + "name": "post_energy_consumption", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_savings": { + "name": "energy_consumption_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_post_retrofit": { + "name": "valuation_post_retrofit", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "valuation_increase": { + "name": "valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cost_of_works": { + "name": "cost_of_works", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "contingency_cost": { + "name": "contingency_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "plan_type": { + "name": "plan_type", + "type": "plan_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_plan_portfolio_scenario": { + "name": "idx_plan_portfolio_scenario", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scenario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plan_latest_per_property": { + "name": "idx_plan_latest_per_property", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scenario_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plan_portfolio_id_portfolio_id_fk": { + "name": "plan_portfolio_id_portfolio_id_fk", + "tableFrom": "plan", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plan_property_id_property_id_fk": { + "name": "plan_property_id_property_id_fk", + "tableFrom": "plan", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plan_scenario_id_scenario_id_fk": { + "name": "plan_scenario_id_scenario_id_fk", + "tableFrom": "plan", + "tableTo": "scenario", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plan_recommendations": { + "name": "plan_recommendations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "plan_id": { + "name": "plan_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "recommendation_id": { + "name": "recommendation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_plan_recommendations_plan_id": { + "name": "idx_plan_recommendations_plan_id", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_plan_recommendations_plan_rec": { + "name": "idx_plan_recommendations_plan_rec", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recommendation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plan_recommendations_plan_id_plan_id_fk": { + "name": "plan_recommendations_plan_id_plan_id_fk", + "tableFrom": "plan_recommendations", + "tableTo": "plan", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plan_recommendations_recommendation_id_recommendation_id_fk": { + "name": "plan_recommendations_recommendation_id_recommendation_id_fk", + "tableFrom": "plan_recommendations", + "tableTo": "recommendation", + "columnsFrom": [ + "recommendation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recommendation": { + "name": "recommendation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "property_id": { + "name": "property_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "measure_type": { + "name": "measure_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "estimated_cost": { + "name": "estimated_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "contingency_cost": { + "name": "contingency_cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "starting_u_value": { + "name": "starting_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "new_u_value": { + "name": "new_u_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "sap_points": { + "name": "sap_points", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "heat_demand": { + "name": "heat_demand", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "kwh_savings": { + "name": "kwh_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "rental_yield_increase": { + "name": "rental_yield_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_days": { + "name": "labour_days", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "already_installed": { + "name": "already_installed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "recommendation_property_id_idx": { + "name": "recommendation_property_id_idx", + "columns": [ + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_recommendation_active_defaults": { + "name": "idx_recommendation_active_defaults", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"recommendation\".\"default\" = true AND \"recommendation\".\"already_installed\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_recommendation_active_id_property": { + "name": "idx_recommendation_active_id_property", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "property_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"recommendation\".\"default\" = true AND \"recommendation\".\"already_installed\" = false", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recommendation_property_id_property_id_fk": { + "name": "recommendation_property_id_property_id_fk", + "tableFrom": "recommendation", + "tableTo": "property", + "columnsFrom": [ + "property_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recommendation_materials": { + "name": "recommendation_materials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "recommendation_id": { + "name": "recommendation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "material_id": { + "name": "material_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "depth": { + "name": "depth", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "quantity_unit": { + "name": "quantity_unit", + "type": "unit_quantity", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "estimated_cost": { + "name": "estimated_cost", + "type": "real", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "recommendation_materials_recommendation_id_idx": { + "name": "recommendation_materials_recommendation_id_idx", + "columns": [ + { + "expression": "recommendation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recommendation_materials_recommendation_id_recommendation_id_fk": { + "name": "recommendation_materials_recommendation_id_recommendation_id_fk", + "tableFrom": "recommendation_materials", + "tableTo": "recommendation", + "columnsFrom": [ + "recommendation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recommendation_materials_material_id_material_id_fk": { + "name": "recommendation_materials_material_id_material_id_fk", + "tableFrom": "recommendation_materials", + "tableTo": "material", + "columnsFrom": [ + "material_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scenario": { + "name": "scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "budget": { + "name": "budget", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "housing_type": { + "name": "housing_type", + "type": "housing_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "goal", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "goal_value": { + "name": "goal_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ashp_cop": { + "name": "ashp_cop", + "type": "real", + "primaryKey": false, + "notNull": false, + "default": 2.8 + }, + "trigger_file_path": { + "name": "trigger_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "already_installed_file_path": { + "name": "already_installed_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "patches_file_path": { + "name": "patches_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "non_invasive_recommendations_file_path": { + "name": "non_invasive_recommendations_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exclusions": { + "name": "exclusions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "multi_plan": { + "name": "multi_plan", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "contingency": { + "name": "contingency", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "funding": { + "name": "funding", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "total_work_hours": { + "name": "total_work_hours", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_savings": { + "name": "energy_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "co2_equivalent_savings": { + "name": "co2_equivalent_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "energy_cost_savings": { + "name": "energy_cost_savings", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "property_valuation_increase": { + "name": "property_valuation_increase", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "labour_days": { + "name": "labour_days", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "epc_breakdown_pre_retrofit": { + "name": "epc_breakdown_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "epc_breakdown_post_retrofit": { + "name": "epc_breakdown_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number_of_properties": { + "name": "number_of_properties", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "n_units_to_retrofit": { + "name": "n_units_to_retrofit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_pre_retrofit": { + "name": "co2_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "co2_per_unit_post_retrofit": { + "name": "co2_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_pre_retrofit": { + "name": "energy_bill_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_bill_per_unit_post_retrofit": { + "name": "energy_bill_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_pre_retrofit": { + "name": "energy_consumption_per_unit_pre_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "energy_consumption_per_unit_post_retrofit": { + "name": "energy_consumption_per_unit_post_retrofit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_improvement_per_unit": { + "name": "valuation_improvement_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_unit": { + "name": "cost_per_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_co2_saved": { + "name": "cost_per_co2_saved", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_per_sap_point": { + "name": "cost_per_sap_point", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valuation_return_on_investment": { + "name": "valuation_return_on_investment", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_portfolio_id_portfolio_id_fk": { + "name": "scenario_portfolio_id_portfolio_id_fk", + "tableFrom": "scenario", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.property_removal_requests": { + "name": "property_removal_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'removal'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "requested_by": { + "name": "requested_by", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "original_batch": { + "name": "original_batch", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_removal_requests_deal_id": { + "name": "idx_removal_requests_deal_id", + "columns": [ + { + "expression": "hubspot_deal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_removal_requests_portfolio_id": { + "name": "idx_removal_requests_portfolio_id", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "property_removal_requests_portfolio_id_portfolio_id_fk": { + "name": "property_removal_requests_portfolio_id_portfolio_id_fk", + "tableFrom": "property_removal_requests", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "property_removal_requests_requested_by_user_id_fk": { + "name": "property_removal_requests_requested_by_user_id_fk", + "tableFrom": "property_removal_requests", + "tableTo": "user", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "property_removal_requests_reviewed_by_user_id_fk": { + "name": "property_removal_requests_reviewed_by_user_id_fk", + "tableFrom": "property_removal_requests", + "tableTo": "user", + "columnsFrom": [ + "reviewed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.solar": { + "name": "solar", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "latitude": { + "name": "latitude", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "google_api_response": { + "name": "google_api_response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.solar_scenario": { + "name": "solar_scenario", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "solar_id": { + "name": "solar_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "scenario_type": { + "name": "scenario_type", + "type": "scenario_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "number_panels": { + "name": "number_panels", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "array_kwhp": { + "name": "array_kwhp", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "lifetime_dc_kwh": { + "name": "lifetime_dc_kwh", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "yearly_dc_kwh": { + "name": "yearly_dc_kwh", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "lifetime_ac_kwh": { + "name": "lifetime_ac_kwh", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "yearly_ac_kwh": { + "name": "yearly_ac_kwh", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "expected_payback_years": { + "name": "expected_payback_years", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "panelled_roof_area": { + "name": "panelled_roof_area", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "solar_scenario_solar_id_solar_id_fk": { + "name": "solar_scenario_solar_id_solar_id_fk", + "tableFrom": "solar_scenario", + "tableTo": "solar", + "columnsFrom": [ + "solar_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.survey_requests": { + "name": "survey_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "survey_type": { + "name": "survey_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "requested_by": { + "name": "requested_by", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "fulfilled_at": { + "name": "fulfilled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_survey_requests_deal_id": { + "name": "idx_survey_requests_deal_id", + "columns": [ + { + "expression": "hubspot_deal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_survey_requests_portfolio_id": { + "name": "idx_survey_requests_portfolio_id", + "columns": [ + { + "expression": "portfolio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "survey_requests_portfolio_id_portfolio_id_fk": { + "name": "survey_requests_portfolio_id_portfolio_id_fk", + "tableFrom": "survey_requests", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "survey_requests_requested_by_user_id_fk": { + "name": "survey_requests_requested_by_user_id_fk", + "tableFrom": "survey_requests", + "tableTo": "user", + "columnsFrom": [ + "requested_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sub_task": { + "name": "sub_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_id": { + "name": "task_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_started": { + "name": "job_started", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "job_completed": { + "name": "job_completed", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'In Progress'" + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cloud_logs_url": { + "name": "cloud_logs_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sub_task_task_id_tasks_id_fk": { + "name": "sub_task_task_id_tasks_id_fk", + "tableFrom": "sub_task", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_source": { + "name": "task_source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_started": { + "name": "job_started", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "job_completed": { + "name": "job_completed", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'In Progress'" + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team": { + "name": "team", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_org_id_organisation_id_fk": { + "name": "team_org_id_organisation_id_fk", + "tableFrom": "team", + "tableTo": "organisation", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_members_user_id_user_id_fk": { + "name": "team_members_user_id_user_id_fk", + "tableFrom": "team_members", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "team_members_team_id_team_id_fk": { + "name": "team_members_team_id_team_id_fk", + "tableFrom": "team_members", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_portfolio_permissions": { + "name": "team_portfolio_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "portfolio_id": { + "name": "portfolio_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_portfolio_permissions_team_id_team_id_fk": { + "name": "team_portfolio_permissions_team_id_team_id_fk", + "tableFrom": "team_portfolio_permissions", + "tableTo": "team", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "team_portfolio_permissions_portfolio_id_portfolio_id_fk": { + "name": "team_portfolio_permissions_portfolio_id_portfolio_id_fk", + "tableFrom": "team_portfolio_permissions", + "tableTo": "portfolio", + "columnsFrom": [ + "portfolio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.uploaded_files": { + "name": "uploaded_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "s3_file_bucket": { + "name": "s3_file_bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "s3_file_key": { + "name": "s3_file_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "s3_upload_timestamp": { + "name": "s3_upload_timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "landlord_property_id": { + "name": "landlord_property_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uprn": { + "name": "uprn", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hubspot_listing_id": { + "name": "hubspot_listing_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "file_type": { + "name": "file_type", + "type": "file_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "file_source": { + "name": "file_source", + "type": "file_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "measure_name": { + "name": "measure_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "uploaded_files_uploaded_by_user_id_fk": { + "name": "uploaded_files_uploaded_by_user_id_fk", + "tableFrom": "uploaded_files", + "tableTo": "user", + "columnsFrom": [ + "uploaded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_defined_deal_measures": { + "name": "user_defined_deal_measures", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "hubspot_deal_id": { + "name": "hubspot_deal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "measure_name": { + "name": "measure_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "user_defined_deal_measure_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "pushed_at": { + "name": "pushed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmed_in_hubspot_at": { + "name": "confirmed_in_hubspot_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_user_defined_deal_measures_deal_id": { + "name": "idx_user_defined_deal_measures_deal_id", + "columns": [ + { + "expression": "hubspot_deal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_defined_deal_measures_source": { + "name": "idx_user_defined_deal_measures_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_defined_deal_measures_created_by_user_id_user_id_fk": { + "name": "user_defined_deal_measures_created_by_user_id_user_id_fk", + "tableFrom": "user_defined_deal_measures", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.authRateLimits": { + "name": "authRateLimits", + "schema": "", + "columns": { + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "window_start": { + "name": "window_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "authRateLimits_scope_key_pk": { + "name": "authRateLimits_scope_key_pk", + "columns": [ + "scope", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "oauth_id": { + "name": "oauth_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_provider": { + "name": "oauth_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "onboarded": { + "name": "onboarded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_login": { + "name": "last_login", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_profiles": { + "name": "user_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_type": { + "name": "user_type", + "type": "user_profiles_user_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "property_count": { + "name": "property_count", + "type": "user_profiles_property_count", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "goals": { + "name": "goals", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "referral_source": { + "name": "referral_source", + "type": "user_profiles_referral_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "nrla_membership_id": { + "name": "nrla_membership_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "accepted_privacy": { + "name": "accepted_privacy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accepted_privacy_at": { + "name": "accepted_privacy_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "marketing_opt_in": { + "name": "marketing_opt_in", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "marketing_opt_in_at": { + "name": "marketing_opt_in_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp (6) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_profiles_user_id_user_id_fk": { + "name": "user_profiles_user_id_user_id_fk", + "tableFrom": "user_profiles", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.whlg": { + "name": "whlg", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "postcode": { + "name": "postcode", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.aspect_type": { + "name": "aspect_type", + "schema": "public", + "values": [ + "material", + "condition", + "type", + "area", + "configuration", + "presence", + "risk", + "severity", + "location", + "finish", + "insulation", + "pointing", + "spalling", + "lintels", + "cladding", + "category", + "quantity", + "adequacy", + "rating", + "strategy", + "extent", + "distribution", + "structure", + "covering", + "fire_rating", + "external_decoration", + "work_required", + "age_band", + "construction_type", + "classification", + "system" + ] + }, + "public.element_type": { + "name": "element_type", + "schema": "public", + "values": [ + "property", + "property_construction_type", + "property_classification", + "property_age_band", + "storey_count", + "floor_level", + "floor_level_front_door", + "accessible_housing_register", + "asbestos", + "quality_standard", + "ccu", + "passenger_lift", + "stairlift", + "disabled_hoist_tracking", + "disabled_facilities", + "steps_to_front_door", + "roof", + "pitched_roof_covering", + "flat_roof_covering", + "rainwater_goods", + "loft_insulation", + "porch_canopy", + "chimney", + "fascia", + "soffit", + "fascia_soffit_bargeboards", + "gutters", + "store_roof", + "garage_roof", + "garage_and_store_roof", + "external_wall", + "external_noise_insulation", + "primary_wall", + "secondary_wall", + "downpipes", + "external_decoration", + "cladding", + "spandrel_panels", + "garage_walls", + "party_wall_fire_break", + "external_brickwork_pointing", + "internal_downpipes_external_area", + "external_windows", + "communal_windows", + "secondary_glazing", + "store_windows", + "garage_windows", + "garage_and_store_windows", + "external_door", + "front_door", + "rear_door", + "store_door", + "garage_door", + "garage_and_store_door", + "communal_entrance_door", + "main_door", + "block_entrance_door", + "lintel", + "patio_french_door", + "door_entry_handset", + "paths_and_hardstandings", + "parking_areas", + "boundary_walls", + "front_fencing", + "rear_fencing", + "side_fencing", + "rear_gate", + "front_gate", + "gates", + "retaining_walls", + "private_balcony", + "balcony_balustrade", + "outbuildings", + "garage_structure", + "paving", + "roads", + "soil_and_vent", + "solar_thermals", + "drop_kerb", + "outbuilding_overhaul", + "external_structural_defects", + "access_ramp", + "kitchen", + "kitchen_space_layout", + "tenant_installed_kitchen", + "kitchen_extractor_fan", + "bathroom", + "secondary_bathroom", + "secondary_toilet", + "bathroom_extractor_fan", + "additional_wc_or_whb", + "bathroom_remaining_life_source", + "kitchen_remaining_life_source", + "central_heating", + "heating_boiler", + "heating_distribution", + "secondary_heating", + "hot_water_system", + "cold_water_storage", + "heating_system", + "boiler_fuel", + "water_heating", + "programmable_heating", + "community_heating", + "gas_available", + "heat_recovery_units", + "heating_improvements", + "electrical_wiring", + "consumer_unit", + "smoke_detection", + "heat_detection", + "carbon_monoxide_detection", + "fire_door_rating", + "fire_risk_assessment", + "internal_wiring", + "electrics", + "communal_heating", + "communal_boiler", + "communal_electrics", + "communal_fire_alarm", + "communal_emergency_lighting", + "communal_door_entry", + "communal_cctv", + "communal_bin_store", + "communal_bin_store_doors", + "communal_bin_store_walls", + "communal_bin_store_roof", + "communal_refuse_chute", + "communal_floor_covering", + "communal_kitchen", + "communal_bathroom", + "communal_toilets", + "communal_gates", + "communal_lift", + "communal_passenger_lift", + "communal_balcony_walkway", + "communal_entrance", + "communal_internal_decorations", + "communal_internal_floor", + "communal_walkways", + "communal_external_doors", + "communal_stairs", + "communal_aerial", + "communal_aov", + "communal_internal_doors", + "communal_lateral_mains", + "communal_lighting", + "communal_lighting_conductor", + "communal_store_roof", + "communal_store_walls", + "communal_store_doors", + "communal_warden_call_system", + "communal_bms", + "communal_booster_pump", + "communal_dry_riser", + "communal_wet_riser", + "communal_cold_water_storage", + "communal_sprinkler", + "communal_plug_sockets", + "communal_circulation_space", + "ffhh_damp", + "ffhh_hold_and_cold_water", + "ffhh_drainage_lavatories", + "ffhh_neglected", + "ffhh_natural_light", + "ffhh_ventilation", + "ffhh_food_prep_and_washup", + "ffhh_unsafe_layout", + "ffhh_unstable_building", + "hhsrs_damp_and_mould", + "hhsrs_excess_cold", + "hhsrs_excess_heat", + "hhsrs_asbestos_and_mmf", + "hhsrs_biocides", + "hhsrs_carbon_monoxide", + "hhsrs_lead", + "hhsrs_radiation", + "hhsrs_uncombusted_fuel_gas", + "hhsrs_volatile_organic_compounds", + "hhsrs_crowding_and_space", + "hhsrs_entry_by_intruders", + "hhsrs_lighting", + "hhsrs_noise", + "hhsrs_domestic_hygiene_pests_refuse", + "hhsrs_food_safety", + "hhsrs_personal_hygiene_sanitation", + "hhsrs_water_supply", + "hhsrs_falls_associated_with_baths", + "hhsrs_falls_on_level_surfaces", + "hhsrs_falls_on_stairs", + "hhsrs_falls_between_levels", + "hhsrs_electrical_hazards", + "hhsrs_fire", + "hhsrs_flames_hot_surfaces", + "hhsrs_collision_and_entrapment", + "hhsrs_collision_hazards_low_headroom", + "hhsrs_explosions", + "hhsrs_ergonomics", + "hhsrs_structural_collapse", + "hhsrs_amenities" + ] + }, + "public.document_type": { + "name": "document_type", + "schema": "public", + "values": [ + "EPR", + "Condition Report", + "Evidence Report", + "Summary Information", + "Floor Plan", + "Scenario Draft EPC", + "Scenario Site Notes" + ] + }, + "public.scheme": { + "name": "scheme", + "schema": "public", + "values": [ + "eco4", + "gbis", + "whlg", + "none" + ] + }, + "public.inspection_archetype_2": { + "name": "inspection_archetype_2", + "schema": "public", + "values": [ + "detached", + "mid-terrace", + "enclosed mid-terrace", + "end-terrace", + "enclosed end-terrace", + "semi-detached" + ] + }, + "public.inspection_archetype": { + "name": "inspection_archetype", + "schema": "public", + "values": [ + "Bungalow", + "Flat", + "Maisonette", + "House", + "non-domestic" + ] + }, + "public.inspection_borescoped": { + "name": "inspection_borescoped", + "schema": "public", + "values": [ + "yes", + "no", + "refused" + ] + }, + "public.inspections_access_issues": { + "name": "inspections_access_issues", + "schema": "public", + "values": [ + "see notes", + "damp issues", + "foliage on walls", + "bushes against wall", + "trees around/anove property", + "high rise block flats/maisonettes", + "conservatory", + "lean-to", + "garage", + "extension", + "decking", + "shed against wall" + ] + }, + "public.inspections_cladding": { + "name": "inspections_cladding", + "schema": "public", + "values": [ + "none", + "cladded with “sufficient space to fill the wall”", + "cladded with “insufficient space to fill the wall”" + ] + }, + "public.inspections_insulation_material": { + "name": "inspections_insulation_material", + "schema": "public", + "values": [ + "empty 50-90", + "empty 100+", + "empty 30-40", + "empty less than 30", + "loose fibre/wool", + "eps/celo/king", + "fibre batts - with cavity", + "fibre batts - no cavity", + "loose bead", + "glued bead", + "formaldehyde", + "bubble wrap", + "poly chunks" + ] + }, + "public.inspections_rendered": { + "name": "inspections_rendered", + "schema": "public", + "values": [ + "no render", + "rendered with “insufficient” space between dpc and render", + "rendered with “sufficient” space between dpc and render" + ] + }, + "public.inspections_roof_orientation": { + "name": "inspections_roof_orientation", + "schema": "public", + "values": [ + "north", + "east", + "south", + "west", + "north-east", + "north-west", + "south-east", + "south-west", + "n/s split", + "e/w split", + "ne/sw split", + "nw/se split", + "flat roof", + "no roof", + "roof too small", + "already has solar pv" + ] + }, + "public.inspections_tile_hung": { + "name": "inspections_tile_hung", + "schema": "public", + "values": [ + "yes", + "no", + "first floor flats are tile hung" + ] + }, + "public.inspections_wall_construction": { + "name": "inspections_wall_construction", + "schema": "public", + "values": [ + "cavity", + "solid", + "system built", + "timber framed", + "steel framed", + "re-walled cavity", + "mansard pre-fab", + "mansard ewi", + "mansard re-walled" + ] + }, + "public.inspections_wall_insulation": { + "name": "inspections_wall_insulation", + "schema": "public", + "values": [ + "empty cavity", + "filled at build", + "partial", + "retro drilled", + "ewi", + "iwi", + "solid non-cavity", + "system built", + "timber framed", + "steel framed" + ] + }, + "public.built_form_type": { + "name": "built_form_type", + "schema": "public", + "values": [ + "Detached", + "Semi-Detached", + "Mid-Terrace", + "End-Terrace", + "Enclosed Mid-Terrace", + "Enclosed End-Terrace", + "Not Recorded", + "Unknown" + ] + }, + "public.override_source": { + "name": "override_source", + "schema": "public", + "values": [ + "classifier", + "user" + ] + }, + "public.property_type": { + "name": "property_type", + "schema": "public", + "values": [ + "House", + "Bungalow", + "Flat", + "Maisonette", + "Park home", + "Unknown" + ] + }, + "public.roof_type": { + "name": "roof_type", + "schema": "public", + "values": [ + "Flat, insulated", + "Flat, insulated (assumed)", + "Flat, limited insulation", + "Flat, limited insulation (assumed)", + "Flat, no insulation", + "Flat, no insulation (assumed)", + "Pitched, insulated", + "Pitched, insulated (assumed)", + "Pitched, insulated at rafters", + "Pitched, limited insulation", + "Pitched, limited insulation (assumed)", + "Pitched, no insulation", + "Pitched, no insulation (assumed)", + "Pitched, Unknown loft insulation", + "Pitched, 0 mm loft insulation", + "Pitched, 12 mm loft insulation", + "Pitched, 25 mm loft insulation", + "Pitched, 50 mm loft insulation", + "Pitched, 75 mm loft insulation", + "Pitched, 100 mm loft insulation", + "Pitched, 125 mm loft insulation", + "Pitched, 150 mm loft insulation", + "Pitched, 175 mm loft insulation", + "Pitched, 200 mm loft insulation", + "Pitched, 225 mm loft insulation", + "Pitched, 250 mm loft insulation", + "Pitched, 270 mm loft insulation", + "Pitched, 300 mm loft insulation", + "Pitched, 350 mm loft insulation", + "Pitched, 400 mm loft insulation", + "Pitched, 400+ mm loft insulation", + "Roof room(s), insulated", + "Roof room(s), insulated (assumed)", + "Roof room(s), limited insulation", + "Roof room(s), limited insulation (assumed)", + "Roof room(s), no insulation", + "Roof room(s), no insulation (assumed)", + "Roof room(s), ceiling insulated", + "Roof room(s), thatched", + "Roof room(s), thatched with additional insulation", + "Thatched", + "Thatched, with additional insulation", + "(another dwelling above)", + "(same dwelling above)", + "(other premises above)", + "(another premises above)", + "Another Premises Above", + "Unknown" + ] + }, + "public.wall_type": { + "name": "wall_type", + "schema": "public", + "values": [ + "Cavity wall, filled cavity", + "Cavity wall, as built, insulated (assumed)", + "Cavity wall, as built, no insulation (assumed)", + "Cavity wall, as built, partial insulation (assumed)", + "Cavity wall, with internal insulation", + "Cavity wall, with external insulation", + "Cavity wall, filled cavity and internal insulation", + "Cavity wall, filled cavity and external insulation", + "Solid brick, as built, no insulation (assumed)", + "Solid brick, as built, insulated (assumed)", + "Solid brick, as built, partial insulation (assumed)", + "Solid brick, with internal insulation", + "Solid brick, with external insulation", + "Timber frame, as built, no insulation (assumed)", + "Timber frame, as built, insulated (assumed)", + "Timber frame, as built, partial insulation (assumed)", + "Timber frame, with additional insulation", + "Sandstone, as built, no insulation (assumed)", + "Sandstone, as built, insulated (assumed)", + "Sandstone, as built, partial insulation (assumed)", + "Sandstone, with internal insulation", + "Sandstone, with external insulation", + "Granite or whin, as built, no insulation (assumed)", + "Granite or whin, as built, insulated (assumed)", + "Granite or whin, as built, partial insulation (assumed)", + "Granite or whin, with internal insulation", + "Granite or whin, with external insulation", + "System built, as built, no insulation (assumed)", + "System built, as built, insulated (assumed)", + "System built, as built, partial insulation (assumed)", + "System built, with internal insulation", + "System built, with external insulation", + "Park home wall, as built", + "Park home wall, with internal insulation", + "Park home wall, with external insulation", + "Cob, as built", + "Cob, with internal insulation", + "Cob, with external insulation", + "Curtain wall", + "Curtain Wall, as built, no insulation (assumed)", + "Curtain Wall, as built, insulated (assumed)", + "Curtain Wall, filled cavity", + "Curtain Wall, with internal insulation", + "Basement wall", + "Basement wall, as built", + "Unknown" + ] + }, + "public.cost_unit": { + "name": "cost_unit", + "schema": "public", + "values": [ + "gbp_sq_meter", + "gbp_per_unit", + "gbp_per_m2", + "gbp_per_m" + ] + }, + "public.depth_unit": { + "name": "depth_unit", + "schema": "public", + "values": [ + "mm" + ] + }, + "public.type": { + "name": "type", + "schema": "public", + "values": [ + "suspended_floor_insulation", + "solid_floor_insulation", + "external_wall_insulation", + "internal_wall_insulation", + "cavity_wall_insulation", + "mechanical_ventilation", + "loft_insulation", + "exposed_floor_insulation", + "flat_roof_insulation", + "room_roof_insulation", + "cavity_wall_extraction", + "iwi_wall_demolition", + "iwi_vapour_barrier", + "iwi_redecoration", + "suspended_floor_demolition", + "suspended_floor_redecoration", + "suspended_floor_vapour_barrier", + "solid_floor_demolition", + "solid_floor_preparation", + "solid_floor_vapour_barrier", + "solid_floor_redecoration", + "ewi_wall_demolition", + "ewi_wall_preparation", + "ewi_wall_redecoration", + "low_energy_lighting_installation", + "flat_roof_preparation", + "flat_roof_vapour_barrier", + "flat_roof_waterproofing", + "windows_glazing", + "secondary_glazing", + "double_glazing", + "trickle_vent", + "door_undercut", + "solar_pv", + "solar_battery", + "scaffolding", + "high_heat_retention_storage_heaters", + "air_source_heat_pump", + "boiler_upgrade", + "roomstat_programmer_trvs", + "time_temperature_zone_control", + "sealing_fireplace" + ] + }, + "public.r_value_unit": { + "name": "r_value_unit", + "schema": "public", + "values": [ + "square_meter_kelvin_per_watt" + ] + }, + "public.size_unit": { + "name": "size_unit", + "schema": "public", + "values": [ + "kWp", + "kW", + "watt", + "storey" + ] + }, + "public.thermal_conductivity_unit": { + "name": "thermal_conductivity_unit", + "schema": "public", + "values": [ + "watt_per_meter_kelvin" + ] + }, + "public.goal": { + "name": "goal", + "schema": "public", + "values": [ + "Valuation Improvement", + "Increasing EPC", + "Reducing CO2 emissions", + "Energy Savings", + "None" + ] + }, + "public.portfolio_capability": { + "name": "portfolio_capability", + "schema": "public", + "values": [ + "approver", + "contractor" + ] + }, + "public.role": { + "name": "role", + "schema": "public", + "values": [ + "creator", + "admin", + "read", + "write" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "scoping", + "survey", + "assessment", + "tendering", + "project underway", + "completion; status: on track", + "completion; status: delayed", + "completion; status: at risk", + "completion; status: completed", + "needs review" + ] + }, + "public.energy_element_type": { + "name": "energy_element_type", + "schema": "public", + "values": [ + "roof", + "wall", + "floor", + "main_heating", + "window", + "lighting", + "hot_water", + "secondary_heating", + "main_heating_controls" + ] + }, + "public.epc": { + "name": "epc", + "schema": "public", + "values": [ + "A", + "B", + "C", + "D", + "E", + "F", + "G" + ] + }, + "public.creation_status": { + "name": "creation_status", + "schema": "public", + "values": [ + "LOADING", + "READY", + "ERROR" + ] + }, + "public.housing_type": { + "name": "housing_type", + "schema": "public", + "values": [ + "Private", + "Social" + ] + }, + "public.measure_type": { + "name": "measure_type", + "schema": "public", + "values": [ + "air_source_heat_pump", + "boiler_upgrade", + "high_heat_retention_storage_heaters", + "secondary_heating", + "roomstat_programmer_trvs", + "time_temperature_zone_control", + "cylinder_thermostat", + "cavity_wall_insulation", + "extension_cavity_wall_insulation", + "external_wall_insulation", + "internal_wall_insulation", + "loft_insulation", + "flat_roof_insulation", + "room_roof_insulation", + "solid_floor_insulation", + "suspended_floor_insulation", + "double_glazing", + "secondary_glazing", + "draught_proofing", + "mechanical_ventilation", + "low_energy_lighting", + "solar_pv", + "hot_water_tank_insulation", + "sealing_open_fireplace" + ] + }, + "public.plan_type": { + "name": "plan_type", + "schema": "public", + "values": [ + "solar_eco4", + "solar_hhrsh_eco4", + "empty_cavity_eco", + "partial_cavity_eco", + "extraction_eco" + ] + }, + "public.unit_quantity": { + "name": "unit_quantity", + "schema": "public", + "values": [ + "m2", + "part", + "kwp" + ] + }, + "public.scenario_type": { + "name": "scenario_type", + "schema": "public", + "values": [ + "unit", + "building" + ] + }, + "public.source": { + "name": "source", + "schema": "public", + "values": [ + "portfolio_id", + "hubspot_deal_id" + ] + }, + "public.file_source": { + "name": "file_source", + "schema": "public", + "values": [ + "pas hub", + "sharepoint", + "hubspot", + "ecmk", + "contractor", + "magic_plan", + "coordination_hub" + ] + }, + "public.file_type": { + "name": "file_type", + "schema": "public", + "values": [ + "photo_pack", + "site_note", + "rd_sap_site_note", + "pas_2023_ventilation", + "pas_2023_condition", + "pas_significance", + "par_photo_pack", + "pas_2023_property", + "pas_2023_occupancy", + "ecmk_site_note", + "ecmk_rd_sap_site_note", + "ecmk_survey_xml", + "pre_photo", + "mid_photo", + "post_photo", + "loft_hatch_photo", + "dmev_photos", + "door_undercut_photos", + "trickle_vent_photos", + "pre_installation_building_inspection", + "point_of_work_risk_assessment", + "claim_of_compliance", + "mcs_compliance_certificate", + "certificate_of_conformity", + "minor_works_electrical_certificate", + "trustmark_licence_numbers", + "operative_competency", + "ventilation_assessment_checklist", + "anemometer_readings", + "commissioning_records", + "part_f_ventilation_document", + "handover_pack", + "insurance_guarantee", + "workmanship_warranty", + "g98_notification", + "installer_qualifications", + "installer_feedback", + "contractor_other", + "magic_plan_json", + "improvement_option_evaluation", + "medium_term_improvement_plan", + "retrofit_design_doc" + ] + }, + "public.user_defined_deal_measure_source": { + "name": "user_defined_deal_measure_source", + "schema": "public", + "values": [ + "instructed", + "pibi_ordered" + ] + }, + "public.user_profiles_property_count": { + "name": "user_profiles_property_count", + "schema": "public", + "values": [ + "1", + "2–5", + "6–20", + "21+", + "1–50", + "51–100", + "101–300", + "301–1000", + "1000+" + ] + }, + "public.user_profiles_referral_source": { + "name": "user_profiles_referral_source", + "schema": "public", + "values": [ + "search", + "social_media", + "NRLA", + "partner", + "word_of_mouth", + "other" + ] + }, + "public.user_profiles_user_type": { + "name": "user_profiles_user_type", + "schema": "public", + "values": [ + "private_landlord", + "private_tenant", + "social_landlord", + "social_tenant", + "homeowner", + "other" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/app/db/migrations/meta/_journal.json b/src/app/db/migrations/meta/_journal.json index 3fc3f2f9..ae83ab61 100644 --- a/src/app/db/migrations/meta/_journal.json +++ b/src/app/db/migrations/meta/_journal.json @@ -1506,6 +1506,20 @@ "when": 1779969672088, "tag": "0214_superb_maelstrom", "breakpoints": true + }, + { + "idx": 215, + "version": "7", + "when": 1779991310301, + "tag": "0215_invert_column_mapping", + "breakpoints": true + }, + { + "idx": 216, + "version": "7", + "when": 1779992128370, + "tag": "0216_add_subtask_service", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/db/schema/tasks/subtask.ts b/src/app/db/schema/tasks/subtask.ts index 0db10f0f..0d65377b 100644 --- a/src/app/db/schema/tasks/subtask.ts +++ b/src/app/db/schema/tasks/subtask.ts @@ -14,6 +14,10 @@ export const subTasks = pgTable("sub_task", { status: text("status").notNull().default("In Progress"), + // Which pipeline this subtask belongs to, e.g. "address2uprn" or + // "landlord_description_overrides". NULL = legacy / address. See ADR-0003. + service: text("service"), + inputs: text("inputs"), // could later change to JSONB if desired outputs: text("outputs"), cloudLogsURL: text("cloud_logs_url"), diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx index 3824b1b4..b298d7f6 100644 --- a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx @@ -41,11 +41,16 @@ export default function OnboardingProgress({ } const { task, upload } = progress.data; - const total = task?.totalSubtasks ?? 0; - const completedSubtasks = task?.completedSubtasks ?? 0; - const failedSubtasks = task?.failedSubtasks ?? 0; + // Address-matching batches drive the bar; classification is shown separately. + const total = task?.addressTotal ?? 0; + const completedSubtasks = task?.addressCompleted ?? 0; + const failedSubtasks = task?.addressFailed ?? 0; const percent = total > 0 ? Math.round((completedSubtasks / total) * 100) : 0; + const classifierTotal = task?.classifierTotal ?? 0; + const classifierCompleted = task?.classifierCompleted ?? 0; + const classifierFailed = task?.classifierFailed ?? 0; + const taskStatus = task?.status.toLowerCase() ?? ""; const taskDone = TASK_TERMINAL_STATUSES.has(taskStatus); const taskFailed = TASK_FAILED_STATUSES.has(taskStatus); @@ -76,6 +81,24 @@ export default function OnboardingProgress({ {failedSubtasks} failed )} + {classifierTotal > 0 && ( + + Classification: + {classifierFailed > 0 ? ( + + + failed + + ) : classifierCompleted >= classifierTotal ? ( + complete + ) : ( + + + running + + )} + + )} {!taskDone && ( diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/map-columns/MapColumnsClient.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/map-columns/MapColumnsClient.tsx index adda7d38..fe729d6d 100644 --- a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/map-columns/MapColumnsClient.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/map-columns/MapColumnsClient.tsx @@ -10,38 +10,14 @@ import { ArrowsRightLeftIcon, } from "@heroicons/react/24/outline"; import { useSetColumnMapping } from "@/lib/bulkUpload/client"; - -const INTERNAL_FIELDS = [ - { value: "address_1", label: "Address 1", required: true }, - { value: "address_2", label: "Address 2", required: false }, - { value: "address_3", label: "Address 3", required: false }, - { value: "postcode", label: "Postcode", required: true }, - { value: "internal_reference", label: "Internal Reference (Optional)", required: false }, - { value: "skip", label: "Skip this column", required: false }, -]; - -const REQUIRED_VALUES = ["address_1", "postcode"]; - -function autoDetect(header: string): string { - const h = header.toLowerCase().replace(/[\s_\-]/g, ""); - if (/^(address|addr)(line)?(1|one)?$/.test(h)) return "address_1"; - if (/^(address|addr)(line)?(2|two)|^street$/.test(h)) return "address_2"; - if (/^(address|addr)(line)?(3|three)|^locality$|^town$|^city$/.test(h)) return "address_3"; - if (/^post(al)?code$|^postcode$|^pcode$/.test(h)) return "postcode"; - if (/^(internal)?ref(erence)?$|^id$/.test(h)) return "internal_reference"; - return "skip"; -} - -function buildInitialMapping( - headers: string[], - existing?: Record -): Record { - const mapping: Record = {}; - for (const h of headers) { - mapping[h] = existing?.[h] ?? autoDetect(h); - } - return mapping; -} +import { + ADDRESS_FIELDS, + CLASSIFIER_FIELDS, + NOT_PROVIDED, + buildInitialMapping, + validateColumnMapping, + type InternalField, +} from "@/lib/bulkUpload/columnFields"; interface Props { portfolioId: string; @@ -59,19 +35,24 @@ export default function MapColumnsClient({ existingMapping, }: Props) { const router = useRouter(); + // mapping: internal field → source CSV header. Unmapped fields are absent. const [mapping, setMapping] = useState>( buildInitialMapping(sourceHeaders, existingMapping) ); const setMappingMutation = useSetColumnMapping(portfolioId, uploadId); - const mappedValues = Object.values(mapping).filter((v) => v !== "skip"); - const missingRequired = REQUIRED_VALUES.filter((r) => !mappedValues.includes(r)); + const validationError = validateColumnMapping(mapping); const submitting = setMappingMutation.isPending; - const error = setMappingMutation.error?.message ?? null; - const canSubmit = missingRequired.length === 0 && !submitting; + const requestError = setMappingMutation.error?.message ?? null; + const canSubmit = validationError === null && !submitting; - function setField(header: string, value: string) { - setMapping((prev) => ({ ...prev, [header]: value })); + function setField(field: string, header: string) { + setMapping((prev) => { + const next = { ...prev }; + if (header === NOT_PROVIDED) delete next[field]; + else next[field] = header; + return next; + }); } function handleSubmit() { @@ -86,6 +67,86 @@ export default function MapColumnsClient({ ); } + function renderRow(field: InternalField) { + const value = mapping[field.value] ?? NOT_PROVIDED; + const isMapped = value !== NOT_PROVIDED; + return ( +
+ {/* Internal field */} +
+
+ +
+
+

+ {field.label} + {field.required && *} +

+

+ {field.kind === "classifier" ? "Landlord description" : "Internal field"} +

+
+
+ + {/* Arrow */} +
+ +
+ + {/* Header picker */} +
+ +
+ + {/* Status badge */} +
+ + + {isMapped ? "Mapped" : "Not provided"} + +
+
+ ); + } + + function renderSection(title: string, subtitle: string, fields: InternalField[]) { + return ( +
+
+

+ {title} +

+

{subtitle}

+
+ {sourceHeaders.length === 0 ? ( +
+ No headers found in this file. +
+ ) : ( +
{fields.map(renderRow)}
+ )} +
+ ); + } + return (
{/* Breadcrumb + step */} @@ -116,102 +177,27 @@ export default function MapColumnsClient({ Column Remapper

- Align your spreadsheet headers with our internal property data structure to - ensure accurate address processing. + Tell us which spreadsheet column feeds each field. Address fields drive + matching; landlord-description fields are classified into property facts.

- {/* Table */} -
- {/* Column headers */} -
- - Spreadsheet Header - - - - Internal Field Mapping - - - Status - -
- - {sourceHeaders.length === 0 ? ( -
- No headers found in this file. -
- ) : ( -
- {sourceHeaders.map((header) => { - const value = mapping[header] ?? "skip"; - const isMapped = value !== "skip"; - return ( -
- {/* Source header */} -
-
- -
-
-

{header}

-

Source column

-
-
- - {/* Arrow */} -
- -
- - {/* Dropdown */} -
- -
- - {/* Status badge */} -
- - - {isMapped ? "Mapped" : "Skipped"} - -
-
- ); - })} -
- )} -
- - {/* Validation error */} - {missingRequired.length > 0 && ( -

- Required fields not yet mapped:{" "} - {missingRequired - .map((r) => INTERNAL_FIELDS.find((f) => f.value === r)?.label) - .join(", ")} -

+ {renderSection( + "Address fields", + "Used for address matching. A column can feed only one address field.", + ADDRESS_FIELDS )} - {error &&

{error}

} + {renderSection( + "Landlord description fields (optional)", + "Classified into property facts. Several fields may share one column.", + CLASSIFIER_FIELDS + )} + + {/* Validation / request error */} + {validationError && ( +

{validationError}

+ )} + {requestError &&

{requestError}

} {/* Footer */}
@@ -249,8 +235,10 @@ export default function MapColumnsClient({ Pro Tip

- “Ensure your source file doesn't have blank headers. Any column mapped to - “Skip” will be ignored during import.” + “Fields left as “Not provided” are ignored. The same + column can feed several landlord-description fields — e.g. one + “Property Type” column can drive both Property Type and Built + Form.”

diff --git a/src/app/portfolio/[slug]/(portfolio)/landlord-overrides/page.tsx b/src/app/portfolio/[slug]/(portfolio)/landlord-overrides/page.tsx new file mode 100644 index 00000000..9e9ac249 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/landlord-overrides/page.tsx @@ -0,0 +1,101 @@ +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { + getLandlordOverrides, + type OverrideRow, + type LandlordOverrideCategory, +} from "@/lib/landlordOverrides/server"; +import { CLASSIFIER_FIELDS } from "@/lib/bulkUpload/columnFields"; + +export default async function LandlordOverridesPage(props: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await props.params; + const session = await getServerSession(AuthOptions); + if (!session) redirect("/login"); + + const results = await getLandlordOverrides(slug); + const total = Object.values(results).reduce((n, rows) => n + rows.length, 0); + + return ( +
+
+

+ Landlord overrides +

+

+ Property facts classified from your bulk-upload descriptions. Read-only + — editing comes later. +

+
+ + {total === 0 ? ( +
+ No classified values yet. They appear here once a bulk upload with + landlord-description columns has been processed. +
+ ) : ( + CLASSIFIER_FIELDS.map((field) => ( + + )) + )} +
+ ); +} + +function OverrideSection({ title, rows }: { title: string; rows: OverrideRow[] }) { + return ( +
+
+

+ {title} +

+ + {rows.length} {rows.length === 1 ? "value" : "values"} + +
+ {rows.length === 0 ? ( +
+ No values for this category. +
+ ) : ( +
+ {rows.map((row, i) => ( +
+

+ {row.description} +

+

+ {row.value} +

+
+ +
+
+ ))} +
+ )} +
+ ); +} + +function SourceBadge({ source }: { source: string }) { + const isUser = source === "user"; + return ( + + {isUser ? "user" : "classifier"} + + ); +} diff --git a/src/lib/bulkUpload/columnFields.ts b/src/lib/bulkUpload/columnFields.ts new file mode 100644 index 00000000..f29bff27 --- /dev/null +++ b/src/lib/bulkUpload/columnFields.ts @@ -0,0 +1,100 @@ +// Single source of truth for BulkUpload column mapping. +// +// The mapping is stored as `field → source CSV header` (one entry per mapped +// internal field). One source header may feed several CLASSIFIER fields (e.g. +// "Property Type" → both property_type and built_form_type) but at most one +// ADDRESS field — see docs/adr/0003-classifier-triggers-as-address-subtask.md +// and docs/wip/landlord-override-frontend-plan.md (Q2.2, Q3). +// +// Classifier field `value`s mirror the Model service's ClassifiableColumn names +// (property_type / built_form_type / wall_type / roof_type) so the mapping can +// be forwarded to the lambda trigger verbatim. + +export type InternalFieldKind = "address" | "classifier"; + +export interface InternalField { + value: string; + label: string; + kind: InternalFieldKind; + required: boolean; + // Canonical header written into the address-matching CSV (address fields only). + outputHeader?: string; +} + +export const INTERNAL_FIELDS: InternalField[] = [ + { value: "address_1", label: "Address 1", kind: "address", required: true, outputHeader: "Address 1" }, + { value: "address_2", label: "Address 2", kind: "address", required: false, outputHeader: "Address 2" }, + { value: "address_3", label: "Address 3", kind: "address", required: false, outputHeader: "Address 3" }, + { value: "postcode", label: "Postcode", kind: "address", required: true, outputHeader: "postcode" }, + { value: "internal_reference", label: "Internal Reference", kind: "address", required: false, outputHeader: "Internal Reference" }, + { value: "property_type", label: "Property Type", kind: "classifier", required: false }, + { value: "built_form_type", label: "Built Form", kind: "classifier", required: false }, + { value: "wall_type", label: "Wall Type", kind: "classifier", required: false }, + { value: "roof_type", label: "Roof Type", kind: "classifier", required: false }, +]; + +export const ADDRESS_FIELDS = INTERNAL_FIELDS.filter((f) => f.kind === "address"); +export const CLASSIFIER_FIELDS = INTERNAL_FIELDS.filter((f) => f.kind === "classifier"); +export const CLASSIFIER_FIELD_VALUES = CLASSIFIER_FIELDS.map((f) => f.value); +export const REQUIRED_FIELD_VALUES = INTERNAL_FIELDS.filter((f) => f.required).map((f) => f.value); + +// Sentinel for an unmapped field in the UI dropdown ("Not provided"). +export const NOT_PROVIDED = ""; + +// header → address field detection. Classifier fields are never auto-detected +// (Q2.1): mapping them is always an explicit user choice. +export function autoDetectField(header: string): string | null { + const h = header.toLowerCase().replace(/[\s_\-]/g, ""); + if (/^(address|addr)(line)?(1|one)?$/.test(h)) return "address_1"; + if (/^(address|addr)(line)?(2|two)|^street$/.test(h)) return "address_2"; + if (/^(address|addr)(line)?(3|three)|^locality$|^town$|^city$/.test(h)) return "address_3"; + if (/^post(al)?code$|^postcode$|^pcode$/.test(h)) return "postcode"; + if (/^(internal)?ref(erence)?$|^id$/.test(h)) return "internal_reference"; + return null; +} + +// Build the initial field→header mapping: keep any existing choices, then +// auto-fill address fields from the headers (first matching header wins). +export function buildInitialMapping( + sourceHeaders: string[], + existing?: Record, +): Record { + const mapping: Record = { ...(existing ?? {}) }; + for (const header of sourceHeaders) { + const field = autoDetectField(header); + if (!field) continue; + if (mapping[field] === undefined) mapping[field] = header; + } + return mapping; +} + +// Validation shared by the client (live) and the server (authoritative). +// Returns the first problem as a message, or null when the mapping is valid. +export function validateColumnMapping(mapping: Record): string | null { + for (const field of REQUIRED_FIELD_VALUES) { + if (!mapping[field]) { + const label = INTERNAL_FIELDS.find((f) => f.value === field)?.label ?? field; + return `${label} must be mapped to a column.`; + } + } + const usedAddressHeaders = new Set(); + for (const field of ADDRESS_FIELDS) { + const header = mapping[field.value]; + if (!header) continue; + if (usedAddressHeaders.has(header)) { + return `Column "${header}" is mapped to more than one address field.`; + } + usedAddressHeaders.add(header); + } + return null; +} + +// The classifier subset of the mapping (category → source header) that gets +// forwarded to the lambda trigger. Address fields are intentionally excluded. +export function classifierMapping(mapping: Record): Record { + const out: Record = {}; + for (const field of CLASSIFIER_FIELD_VALUES) { + if (mapping[field]) out[field] = mapping[field]; + } + return out; +} diff --git a/src/lib/bulkUpload/server.ts b/src/lib/bulkUpload/server.ts index a5785c39..e5260627 100644 --- a/src/lib/bulkUpload/server.ts +++ b/src/lib/bulkUpload/server.ts @@ -4,6 +4,8 @@ import { tasks } from "@/app/db/schema/tasks/tasks"; import { subTasks } from "@/app/db/schema/tasks/subtask"; import { count, desc, eq, sql } from "drizzle-orm"; import type { BulkUpload, BulkUploadStatus, ProgressView, TaskSummary } from "./types"; +import { validateColumnMapping, classifierMapping } from "./columnFields"; +import { SUBTASK_SERVICE } from "./types"; const REMAP_ALLOWED: ReadonlySet = new Set([ "ready_for_processing", @@ -78,6 +80,12 @@ async function loadTaskSummary(taskId: string): Promise { totalSubtasks: count(subTasks.id), completedSubtasks: sql`count(case when lower(${subTasks.status}) in ('completed', 'complete') then 1 end)::int`, failedSubtasks: sql`count(case when lower(${subTasks.status}) in ('failed', 'failure', 'error') then 1 end)::int`, + addressTotal: sql`count(case when (${subTasks.service} = 'address2uprn' or ${subTasks.service} is null) and ${subTasks.id} is not null then 1 end)::int`, + addressCompleted: sql`count(case when (${subTasks.service} = 'address2uprn' or ${subTasks.service} is null) and lower(${subTasks.status}) in ('completed', 'complete') then 1 end)::int`, + addressFailed: sql`count(case when (${subTasks.service} = 'address2uprn' or ${subTasks.service} is null) and lower(${subTasks.status}) in ('failed', 'failure', 'error') then 1 end)::int`, + classifierTotal: sql`count(case when ${subTasks.service} = 'landlord_description_overrides' then 1 end)::int`, + classifierCompleted: sql`count(case when ${subTasks.service} = 'landlord_description_overrides' and lower(${subTasks.status}) in ('completed', 'complete') then 1 end)::int`, + classifierFailed: sql`count(case when ${subTasks.service} = 'landlord_description_overrides' and lower(${subTasks.status}) in ('failed', 'failure', 'error') then 1 end)::int`, }) .from(tasks) .leftJoin(subTasks, eq(subTasks.taskId, tasks.id)) @@ -94,13 +102,6 @@ export async function getProgressView(uploadId: string): Promise): string | null { - const values = Object.values(mapping); - if (!values.includes("address_1")) return "Mapping must include address_1."; - if (!values.includes("postcode")) return "Mapping must include postcode."; - return null; -} - export type SetMappingOutcome = | { kind: "ok"; upload: BulkUpload } | { kind: "not_found" } @@ -116,7 +117,7 @@ export async function setColumnMapping( if (!REMAP_ALLOWED.has(upload.status as BulkUploadStatus)) return { kind: "invalid_status", current: upload.status }; - const reason = validateMapping(mapping); + const reason = validateColumnMapping(mapping); if (reason) return { kind: "invalid_mapping", reason }; const [updated] = await db @@ -174,6 +175,7 @@ export async function triggerAddressMatching(args: { .values({ taskId: task.id, status: "waiting", + service: SUBTASK_SERVICE.address, inputs: JSON.stringify({ bulk_upload_id: args.uploadId }), }) .returning(); @@ -209,6 +211,62 @@ export async function triggerAddressMatching(args: { return { kind: "ok", taskId: task.id }; } +// Co-fires the landlord classifier as a subtask under the address task. Reads +// the ORIGINAL upload (the address-matching CSV strips the description columns) +// and is non-blocking: a trigger failure marks only the classifier subtask, so +// address matching is unaffected. See ADR-0003. +export async function triggerClassifier(args: { + taskId: string; + uploadId: string; + sessionToken: string | undefined; +}): Promise { + const upload = await loadById(args.uploadId); + if (!upload || !upload.columnMapping) return; + + const columnMapping = classifierMapping(upload.columnMapping); + if (Object.keys(columnMapping).length === 0) return; + + const [subTask] = await db + .insert(subTasks) + .values({ + taskId: args.taskId, + status: "waiting", + service: SUBTASK_SERVICE.classifier, + inputs: JSON.stringify({ bulk_upload_id: args.uploadId }), + }) + .returning(); + + const payload = { + task_id: args.taskId, + sub_task_id: subTask.id, + s3_uri: `s3://${upload.s3Bucket}/${upload.s3Key}`, + portfolio_id: Number(upload.portfolioId), + column_mapping: columnMapping, + }; + + const trigger = await triggerFastApiPipeline({ + endpoint: "/v1/bulk-uploads/trigger-landlord-overrides", + payload, + sessionToken: args.sessionToken, + }); + + if (!trigger.ok) { + await db + .update(subTasks) + .set({ + status: "failed", + outputs: JSON.stringify({ error: trigger.message }), + }) + .where(eq(subTasks.id, subTask.id)); + return; + } + + await db + .update(subTasks) + .set({ status: "in progress", inputs: JSON.stringify(payload) }) + .where(eq(subTasks.id, subTask.id)); +} + export type CombineRetriggerOutcome = | { kind: "triggered"; taskId: string; subTaskId: string } | { kind: "already_combined" } diff --git a/src/lib/bulkUpload/types.ts b/src/lib/bulkUpload/types.ts index c2b1cb63..38d8f125 100644 --- a/src/lib/bulkUpload/types.ts +++ b/src/lib/bulkUpload/types.ts @@ -14,6 +14,13 @@ export type BulkUploadStatus = (typeof BULK_UPLOAD_STATUSES)[number]; export type BulkUpload = typeof bulkAddressUploads.$inferSelect; +// sub_task.service values. NULL is treated as address (legacy rows + the +// backend-spawned postcode-split children, which don't set it). See ADR-0003. +export const SUBTASK_SERVICE = { + address: "address2uprn", + classifier: "landlord_description_overrides", +} as const; + export type TaskSummary = { id: string; taskSource: string; @@ -25,6 +32,14 @@ export type TaskSummary = { totalSubtasks: number; completedSubtasks: number; failedSubtasks: number; + // Per-pipeline breakdown so onboarding progress can separate address + // matching from landlord classification (ADR-0003). + addressTotal: number; + addressCompleted: number; + addressFailed: number; + classifierTotal: number; + classifierCompleted: number; + classifierFailed: number; }; export type ProgressView = { diff --git a/src/lib/landlordOverrides/server.ts b/src/lib/landlordOverrides/server.ts new file mode 100644 index 00000000..c8f98eab --- /dev/null +++ b/src/lib/landlordOverrides/server.ts @@ -0,0 +1,79 @@ +import { db } from "@/app/db/db"; +import { + landlordPropertyTypeOverrides, + landlordBuiltFormTypeOverrides, + landlordWallTypeOverrides, + landlordRoofTypeOverrides, +} from "@/app/db/schema/landlord_overrides"; +import { asc, eq } from "drizzle-orm"; + +export interface OverrideRow { + description: string; + value: string; + source: string; +} + +export type LandlordOverrideCategory = + | "property_type" + | "built_form_type" + | "wall_type" + | "roof_type"; + +export type LandlordOverrideResults = Record; + +const EMPTY: LandlordOverrideResults = { + property_type: [], + built_form_type: [], + wall_type: [], + roof_type: [], +}; + +// Reads the four landlord_*_overrides tables for a portfolio. The portfolio id +// is a bigint FK; the bulk-upload flow carries it as a numeric string. +export async function getLandlordOverrides( + portfolioId: string +): Promise { + if (!/^\d+$/.test(portfolioId)) return EMPTY; + const pid = BigInt(portfolioId); + + const [property_type, built_form_type, wall_type, roof_type] = await Promise.all([ + db + .select({ + description: landlordPropertyTypeOverrides.description, + value: landlordPropertyTypeOverrides.value, + source: landlordPropertyTypeOverrides.source, + }) + .from(landlordPropertyTypeOverrides) + .where(eq(landlordPropertyTypeOverrides.portfolioId, pid)) + .orderBy(asc(landlordPropertyTypeOverrides.description)), + db + .select({ + description: landlordBuiltFormTypeOverrides.description, + value: landlordBuiltFormTypeOverrides.value, + source: landlordBuiltFormTypeOverrides.source, + }) + .from(landlordBuiltFormTypeOverrides) + .where(eq(landlordBuiltFormTypeOverrides.portfolioId, pid)) + .orderBy(asc(landlordBuiltFormTypeOverrides.description)), + db + .select({ + description: landlordWallTypeOverrides.description, + value: landlordWallTypeOverrides.value, + source: landlordWallTypeOverrides.source, + }) + .from(landlordWallTypeOverrides) + .where(eq(landlordWallTypeOverrides.portfolioId, pid)) + .orderBy(asc(landlordWallTypeOverrides.description)), + db + .select({ + description: landlordRoofTypeOverrides.description, + value: landlordRoofTypeOverrides.value, + source: landlordRoofTypeOverrides.source, + }) + .from(landlordRoofTypeOverrides) + .where(eq(landlordRoofTypeOverrides.portfolioId, pid)) + .orderBy(asc(landlordRoofTypeOverrides.description)), + ]); + + return { property_type, built_form_type, wall_type, roof_type }; +} From 90407799ac54677d8bbcf31ce69b3abb193d74db Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Mon, 1 Jun 2026 11:16:56 +0000 Subject: [PATCH 03/13] dev container change so its more dynamic --- .claude/settings.json | 7 +++++-- .devcontainer/devcontainer.json | 3 +-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 878b2d3b..9e885fd0 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -13,7 +13,9 @@ "Bash(echo \"tsc exit: $?\")", "Bash(npm install *)", "Bash(npx drizzle-kit *)", - "Bash(echo \"frontend tsc exit: $?\")" + "Bash(echo \"frontend tsc exit: $?\")", + "Bash(python3 -c ' *)", + "Bash(rm -f /workspaces/home/github/Model/backend/address2UPRN/local_handler/.env.local /workspaces/home/github/Model/backend/bulk_address2uprn_combiner/local_handler/.env.local && echo \"removed stub .env.local files\")" ], "deny": [ "Bash(npx drizzle-kit generate)", @@ -22,7 +24,8 @@ "additionalDirectories": [ "/workspaces/home/github/Model/backend/app/bulk_uploads", "/workspaces/home/github/Model/applications/landlord_description_overrides", - "/workspaces/home/github/Model/orchestration" + "/workspaces/home/github/Model/orchestration", + "/workspaces/home/github/Model/backend/address2UPRN/local_handler" ] } } diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index be7d2bc0..89f71c6c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,8 +14,7 @@ // the mounted host ~/.config/gh. "postCreateCommand": "gh repo clone Hestia-Homes/agentic-toolkit /tmp/agentic-toolkit -- --branch 0.0.5 --depth 1 && bash /tmp/agentic-toolkit/setup.sh && npm install", - "forwardPorts": [3000], - "appPort": ["3000:3000"], + "forwardPorts": ["frontend:3000", "pgadmin:80"], "mounts": [ // Optional, just makes getting from Downloads (local env) easier From 38c82ebca3e8eeaf4749e4c81cb609cdf6f6b35c Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Tue, 2 Jun 2026 12:57:05 +0000 Subject: [PATCH 04/13] lanlord exetension application --- .claude/settings.json | 32 ++++- CONTEXT.md | 23 ++++ .../start-address-matching/route.ts | 79 ++++++++++-- src/app/db/schema/bulk_address_uploads.ts | 27 ++++ .../[uploadId]/OnboardingProgress.tsx | 90 ++++++++++--- .../map-columns/MapColumnsClient.tsx | 4 +- src/lib/bulkUpload/multiEntry.ts | 119 ++++++++++++++++++ src/lib/bulkUpload/server.ts | 28 ++++- 8 files changed, 364 insertions(+), 38 deletions(-) create mode 100644 src/lib/bulkUpload/multiEntry.ts diff --git a/.claude/settings.json b/.claude/settings.json index 9e885fd0..e5143ec4 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -15,7 +15,33 @@ "Bash(npx drizzle-kit *)", "Bash(echo \"frontend tsc exit: $?\")", "Bash(python3 -c ' *)", - "Bash(rm -f /workspaces/home/github/Model/backend/address2UPRN/local_handler/.env.local /workspaces/home/github/Model/backend/bulk_address2uprn_combiner/local_handler/.env.local && echo \"removed stub .env.local files\")" + "Bash(rm -f /workspaces/home/github/Model/backend/address2UPRN/local_handler/.env.local /workspaces/home/github/Model/backend/bulk_address2uprn_combiner/local_handler/.env.local && echo \"removed stub .env.local files\")", + "Bash(cat deployment/terraform/modules/s3_iam_policy/main.tf)", + "Bash(cat deployment/terraform/modules/s3_iam_policy/variables.tf)", + "Bash(terraform fmt *)", + "Bash(echo \"exit: $?\")", + "Bash(pip install *)", + "Bash(git -C /workspaces/assessment-model remote -v)", + "Bash(gh label *)", + "Bash(gh issue create --repo Hestia-Homes/assessment-model --label ready-for-agent --title 'Detect multi-entry rows and surface the largest-count sample on awaiting_review' --body ' *)", + "Bash(gh issue create --repo Hestia-Homes/assessment-model --label ready-for-agent --title 'Confirm building-part ordering and gate Finalise on it' --body ' *)", + "Bash(gh issue create --repo Hestia-Homes/assessment-model --label ready-for-agent --title 'Show our classification next to each multi-entry sample entry \\(read-only\\)' --body ' *)", + "Bash(gh issue create --repo Hestia-Homes/assessment-model --label ready-for-agent --title 'Editable classification verification writing source='\\\\''user'\\\\'', gating Finalise' --body ' *)", + "Bash(git check-ignore *)", + "Bash(git ls-tree *)", + "Bash(git worktree *)", + "Read(//workspaces/mig-wt/src/app/db/**)", + "Read(//workspaces/mig-wt/src/app/db/migrations/meta/**)", + "Bash(git branch *)", + "Bash(cp /workspaces/assessment-model/.env.local /tmp/mig-wt/.env.local; echo \"env copied\"; cat -n /tmp/mig-wt/src/app/db/schema/bulk_address_uploads.ts)", + "Bash(node /workspaces/assessment-model/node_modules/drizzle-kit/bin.cjs generate)", + "Bash(ln -s /workspaces/assessment-model/node_modules /tmp/mig-wt/node_modules)", + "Bash(node node_modules/drizzle-kit/bin.cjs generate)", + "Bash(git push *)", + "Bash(npm run *)", + "Bash(grep '\\\\.sql$')", + "Bash(git status *)", + "Bash(git checkout *)" ], "deny": [ "Bash(npx drizzle-kit generate)", @@ -25,7 +51,9 @@ "/workspaces/home/github/Model/backend/app/bulk_uploads", "/workspaces/home/github/Model/applications/landlord_description_overrides", "/workspaces/home/github/Model/orchestration", - "/workspaces/home/github/Model/backend/address2UPRN/local_handler" + "/workspaces/home/github/Model/backend/address2UPRN/local_handler", + "/workspaces/home/github/Model/deployment/terraform/shared", + "/tmp/mig-wt" ] } } diff --git a/CONTEXT.md b/CONTEXT.md index 7a79d1c4..24963b57 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -45,6 +45,27 @@ _Avoid_: customer data, manual override, landlord data The translation from a Landlord's free-text description in a BulkUpload column (e.g. `"cavity: filledcavity"`) to a canonical domain enum value (e.g. `WallType.CAVITY`). Produced by a `ColumnClassifier` (today an LLM, tomorrow possibly a lookup table or rules engine) in the Model service. Stored per-Portfolio, one row per `(category, description)`. A row carries provenance (`classifier` or `user`) so user overrides survive re-classification. _Avoid_: column mapping (that's a separate concept — see `ColumnMapping` above), classification, dictionary +### Building parts + +**Building part**: +One physically distinct part of a dwelling described by a single entry within a multi-valued cell. A dwelling is one **Main building** plus zero or more **Extensions**. Per-part descriptions appear as comma-separated entries in physical-element columns (e.g. `Walls`, `Roofs`); whole-dwelling columns (e.g. `Property Type`) carry a single entry and are **not** split per part. +_Avoid_: annexe, unit, section, dwelling part + +**Main building**: +The principal building part of a dwelling — exactly one per address. The others are **Extensions**. + +**Extension**: +A building part that is not the Main building, numbered **Extension 1 … Extension N-1** for an N-entry address. +_Avoid_: annexe, addition, outbuilding + +**Multi-entry**: +The property of a BulkUpload row whose physical-element cells hold **more than one comma-separated entry**, one per **Building part**. Always intra-cell in our data — never multiple rows sharing one address/UPRN. Within a row, the multi-valued columns agree on entry-count, so **position `i` is the same Building part across every multi-valued column**. +_Avoid_: multi-row, multi-record, duplicate address + +**Building-part ordering** (a.k.a. **ordering**): +The user's declaration, captured once per file, of which list-position maps to which Building part — because the entry order is a consistent per-file mistake (`"A, B"` could be `[Main, Extension 1]` or `[Extension 1, Main]`). Stored per entry-count as a permutation. See [ADR-0004](./docs/adr/0004-multi-entry-building-part-ordering.md). +_Avoid_: sort order, sequence, column mapping + ## Lifecycle A **BulkUpload** moves through these statuses: @@ -65,6 +86,8 @@ Re-mapping (PATCHing `columnMapping`) is legal only in `ready_for_processing` an **Two writers**: Next.js owns transitions out of `mapping_complete`, into `processing`, and the terminal Finalise outcomes. FastAPI owns `combining` and `awaiting_review` — writing them direct to the DB during the combiner run. The BulkUpload aggregate observes both. +At `awaiting_review`, **Finalise is gated** (not a new status — a precondition on the action): when classifier columns were mapped the user must acknowledge the classification-verification step, and when the file is **Multi-entry** they must confirm the **Building-part ordering**. See [ADR-0004](./docs/adr/0004-multi-entry-building-part-ordering.md). + See [ADR-0001](./docs/adr/0001-bulk-upload-state-machine.md) for the deliberate "not yet" decisions baked into this lifecycle. ## Relationships diff --git a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/start-address-matching/route.ts b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/start-address-matching/route.ts index 14441ce2..22bfa8b9 100644 --- a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/start-address-matching/route.ts +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/start-address-matching/route.ts @@ -3,20 +3,24 @@ import { getServerSession } from "next-auth"; import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { createS3Client, createRetrofitDataS3Client, retrofitDataS3Bucket } from "@/app/utils/s3"; import * as XLSX from "xlsx"; -import { loadForAddressMatching, triggerAddressMatching, triggerClassifier } from "@/lib/bulkUpload/server"; +import { loadForAddressMatching, saveMultiEntrySummary, triggerAddressMatching, triggerClassifier } from "@/lib/bulkUpload/server"; import { readSessionToken } from "@/lib/session"; -import { ADDRESS_FIELDS } from "@/lib/bulkUpload/columnFields"; +import { ADDRESS_FIELDS, classifierMapping } from "@/lib/bulkUpload/columnFields"; +import { detectMultiEntry } from "@/lib/bulkUpload/multiEntry"; -function transformFile( - buffer: Buffer, - columnMapping: Record // field → source header -): { csv: string; error?: never } | { csv?: never; error: string } { +type SheetRow = Record; + +function readRows(buffer: Buffer): SheetRow[] { const wb = XLSX.read(buffer, { type: "buffer" }); const sheet = wb.Sheets[wb.SheetNames[0]]; - const rows = XLSX.utils.sheet_to_json>(sheet, { defval: "" }); - - if (rows.length === 0) return { error: "Empty file" }; + return XLSX.utils.sheet_to_json(sheet, { defval: "" }); +} +// Address-matching CSV: address fields only, renamed to canonical headers. +function buildAddressCsv( + rows: SheetRow[], + columnMapping: Record // field → source header +): { csv: string; error?: never } | { csv?: never; error: string } { const outputHeaders: string[] = []; const outputToSource: Record = {}; for (const field of ADDRESS_FIELDS) { @@ -32,7 +36,7 @@ function transformFile( return { error: 'Mapping must include "postcode"' }; const outputRows = rows.map((row) => { - const out: Record = {}; + const out: SheetRow = {}; for (const [outName, src] of Object.entries(outputToSource)) { out[outName] = row[src] ?? ""; } @@ -43,6 +47,25 @@ function transformFile( return { csv: XLSX.utils.sheet_to_csv(outSheet) }; } +// Classifier CSV: the mapped classifier source columns only, original headers +// preserved (the lambda resolves them via column_mapping). Converting here means +// the classifier always reads a real CSV even when the upload was .xlsx/.xls — +// see ADR-0003. One source header may feed several categories, so dedupe to +// distinct headers. +function buildClassifierCsv( + rows: SheetRow[], + classifierMap: Record // category → source header +): string { + const headers = [...new Set(Object.values(classifierMap))]; + const outputRows = rows.map((row) => { + const out: SheetRow = {}; + for (const h of headers) out[h] = row[h] ?? ""; + return out; + }); + const outSheet = XLSX.utils.json_to_sheet(outputRows, { header: headers }); + return XLSX.utils.sheet_to_csv(outSheet); +} + export async function POST( request: NextRequest, { params }: { params: Promise<{ portfolioId: string; uploadId: string }> } @@ -81,7 +104,15 @@ export async function POST( return NextResponse.json({ error: "Failed to read source file" }, { status: 500 }); } - const transformed = transformFile(fileBuffer, upload.columnMapping!); + const rows = readRows(fileBuffer); + if (rows.length === 0) + return NextResponse.json({ error: "Empty file" }, { status: 422 }); + + // Detect multi-entry building parts now, while the whole file is parsed in + // memory, so the awaiting_review surface never re-reads it (ADR-0004). + await saveMultiEntrySummary(uploadId, detectMultiEntry(rows, upload.columnMapping!)); + + const transformed = buildAddressCsv(rows, upload.columnMapping!); if (transformed.error) return NextResponse.json({ error: transformed.error }, { status: 422 }); @@ -102,13 +133,37 @@ export async function POST( const s3Uri = `s3://${outputBucket}/${transformedKey}`; + // Convert the mapped classifier columns to their own CSV so the classifier + // lambda always parses a real CSV, never the raw upload (which may be + // .xlsx/.xls). Only when the user mapped ≥1 classifier column. See ADR-0003. + const classifierMap = classifierMapping(upload.columnMapping!); + let classifierS3Uri: string | undefined; + if (Object.keys(classifierMap).length > 0) { + const classifierKey = `bulk_onboarding_inputs/${portfolioId}/${uploadId}-classifier.csv`; + try { + await outputS3 + .putObject({ + Bucket: outputBucket, + Key: classifierKey, + Body: buildClassifierCsv(rows, classifierMap), + ContentType: "text/csv", + }) + .promise(); + classifierS3Uri = `s3://${outputBucket}/${classifierKey}`; + } catch (err) { + // Non-blocking: classification is skipped, address matching proceeds. + console.error("Failed to upload classifier CSV:", err); + } + } + const sessionToken = readSessionToken(request); const trigger = await triggerAddressMatching({ uploadId, s3Uri, sessionToken }); if (trigger.kind === "trigger_failed") return NextResponse.json({ error: trigger.message }, { status: trigger.status }); // Co-fire the landlord classifier (non-blocking) under the same task. - await triggerClassifier({ taskId: trigger.taskId, uploadId, sessionToken }); + if (classifierS3Uri) + await triggerClassifier({ taskId: trigger.taskId, uploadId, s3Uri: classifierS3Uri, sessionToken }); return NextResponse.json({ taskId: trigger.taskId }, { status: 200 }); } diff --git a/src/app/db/schema/bulk_address_uploads.ts b/src/app/db/schema/bulk_address_uploads.ts index 52f32110..8166551b 100644 --- a/src/app/db/schema/bulk_address_uploads.ts +++ b/src/app/db/schema/bulk_address_uploads.ts @@ -1,6 +1,30 @@ import { pgTable, uuid, text, timestamp, jsonb } from "drizzle-orm/pg-core"; import { sql } from "drizzle-orm"; +// Shape of the multi_entry_summary jsonb (ADR-0004). Co-located with the column +// so the schema is self-contained; the detection logic in +// src/lib/bulkUpload/multiEntry.ts imports these. +export interface MultiEntryEntry { + raw: string; + description: string; +} +export interface MultiEntryColumn { + field: string; + header: string; + entries: MultiEntryEntry[]; +} +export interface MultiEntrySample { + address: string; + count: number; + columns: MultiEntryColumn[]; +} +export interface MultiEntrySummary { + multiValuedFields: string[]; + countDistribution: Record; + largestCount: number; + sample: MultiEntrySample | null; +} + export const bulkAddressUploads = pgTable("bulk_address_uploads", { id: uuid("id").defaultRandom().primaryKey(), portfolioId: text("portfolio_id").notNull(), @@ -11,6 +35,9 @@ export const bulkAddressUploads = pgTable("bulk_address_uploads", { status: text("status").notNull().default("ready_for_processing"), sourceHeaders: text("source_headers").array().notNull().default(sql`'{}'`), columnMapping: jsonb("column_mapping").$type>(), + // Multi-entry building-part detection, computed at start-address-matching + // and read by the awaiting_review review surface (ADR-0004). + multiEntrySummary: jsonb("multi_entry_summary").$type(), taskId: uuid("task_id"), combinedOutputS3Uri: text("combined_output_s3_uri"), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx index b298d7f6..b80ae1cf 100644 --- a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx @@ -8,6 +8,7 @@ import { useFinalize, useRequestCombine, } from "@/lib/bulkUpload/client"; +import type { MultiEntrySample } from "@/lib/bulkUpload/multiEntry"; interface Props { portfolioSlug: string; @@ -60,6 +61,13 @@ export default function OnboardingProgress({ const canRunCombiner = taskDone && !taskFailed && upload.status === "processing"; const canFinalize = upload.status === "awaiting_review"; + // Multi-entry building-part sample, shown read-only on the review surface + // (ADR-0004). Ordering confirmation arrives in a later slice. + const multiEntrySample = + upload.status === "awaiting_review" + ? (upload.multiEntrySummary?.sample ?? null) + : null; + return (
@@ -70,17 +78,24 @@ export default function OnboardingProgress({
- {total > 0 && ( - - {completedSubtasks} / {total} batches complete - - )} - {failedSubtasks > 0 && ( - - - {failedSubtasks} failed - - )} + {/* Address matching: standardises addresses against the OS lookup, in batches. */} + + Address matching: + {failedSubtasks > 0 ? ( + + + {failedSubtasks} of {total} batches failed + + ) : total > 0 && completedSubtasks >= total ? ( + complete + ) : ( + + + running{total > 0 ? ` · ${completedSubtasks} / ${total} batches` : ""} + + )} + + {/* Classification: turns the landlord's free-text descriptions into EPC categories. */} {classifierTotal > 0 && ( Classification: @@ -99,12 +114,6 @@ export default function OnboardingProgress({ )} )} - {!taskDone && ( - - - Running - - )} {isCombining && ( @@ -119,6 +128,8 @@ export default function OnboardingProgress({ )}
+ {multiEntrySample && } + {(canRunCombiner || canFinalize) && (
{canRunCombiner && ( @@ -164,6 +175,51 @@ export default function OnboardingProgress({ ); } +// Read-only preview of the largest-count multi-entry row (ADR-0004). Each +// comma-separated entry is a building part; the user will confirm their order +// in a later slice. Positions are shown 1-based, unlabelled for now. +function MultiEntrySamplePanel({ sample }: { sample: MultiEntrySample }) { + return ( +
+

+ Multiple building parts detected +

+

+ {sample.address ? {sample.address} : "An address"}{" "} + has {sample.count} building parts (e.g. a main building and extensions). + You'll be asked to confirm their order before finalising. +

+ +
+ + + + + {sample.columns.map((column) => ( + + ))} + + + + {Array.from({ length: sample.count }).map((_, position) => ( + + + {sample.columns.map((column) => ( + + ))} + + ))} + +
Position + {column.header} +
{position + 1} + {column.entries[position]?.raw ?? "—"} +
+
+
+ ); +} + function StageButton({ label, activeLabel, diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/map-columns/MapColumnsClient.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/map-columns/MapColumnsClient.tsx index fe729d6d..88ab9ce7 100644 --- a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/map-columns/MapColumnsClient.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/map-columns/MapColumnsClient.tsx @@ -104,8 +104,8 @@ export default function MapColumnsClient({ className="w-full text-sm border border-gray-200 rounded-lg px-3 py-2 bg-white text-gray-800 focus:outline-none focus:ring-2 focus:ring-[#14163d]/20 focus:border-[#14163d]" > - {sourceHeaders.map((header) => ( - ))} diff --git a/src/lib/bulkUpload/multiEntry.ts b/src/lib/bulkUpload/multiEntry.ts new file mode 100644 index 00000000..b0ebe3fb --- /dev/null +++ b/src/lib/bulkUpload/multiEntry.ts @@ -0,0 +1,119 @@ +// Multi-entry building-part detection (ADR-0004). +// +// A BulkUpload row can carry several comma-separated entries in a physical- +// element column (e.g. Walls = "Cavity: AsBuilt (1976-1982), Cavity: +// FilledCavity"). Each entry is a Building part (Main building + Extensions). +// This module finds that pattern and captures one sample — the row with the +// MOST building parts — so the user can confirm the ordering downstream. +// +// Pure + I/O-free so it's unit-testable; the start-address-matching route runs +// it over the already-parsed upload rows and persists the result on the upload. + +import { ADDRESS_FIELDS, classifierMapping } from "./columnFields"; +import type { + MultiEntryEntry, + MultiEntryColumn, + MultiEntrySummary, +} from "@/app/db/schema/bulk_address_uploads"; + +// The jsonb shape lives with the column (schema/bulk_address_uploads.ts) so the +// migration is self-contained; re-export here for callers of this module. +export type { + MultiEntryEntry, + MultiEntryColumn, + MultiEntrySample, + MultiEntrySummary, +} from "@/app/db/schema/bulk_address_uploads"; + +export const EMPTY_MULTI_ENTRY_SUMMARY: MultiEntrySummary = { + multiValuedFields: [], + countDistribution: {}, + largestCount: 0, + sample: null, +}; + +// Split a cell into building-part entries. Mirrors the classifier's +// split(",") → trim → lower, dropping empty fragments so positions align +// across raw and normalized forms. +export function splitEntries(value: unknown): MultiEntryEntry[] { + return String(value ?? "") + .split(",") + .map((s) => s.trim()) + .filter((s) => s.length > 0) + .map((raw) => ({ raw, description: raw.toLowerCase() })); +} + +// Compose a display address from the mapped address fields (reference excluded). +function buildAddress( + row: Record, + columnMapping: Record, +): string { + const parts: string[] = []; + for (const field of ADDRESS_FIELDS) { + if (field.value === "internal_reference") continue; + const header = columnMapping[field.value]; + if (!header) continue; + const value = String(row[header] ?? "").trim(); + if (value) parts.push(value); + } + return parts.join(", "); +} + +// Scan the mapped classifier columns for multi-entry rows and capture the +// largest-count sample. Only classifier columns are considered — they're the +// physical-element descriptions we slice into building parts; address columns +// are single-valued by nature. +export function detectMultiEntry( + rows: Array>, + columnMapping: Record, +): MultiEntrySummary { + const classifierCols = Object.entries(classifierMapping(columnMapping)); + if (classifierCols.length === 0) return EMPTY_MULTI_ENTRY_SUMMARY; + + const multiValued = new Set(); + const countDistribution: Record = {}; + let largestCount = 0; + let sampleRowIndex = -1; + + rows.forEach((row, index) => { + let rowMax = 0; + for (const [field, header] of classifierCols) { + const n = splitEntries(row[header]).length; + if (n > 1) multiValued.add(field); + if (n > rowMax) rowMax = n; + } + if (rowMax >= 2) { + const key = String(rowMax); + countDistribution[key] = (countDistribution[key] ?? 0) + 1; + // First row at a new maximum becomes the sample. + if (rowMax > largestCount) { + largestCount = rowMax; + sampleRowIndex = index; + } + } + }); + + if (sampleRowIndex === -1) return EMPTY_MULTI_ENTRY_SUMMARY; + + const sampleRow = rows[sampleRowIndex]; + // Show only the columns that are actually split in the sample row; + // single-value columns are whole-dwelling facts, not building parts. + const columns: MultiEntryColumn[] = classifierCols + .map(([field, header]) => ({ + field, + header, + entries: splitEntries(sampleRow[header]), + })) + .filter((column) => column.entries.length > 1); + + return { + multiValuedFields: [...multiValued], + countDistribution, + largestCount, + sample: { + address: buildAddress(sampleRow, columnMapping), + count: largestCount, + columns, + }, + }; +} diff --git a/src/lib/bulkUpload/server.ts b/src/lib/bulkUpload/server.ts index e5260627..f020dbc0 100644 --- a/src/lib/bulkUpload/server.ts +++ b/src/lib/bulkUpload/server.ts @@ -6,6 +6,7 @@ import { count, desc, eq, sql } from "drizzle-orm"; import type { BulkUpload, BulkUploadStatus, ProgressView, TaskSummary } from "./types"; import { validateColumnMapping, classifierMapping } from "./columnFields"; import { SUBTASK_SERVICE } from "./types"; +import type { MultiEntrySummary } from "./multiEntry"; const REMAP_ALLOWED: ReadonlySet = new Set([ "ready_for_processing", @@ -102,6 +103,20 @@ export async function getProgressView(uploadId: string): Promise { + await db + .update(bulkAddressUploads) + .set({ multiEntrySummary: summary }) + .where(eq(bulkAddressUploads.id, uploadId)); +} + export type SetMappingOutcome = | { kind: "ok"; upload: BulkUpload } | { kind: "not_found" } @@ -211,13 +226,16 @@ export async function triggerAddressMatching(args: { return { kind: "ok", taskId: task.id }; } -// Co-fires the landlord classifier as a subtask under the address task. Reads -// the ORIGINAL upload (the address-matching CSV strips the description columns) -// and is non-blocking: a trigger failure marks only the classifier subtask, so -// address matching is unaffected. See ADR-0003. +// Co-fires the landlord classifier as a subtask under the address task. Reads a +// dedicated classifier CSV (the classifier columns converted from the upload by +// the start-address-matching route — the address-matching CSV strips the +// description columns), so the lambda always parses a real CSV even for +// .xlsx/.xls uploads. Non-blocking: a trigger failure marks only the classifier +// subtask, so address matching is unaffected. See ADR-0003. export async function triggerClassifier(args: { taskId: string; uploadId: string; + s3Uri: string; sessionToken: string | undefined; }): Promise { const upload = await loadById(args.uploadId); @@ -239,7 +257,7 @@ export async function triggerClassifier(args: { const payload = { task_id: args.taskId, sub_task_id: subTask.id, - s3_uri: `s3://${upload.s3Bucket}/${upload.s3Key}`, + s3_uri: args.s3Uri, portfolio_id: Number(upload.portfolioId), column_mapping: columnMapping, }; From af86d53397a71c1e589a8c5ef333569acbacbdb7 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Tue, 2 Jun 2026 13:46:55 +0000 Subject: [PATCH 05/13] Confirm building-part ordering on awaiting_review (#297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the multiEntryOrdering jsonb column + interactive order picker: the largest-count multi-entry sample is shown with a building-part dropdown per file position (one Main building + Extensions), validated as a permutation. A PATCH route persists { count: permutation } + confirmed, and Finalise is disabled until the ordering is confirmed when the upload is multi-entry. Migration for the new column is intentionally not included here — generated after origin/main is merged (its multi_entry_summary migration lands first). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/wip/multi-entry-ordering-plan.md | 178 ++++++++++++++++++ .../[uploadId]/multi-entry-ordering/route.ts | 49 +++++ src/app/db/schema/bulk_address_uploads.ts | 13 ++ .../[uploadId]/OnboardingProgress.tsx | 156 ++++++++++++--- src/lib/bulkUpload/client.ts | 21 +++ src/lib/bulkUpload/multiEntry.ts | 31 +++ src/lib/bulkUpload/server.ts | 42 +++++ 7 files changed, 467 insertions(+), 23 deletions(-) create mode 100644 docs/wip/multi-entry-ordering-plan.md create mode 100644 src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/multi-entry-ordering/route.ts diff --git a/docs/wip/multi-entry-ordering-plan.md b/docs/wip/multi-entry-ordering-plan.md new file mode 100644 index 00000000..045f9522 --- /dev/null +++ b/docs/wip/multi-entry-ordering-plan.md @@ -0,0 +1,178 @@ +# Multi-entry building-part ordering — in-flight design notes + +**Status:** Grilling complete (2026-06-02) — ready to break into issues +**Branch:** `feature/frontend_landlord_overrides` +**Author:** Jun-te (with Claude, via `/grill-me`) + +A _design-in-progress_ document, not the ADR. It records the decisions reached +during grilling so the conversation can resume without re-litigating settled +questions. The flow + schema decision is promoted to +[ADR-0004](../adr/0004-multi-entry-building-part-ordering.md); new domain terms +are promoted to [CONTEXT.md](../../CONTEXT.md#building-parts). + +## Goal + +After address matching and classification finish, a single address row can carry +**comma-separated entries** in physical-element columns — e.g. +`Walls = "Cavity: AsBuilt (1976-1982), Cavity: FilledCavity"`, +`Roofs = "Flat: As Built, PitchedNormalLoftAccess: 200mm"`. Each entry is a +**building part** (main building + extensions). The order is ambiguous and a +**consistent per-file mistake**, so we capture the correct ordering from the user +**once per file** and persist it on the BulkUpload for a later consumer. + +## Backstory / ground truth (verified against the example file + code) + +- In `ARA AddressProfiling_Download_28-04-2026_10501 (2).xlsx` (32,213 data + rows): **0 UPRNs appear in more than one row** — multi-entry is + comma-separated values **inside one cell**, never multiple rows per address. +- In a multi-entry row the multi-valued columns **agree on count** (Walls=2 ∧ + Roofs=2) while whole-dwelling columns stay at 1 (`Property Type` = `"House: + EndTerrace"`). So position *i* is the **same building part across every + multi-valued column**. +- The classifier today **discards** this: [`get_col_to_description_mappings`](/workspaces/home/github/Model/orchestration/landlord_description_overrides_orchestrator.py) + does `value.split(",")` into a **`set`** — orderless, deduped. Correct for the + vocabulary layer (description→enum), but it drops exactly the + position/building-part association this feature needs. +- This is the **per-Property building-part fact** territory ADR-0002 deferred + ("a per-Property fact layer (not yet modelled)"). We are **not** building that + layer here — only **capturing** the ordering it will need. + +## Decided + +### Q1 — Order semantics: full reorder, keyed by count + +Position *i* = a building part. The user supplies a **permutation per distinct +entry-count**; persisted as `{ count: permutation }`. This iteration captures +only the **largest-count** sample (see Q5). + +### Q1.1 — Order scope: one ordering across all columns + +A single per-count permutation realigns **every** multi-valued column at once +(index-aligned — Walls[i] and Roofs[i] are the same part). Not per-column. +Matches the data (counts agree across columns). + +### Q1.2 — Mixed counts: single-value columns are whole-property + +A 1-entry column (e.g. `Property Type`) is a **whole-dwelling** fact attached to +the property; only columns with N>1 are sliced into building parts. No padding. + +### Q2 — Scope: capture + persist ordering only + +Detect multi-entry, show one sample address + our classification, capture the +per-count ordering, persist on the BulkUpload. **Not** in scope: the +per-Property fact table or writing main/extension facts at finalise. The +ordering is stored for a later consumer. + +### Q2.1 — Editable verification IS in scope (expands Q2) + +The "verify classification" step lets the user **correct** a classification, +written back as `source='user'`. This deliberately picks up ADR-0002 Q7's +deferred **vocabulary** user-override write path — distinct from the per-Property +fact layer, which stays deferred. + +### Q3 — Placement: on the `awaiting_review` surface + +Render the flow on the existing +[OnboardingProgress](<../../src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx>) +page when `status === "awaiting_review"`. Classification finishes *before* the +combiner (both subtasks must complete → combiner → `awaiting_review`), so by the +time Finalise is offered the classification output exists. No new route. + +### Q3.1 — Flow: two-step stepper, steps appear independently + +- **Step 1 — Verify classification** — shows whenever **≥1 classifier column** + was mapped. +- **Step 2 — Confirm order** — shows only when **multi-entry was detected**. +- A file with classifier columns but no multi-entry shows only Step 1; a file + with neither goes straight to Finalise. + +### Q3.2 — Gate: both steps gate Finalise (where each applies) + +`canFinalize = status==="awaiting_review" && (noClassifierCols || verifyAck) && +(noMultiEntry || orderingConfirmed)`. Two flags persisted. Finalise is one +click but the button stays disabled until its applicable gates are satisfied. + +### Q4 — Verify step lists the sample address's entries only + +Step 1 lists just the descriptions in the **one sample address** (matches "one +address"). Because a correction is per-`(portfolio, description)`, editing one +changes the mapping **portfolio-wide** for that text — the UI must say so. A +spot-check, not full-vocabulary coverage. + +### Q4.1 — Write-back: Next.js upsert, `source='user'`, single row (as built) + +A Next.js route handler / server action upserts the `landlord_*_overrides` row +by `(portfolio_id, description)` setting `value` + `source='user'`, validating +against the pgEnum. **Schema unchanged** — we keep ADR-0002's `UNIQUE +(portfolio_id, description)` and flip the single row's source in place. The +Python classifier's existing `ON CONFLICT … WHERE source='classifier'` +([landlord_overrides_postgres_repository.py:84-91](/workspaces/home/github/Model/infrastructure/landlord_overrides/landlord_overrides_postgres_repository.py#L84)) +then never re-clobbers it. + +> Considered and **rejected**: two rows per description (classifier + user) with +> read-time `user > classifier` resolution. It buys "revert to our suggestion" + +> provenance, and is cheap now (no readers exist yet), but reopens ADR-0002's +> `UNIQUE` decision and migrates Drizzle + 4 Python tables + the conflict target. +> Not worth it for this iteration; the single-row flip already gives "user wins". +> This is the first Next.js writer of a `source='user'` row. + +### Q5 — Which sample: the largest-count row + +Show one sample address — the row with the **most** building parts — so ordering +it reveals the fullest convention. In the common case (only N=2) that is a +single 2-part address. + +### Q5.1 — Reorder UI: label each position + +Lay the file's entries out as rows (position 0, 1, …), each with a building-part +dropdown (**Main building** / **Extension 1** / …). Assigning labels yields the +permutation and validates (each part used once, exactly one Main building). All +multi-valued columns are shown together, each raw entry annotated with our +classified enum, so the user sanity-checks classification **and** alignment. + +### Q6 — Detection: at start, persist a summary + +Compute the multi-entry summary in the **start-address-matching POST** +([route.ts:106](<../../src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/start-address-matching/route.ts#L106>)) +where the full `rows` are already parsed in memory — which columns are +multi-valued, the distinct counts (with row-counts so we can pick the largest), +and the largest-count sample (address + per-column raw entries). Avoids +re-reading a 32k-row file at render. Classification enums are joined at render +from the override tables. + +### Q7 — Persistence: two jsonb columns on `bulk_address_uploads` + +- `multiEntrySummary jsonb` — written at start (detection). +- `multiEntryOrdering jsonb` — written at confirm: `{ count: permutation }` plus + `verifyAck` / `orderingConfirmed` flags (final shape TBD; may split flags into + their own columns). + +No new table — mirrors how `columnMapping` lives on the upload row. + +## Risks / load-bearing assumptions + +1. **Consistent-mistake assumption.** All rows of a given count share one + ordering convention. The whole "ask once" design rests on this; if a file + mixes conventions within a count, a single per-count permutation is wrong. +2. **Largest-count-only capture.** Smaller counts stay unpopulated in the map. + A future consumer (or a later UI iteration) needs a derivation rule to apply + the convention to other counts. +3. **Normalization coupling — mitigated.** To join the sample's raw entries to + the override tables the frontend must match the backend's `split(",")` → + `strip` → `lower`. **Resolution:** store the *normalized* description keys in + `multiEntrySummary` at start (the route already holds the rows), so the + render-time join is exact-match — no cross-repo string-normalization drift. +4. **Portfolio-wide blast radius.** A verify-step edit changes the mapping for + every row with that description, not just the sample address. Must be + messaged in the UI. + +## Suggested issues (`/to-issues`) + +1. Schema: two jsonb columns on `bulk_address_uploads` + migration. +2. Detection at start: compute + persist `multiEntrySummary` (with normalized + description keys). +3. Verify step: list sample descriptions → enum (join override tables), + editable; Next.js upsert route writing `source='user'`; `verifyAck` flag. +4. Order step: largest-count sample, position→part dropdowns → permutation; + persist `multiEntryOrdering`; `orderingConfirmed` flag. +5. Gate: wire `canFinalize` to the two flags; conditional stepper rendering. diff --git a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/multi-entry-ordering/route.ts b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/multi-entry-ordering/route.ts new file mode 100644 index 00000000..676032fa --- /dev/null +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/multi-entry-ordering/route.ts @@ -0,0 +1,49 @@ +import { setMultiEntryOrdering } from "@/lib/bulkUpload/server"; +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { z } from "zod"; + +const PatchSchema = z.object({ + // entry-count -> permutation (part slot -> file position). See ADR-0004. + permutations: z.record(z.string(), z.array(z.number().int().nonnegative())), +}); + +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ portfolioId: string; uploadId: string }> } +) { + const session = await getServerSession(AuthOptions); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + + const { uploadId } = await params; + + let body; + try { + body = PatchSchema.parse(await request.json()); + } catch { + return NextResponse.json({ error: "Invalid input" }, { status: 400 }); + } + + try { + const result = await setMultiEntryOrdering(uploadId, body.permutations); + switch (result.kind) { + case "ok": + return NextResponse.json(result.upload, { status: 200 }); + case "not_found": + return NextResponse.json({ error: "Not found" }, { status: 404 }); + case "wrong_state": + return NextResponse.json( + { error: `Cannot set ordering in state '${result.current}'` }, + { status: 409 } + ); + case "not_multi_entry": + return NextResponse.json({ error: "Upload has no multi-entry rows" }, { status: 409 }); + case "invalid_ordering": + return NextResponse.json({ error: result.reason }, { status: 422 }); + } + } catch (error) { + console.error("Failed to save multi-entry ordering:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/src/app/db/schema/bulk_address_uploads.ts b/src/app/db/schema/bulk_address_uploads.ts index 8166551b..6d6238af 100644 --- a/src/app/db/schema/bulk_address_uploads.ts +++ b/src/app/db/schema/bulk_address_uploads.ts @@ -25,6 +25,17 @@ export interface MultiEntrySummary { sample: MultiEntrySample | null; } +// User-confirmed building-part ordering (ADR-0004). Keyed by entry-count so it +// can hold more than one count later; this iteration populates only the +// largest. permutations[count][k] = the 0-based file position holding building +// part k, where 0 = Main building, 1..N-1 = Extension 1..N-1. +// e.g. { "2": [1, 0] } => for 2-part rows the main building is file position 1. +export interface MultiEntryOrdering { + permutations: Record; + // Set once the user confirms; gates Finalise when the upload is multi-entry. + confirmed: boolean; +} + export const bulkAddressUploads = pgTable("bulk_address_uploads", { id: uuid("id").defaultRandom().primaryKey(), portfolioId: text("portfolio_id").notNull(), @@ -38,6 +49,8 @@ export const bulkAddressUploads = pgTable("bulk_address_uploads", { // Multi-entry building-part detection, computed at start-address-matching // and read by the awaiting_review review surface (ADR-0004). multiEntrySummary: jsonb("multi_entry_summary").$type(), + // User-confirmed building-part ordering for the multi-entry sample (ADR-0004). + multiEntryOrdering: jsonb("multi_entry_ordering").$type(), taskId: uuid("task_id"), combinedOutputS3Uri: text("combined_output_s3_uri"), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx index b80ae1cf..b5ae7f43 100644 --- a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx @@ -1,14 +1,22 @@ "use client"; +import { useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; -import { ArrowRightIcon } from "@heroicons/react/24/outline"; +import { ArrowRightIcon, CheckCircleIcon } from "@heroicons/react/24/outline"; import { useBulkUploadProgress, + useConfirmMultiEntryOrdering, useFinalize, useRequestCombine, } from "@/lib/bulkUpload/client"; -import type { MultiEntrySample } from "@/lib/bulkUpload/multiEntry"; +import { + partLabel, + isPermutation, + assignmentToPermutation, + type MultiEntrySample, +} from "@/lib/bulkUpload/multiEntry"; +import type { MultiEntryOrdering } from "@/app/db/schema/bulk_address_uploads"; interface Props { portfolioSlug: string; @@ -59,14 +67,15 @@ export default function OnboardingProgress({ const isImporting = upload.status === "awaiting_review"; const canRunCombiner = taskDone && !taskFailed && upload.status === "processing"; - const canFinalize = upload.status === "awaiting_review"; + const isAwaitingReview = upload.status === "awaiting_review"; - // Multi-entry building-part sample, shown read-only on the review surface - // (ADR-0004). Ordering confirmation arrives in a later slice. - const multiEntrySample = - upload.status === "awaiting_review" - ? (upload.multiEntrySummary?.sample ?? null) - : null; + // Multi-entry building-part sample on the review surface (ADR-0004). When the + // upload is multi-entry, Finalise is gated on the user confirming the order. + const multiEntrySample = isAwaitingReview + ? (upload.multiEntrySummary?.sample ?? null) + : null; + const orderingConfirmed = upload.multiEntryOrdering?.confirmed ?? false; + const canFinalize = isAwaitingReview && (!multiEntrySample || orderingConfirmed); return (
@@ -128,9 +137,16 @@ export default function OnboardingProgress({ )}
- {multiEntrySample && } + {multiEntrySample && ( + + )} - {(canRunCombiner || canFinalize) && ( + {(canRunCombiner || isAwaitingReview) && (
{canRunCombiner && ( combine.mutate()} /> )} - {canFinalize && ( + {isAwaitingReview && ( finalize.mutate(undefined, { onSuccess: () => router.refresh() }) } @@ -175,35 +193,78 @@ export default function OnboardingProgress({ ); } -// Read-only preview of the largest-count multi-entry row (ADR-0004). Each -// comma-separated entry is a building part; the user will confirm their order -// in a later slice. Positions are shown 1-based, unlabelled for now. -function MultiEntrySamplePanel({ sample }: { sample: MultiEntrySample }) { +// Interactive building-part ordering for the largest-count multi-entry sample +// (ADR-0004). The user labels each file position with a building part (one Main +// building + Extensions); the labels must form a permutation. Confirming +// persists the ordering and unlocks Finalise. +function MultiEntryOrderingPanel({ + sample, + ordering, + portfolioId, + uploadId, +}: { + sample: MultiEntrySample; + ordering: MultiEntryOrdering | null; + portfolioId: string; + uploadId: string; +}) { + const confirm = useConfirmMultiEntryOrdering(portfolioId, uploadId); + const count = sample.count; + + // assignment[filePosition] = building-part slot. Seed from a stored ordering + // (slot -> position, so invert) or default to identity (main building first). + const [assignment, setAssignment] = useState(() => { + const stored = ordering?.permutations?.[String(count)]; + if (stored && stored.length === count) { + const seeded = new Array(count); + stored.forEach((position, slot) => { + seeded[position] = slot; + }); + return seeded; + } + return Array.from({ length: count }, (_, i) => i); + }); + + const confirmed = ordering?.confirmed ?? false; + const valid = isPermutation(assignment); + + const setSlot = (position: number, slot: number) => + setAssignment((prev) => prev.map((s, i) => (i === position ? slot : s))); + + const onConfirm = () => { + if (!valid) return; + confirm.mutate({ + permutations: { [String(count)]: assignmentToPermutation(assignment) }, + }); + }; + return (

- Multiple building parts detected + Confirm building-part order

{sample.address ? {sample.address} : "An address"}{" "} - has {sample.count} building parts (e.g. a main building and extensions). - You'll be asked to confirm their order before finalising. + has {count} building parts. Tell us which entry is the main building and + which are extensions — we'll apply the same order to every{" "} + {count}-part row in this file.

- + {sample.columns.map((column) => ( ))} + - {Array.from({ length: sample.count }).map((_, position) => ( + {Array.from({ length: count }).map((_, position) => ( {sample.columns.map((column) => ( @@ -211,11 +272,54 @@ function MultiEntrySamplePanel({ sample }: { sample: MultiEntrySample }) { {column.entries[position]?.raw ?? "—"} ))} + ))}
PositionEntry {column.header} Building part
{position + 1} + +
+ + {!valid && ( +

+ Each part (Main building, Extension 1, …) must be used exactly once. +

+ )} + +
+ + {confirmed && !confirm.isPending && ( + + + Order confirmed + + )} +
+ + {confirm.error && ( +

{confirm.error.message}

+ )}
); } @@ -224,19 +328,25 @@ function StageButton({ label, activeLabel, isPending, + disabled = false, + disabledReason, onClick, }: { label: string; activeLabel: string; isPending: boolean; + disabled?: boolean; + disabledReason?: string; onClick: () => void; }) { + const blocked = isPending || disabled; return (
+ ); + })} +
+
+ ); + })} + + +

+ Correcting a classification updates that description for{" "} + every row across the portfolio, not + just this address. +

+ {editClassification.error && ( +

{editClassification.error.message}

+ )} + +
+ + {verified && !confirm.isPending && ( + + + Classification verified + + )} +
+ {confirm.error && ( +

{confirm.error.message}

+ )} + + ); +} + // Interactive building-part ordering for the largest-count multi-entry sample // (ADR-0004). The user labels each file position with a building part (one Main // building + Extensions); the labels must form a permutation. Confirming @@ -227,18 +375,22 @@ function MultiEntryOrderingPanel({ sample, ordering, classifications, + stepLabel, portfolioId, uploadId, }: { sample: MultiEntrySample; ordering: MultiEntryOrdering | null; classifications: SampleClassifications; + stepLabel?: string; portfolioId: string; uploadId: string; }) { const confirm = useConfirmMultiEntryOrdering(portfolioId, uploadId); - const editClassification = useEditClassification(portfolioId, uploadId); const count = sample.count; + // Only the multi-valued columns are sliced into building parts; single-value + // columns are whole-dwelling facts (verified in Step 1, not ordered here). + const orderColumns = sample.columns.filter((column) => column.entries.length > 1); // assignment[filePosition] = building-part slot. Seed from a stored ordering // (slot -> position, so invert) or default to identity (main building first). @@ -270,7 +422,7 @@ function MultiEntryOrderingPanel({ return (

- Confirm building-part order + {stepLabel ? `${stepLabel} — ` : ""}Confirm building-part order

{sample.address ? {sample.address} : "An address"}{" "} @@ -284,7 +436,7 @@ function MultiEntryOrderingPanel({ Entry - {sample.columns.map((column) => ( + {orderColumns.map((column) => ( {column.header} @@ -296,37 +448,19 @@ function MultiEntryOrderingPanel({ {Array.from({ length: count }).map((_, position) => ( {position + 1} - {sample.columns.map((column) => { + {orderColumns.map((column) => { const entry = column.entries[position]; const classified = entry ? classifications[column.field]?.[entry.description] ?? "" : ""; - const options = CATEGORY_VALUES[column.field] ?? []; return (

{entry?.raw ?? "—"}
+ {/* Read-only classification annotation; edit it in Step 1. */} {entry && ( - +
+ {classified || "not classified"} +
)} ); @@ -350,15 +484,6 @@ function MultiEntryOrderingPanel({
-

- Correcting a classification updates that description for{" "} - every row across the portfolio, not - just this address. -

- {editClassification.error && ( -

{editClassification.error.message}

- )} - {!valid && (

Each part (Main building, Extension 1, …) must be used exactly once. diff --git a/src/lib/bulkUpload/client.ts b/src/lib/bulkUpload/client.ts index eb29c64a..44d1d158 100644 --- a/src/lib/bulkUpload/client.ts +++ b/src/lib/bulkUpload/client.ts @@ -161,6 +161,25 @@ export function useConfirmMultiEntryOrdering(portfolioId: string, uploadId: stri }); } +// Records the "Verify classification" acknowledgement (ADR-0004 Step 1), +// unlocking Finalise. Per-row corrections go through useEditClassification. +export function useConfirmVerification(portfolioId: string, uploadId: string) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async () => { + const res = await fetch( + `/api/portfolio/${portfolioId}/bulk-uploads/${uploadId}/verify-classification`, + { method: "PATCH" }, + ); + if (!res.ok) throw await parseError(res, "Failed to confirm classification."); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: bulkUploadKeys.progress(uploadId) }); + }, + }); +} + export function useStartAddressMatching(portfolioId: string, uploadId: string) { const queryClient = useQueryClient(); return useMutation<{ taskId: string }, Error, void>({ diff --git a/src/lib/bulkUpload/multiEntry.test.ts b/src/lib/bulkUpload/multiEntry.test.ts new file mode 100644 index 00000000..e7ff6a9b --- /dev/null +++ b/src/lib/bulkUpload/multiEntry.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest"; +import { detectMultiEntry, assignmentToPermutation, isPermutation } from "./multiEntry"; + +// field -> source header, the shape stored on the upload. property_type and +// built_form_type intentionally share a header (the classifier allows it). +const MAPPING = { + address_1: "Addr", + postcode: "PC", + property_type: "Property Type", + wall_type: "Walls", + roof_type: "Roofs", +}; + +describe("detectMultiEntry", () => { + it("returns an empty summary when no classifier columns are mapped", () => { + const rows = [{ Addr: "1 High St", PC: "AB1 2CD" }]; + const summary = detectMultiEntry(rows, { address_1: "Addr", postcode: "PC" }); + expect(summary.sample).toBeNull(); + expect(summary.largestCount).toBe(0); + }); + + it("captures a single-part verify sample when classifier columns exist but no row is multi-entry", () => { + const rows = [ + { Addr: "1 High St", PC: "AB1 2CD", "Property Type": "House: EndTerrace", Walls: "Cavity: AsBuilt", Roofs: "Pitched: 200mm" }, + ]; + const summary = detectMultiEntry(rows, MAPPING); + + // Not multi-entry, but there IS a sample to verify (ADR-0004 Step 1). + expect(summary.largestCount).toBe(0); + expect(summary.sample).not.toBeNull(); + expect(summary.sample!.count).toBe(1); + expect(summary.sample!.address).toBe("1 High St, AB1 2CD"); + // All three mapped classifier columns are present, one entry each. + expect(summary.sample!.columns.map((c) => c.field).sort()).toEqual([ + "property_type", + "roof_type", + "wall_type", + ]); + expect(summary.sample!.columns.every((c) => c.entries.length === 1)).toBe(true); + }); + + it("picks the largest-count row as the sample and reports it as multi-entry", () => { + const rows = [ + { Addr: "1 High St", PC: "AB1 2CD", "Property Type": "House: EndTerrace", Walls: "Cavity: AsBuilt", Roofs: "Pitched: 200mm" }, + { Addr: "2 Low St", PC: "AB3 4EF", "Property Type": "House: Detached", Walls: "Cavity: AsBuilt, Cavity: Filled", Roofs: "Flat: AsBuilt, Pitched: 200mm" }, + ]; + const summary = detectMultiEntry(rows, MAPPING); + + expect(summary.largestCount).toBe(2); + expect(summary.countDistribution).toEqual({ "2": 1 }); + expect(summary.sample!.address).toBe("2 Low St, AB3 4EF"); + expect(summary.sample!.count).toBe(2); + // multiValuedFields are the ones that actually split. + expect([...summary.multiValuedFields].sort()).toEqual(["roof_type", "wall_type"]); + // The whole-dwelling Property Type column is still carried (for Step 1), + // with a single entry — Step 2 filters it out by entries.length. + const propertyCol = summary.sample!.columns.find((c) => c.field === "property_type"); + expect(propertyCol?.entries).toHaveLength(1); + const wallCol = summary.sample!.columns.find((c) => c.field === "wall_type"); + expect(wallCol?.entries.map((e) => e.raw)).toEqual(["Cavity: AsBuilt", "Cavity: Filled"]); + }); + + it("normalizes descriptions to lower-case (matching the classifier's key)", () => { + const rows = [{ Addr: "1 High St", PC: "AB1 2CD", "Property Type": "House: EndTerrace", Walls: "", Roofs: "" }]; + const summary = detectMultiEntry(rows, MAPPING); + const entry = summary.sample!.columns.find((c) => c.field === "property_type")!.entries[0]; + expect(entry.raw).toBe("House: EndTerrace"); + expect(entry.description).toBe("house: endterrace"); + }); +}); + +describe("ordering helpers", () => { + it("isPermutation accepts a bijection and rejects duplicates/out-of-range", () => { + expect(isPermutation([0, 1])).toBe(true); + expect(isPermutation([1, 0, 2])).toBe(true); + expect(isPermutation([0, 0])).toBe(false); + expect(isPermutation([0, 2])).toBe(false); + }); + + it("assignmentToPermutation inverts assignment[pos]=slot to permutation[slot]=pos", () => { + // file position 1 holds the main building (slot 0), position 0 is extension 1. + expect(assignmentToPermutation([1, 0])).toEqual([1, 0]); + expect(assignmentToPermutation([0, 1, 2])).toEqual([0, 1, 2]); + expect(assignmentToPermutation([2, 0, 1])).toEqual([1, 2, 0]); + }); +}); diff --git a/src/lib/bulkUpload/multiEntry.ts b/src/lib/bulkUpload/multiEntry.ts index ee47d774..3bbd053e 100644 --- a/src/lib/bulkUpload/multiEntry.ts +++ b/src/lib/bulkUpload/multiEntry.ts @@ -90,10 +90,17 @@ function buildAddress( return parts.join(", "); } -// Scan the mapped classifier columns for multi-entry rows and capture the -// largest-count sample. Only classifier columns are considered — they're the -// physical-element descriptions we slice into building parts; address columns -// are single-valued by nature. +// Scan the mapped classifier columns and capture one sample address. Only +// classifier columns are considered — they're the physical-element descriptions +// we slice into building parts; address columns are single-valued by nature. +// +// The sample serves both review steps (ADR-0004): the largest-count multi-entry +// row when one exists (Step 2 — Confirm order), otherwise the first row carrying +// any classifier value so Step 1 — Verify classification still has something to +// show. `largestCount >= 2` is the multi-entry signal; `sample != null` means +// "there is something to verify". The sample carries every mapped classifier +// column with a value — Step 1 lists them all; Step 2 renders only the +// multi-valued ones. export function detectMultiEntry( rows: Array>, columnMapping: Record, @@ -104,38 +111,49 @@ export function detectMultiEntry( const multiValued = new Set(); const countDistribution: Record = {}; let largestCount = 0; - let sampleRowIndex = -1; + let multiEntryRowIndex = -1; + // Fallback sample for Step 1 when no row is multi-entry: the first row that + // carries any classifier value. + let firstClassifiedRowIndex = -1; rows.forEach((row, index) => { let rowMax = 0; + let hasValue = false; for (const [field, header] of classifierCols) { const n = splitEntries(row[header]).length; + if (n > 0) hasValue = true; if (n > 1) multiValued.add(field); if (n > rowMax) rowMax = n; } + if (hasValue && firstClassifiedRowIndex === -1) firstClassifiedRowIndex = index; if (rowMax >= 2) { const key = String(rowMax); countDistribution[key] = (countDistribution[key] ?? 0) + 1; - // First row at a new maximum becomes the sample. + // First row at a new maximum becomes the multi-entry sample. if (rowMax > largestCount) { largestCount = rowMax; - sampleRowIndex = index; + multiEntryRowIndex = index; } } }); - if (sampleRowIndex === -1) return EMPTY_MULTI_ENTRY_SUMMARY; + const sampleRowIndex = + multiEntryRowIndex !== -1 ? multiEntryRowIndex : firstClassifiedRowIndex; + if (sampleRowIndex === -1) { + return { multiValuedFields: [...multiValued], countDistribution, largestCount, sample: null }; + } const sampleRow = rows[sampleRowIndex]; - // Show only the columns that are actually split in the sample row; - // single-value columns are whole-dwelling facts, not building parts. + // Every mapped classifier column with a value in the sample row. Step 1 lists + // them all; Step 2's ordering table filters to the multi-valued ones + // (single-value columns are whole-dwelling facts, not building parts). const columns: MultiEntryColumn[] = classifierCols .map(([field, header]) => ({ field, header, entries: splitEntries(sampleRow[header]), })) - .filter((column) => column.entries.length > 1); + .filter((column) => column.entries.length > 0); return { multiValuedFields: [...multiValued], @@ -143,7 +161,7 @@ export function detectMultiEntry( largestCount, sample: { address: buildAddress(sampleRow, columnMapping), - count: largestCount, + count: largestCount >= 2 ? largestCount : 1, columns, }, }; diff --git a/src/lib/bulkUpload/server.ts b/src/lib/bulkUpload/server.ts index 9daae188..09710eb2 100644 --- a/src/lib/bulkUpload/server.ts +++ b/src/lib/bulkUpload/server.ts @@ -289,8 +289,12 @@ export async function setMultiEntryOrdering( if (upload.status !== "awaiting_review") return { kind: "wrong_state", current: upload.status }; - const sample = upload.multiEntrySummary?.sample ?? null; - if (!sample) return { kind: "not_multi_entry" }; + const summary = upload.multiEntrySummary; + // A sample now exists for non-multi-entry uploads too (Step 1's verify + // sample), so "is multi-entry" is largestCount >= 2, not "has a sample". + if (!summary || summary.largestCount < 2 || !summary.sample) + return { kind: "not_multi_entry" }; + const sample = summary.sample; const largest = String(sample.count); if (!permutations[largest]) @@ -310,6 +314,30 @@ export async function setMultiEntryOrdering( return { kind: "ok", upload: updated }; } +export type SetVerifyAckOutcome = + | { kind: "ok"; upload: BulkUpload } + | { kind: "not_found" } + | { kind: "wrong_state"; current: string }; + +// Record the user's "Verify classification" acknowledgement (ADR-0004 Step 1). +// Allowed only at awaiting_review. Gates Finalise whenever the upload has +// classifier columns — independent of multi-entry, hence its own column rather +// than a flag on multiEntryOrdering. +export async function setVerifyAck(uploadId: string): Promise { + const upload = await loadById(uploadId); + if (!upload) return { kind: "not_found" }; + if (upload.status !== "awaiting_review") + return { kind: "wrong_state", current: upload.status }; + + const [updated] = await db + .update(bulkAddressUploads) + .set({ verifyAck: true }) + .where(eq(bulkAddressUploads.id, uploadId)) + .returning(); + if (!updated) return { kind: "not_found" }; + return { kind: "ok", upload: updated }; +} + export type SetMappingOutcome = | { kind: "ok"; upload: BulkUpload } | { kind: "not_found" } From 77cb7d9c0572df51da0498e06fdaf80685d60645 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 3 Jun 2026 11:41:48 +0000 Subject: [PATCH 08/13] save current progress --- .claude/settings.json | 10 +- docs/design/bulk-upload-finaliser.md | 163 ++++++++++++++++++ .../[uploadId]/OnboardingProgress.tsx | 19 +- 3 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 docs/design/bulk-upload-finaliser.md diff --git a/.claude/settings.json b/.claude/settings.json index e5143ec4..974af6a4 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -41,7 +41,15 @@ "Bash(npm run *)", "Bash(grep '\\\\.sql$')", "Bash(git status *)", - "Bash(git checkout *)" + "Bash(git checkout *)", + "Bash(git stash *)", + "Bash(git pull *)", + "Bash(git config *)", + "Bash(GIT_LITERAL_PATHSPECS=1 git add src/app/db/schema/bulk_address_uploads.ts src/lib/bulkUpload/multiEntry.ts src/lib/bulkUpload/server.ts src/lib/bulkUpload/client.ts src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/multi-entry-ordering/route.ts 'src/app/portfolio/[slug]/\\(portfolio\\)/bulk-upload/[uploadId]/OnboardingProgress.tsx' docs/wip/multi-entry-ordering-plan.md)", + "Bash(GIT_LITERAL_PATHSPECS=1 npx eslint src/lib/bulkUpload/server.ts src/lib/bulkUpload/client.ts \"src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/classifications/route.ts\" \"src/app/portfolio/[slug]/\\(portfolio\\)/bulk-upload/[uploadId]/OnboardingProgress.tsx\")", + "Bash(python3 -c \"from backend.app.config import Settings; print\\('COMBINER_SQS_URL =', Settings\\(\\).COMBINER_SQS_URL\\)\")", + "Bash(find /workspaces -name \"*.py\" -path \"*/domain/*\" -o -name \"subtasks.py\" 2>/dev/null | head -20)", + "Read(//workspaces/**)" ], "deny": [ "Bash(npx drizzle-kit generate)", diff --git a/docs/design/bulk-upload-finaliser.md b/docs/design/bulk-upload-finaliser.md new file mode 100644 index 00000000..b56c7b75 --- /dev/null +++ b/docs/design/bulk-upload-finaliser.md @@ -0,0 +1,163 @@ +# Design WIP: `bulk_upload_finaliser` + `property_overrides` + +> **Status:** In progress (grilling session paused 2026-06-03). Not an ADR yet. +> Resume from **Open question Q6** below. When decisions stabilise this should +> graduate into a new ADR in `docs/adr/` (frontend) and likely a companion ADR +> in the Model repo, plus a CONTEXT.md update (see "Docs to update"). + +## Goal + +Two linked pieces of work: + +1. **New backend application `bulk_upload_finaliser`** (lives in + `/workspaces/home/github/Model/applications/`, DDD-aligned — study + `/workspaces/home/github/Model/domain`). It reads the address-matching / + combiner output **and** the `landlord_*_overrides` vocabulary tables, then + writes Postgres correctly: the `property` rows (UPRN + address, as the + frontend does today) and — later — the new `property_overrides` rows. + Motivation: a property list can be ~40,000 rows, too big for a synchronous + Next.js HTTP handler. + +2. **New `property_overrides` table** — the per-Property fact layer that + **ADR-0004 explicitly deferred** ("the per-Property building-part fact layer + that consumes `multiEntryOrdering` and writes main/extension facts at + finalise"). One row per `(property, building_part, component)` carrying the + resolved enum value + provenance. + +**v1 scope:** the finaliser only needs to write `property` (UPRN + address), +matching today's frontend `/finalize`. The `property_overrides` *table* is +designed now; populating it is follow-up work. (Confirm scope — see Q-scope.) + +## Where this sits in the existing pipeline + +``` +BulkUpload → address matching → combiner → awaiting_review → [Finalise] + │ + (new) bulk_upload_finaliser ──────────┘ + reads: combiner output (S3) + landlord_*_overrides + writes: property (+ later property_overrides) + │ + downstream: Ingestion (EPC/solar fetch) + → PropertyBaseline (stage 2, + re-score-on-override seam, + Model ADR-0011/0012) +``` + +`Finalise` (the user action + state-machine gate) stays in Next.js; the new +application is the **worker it dispatches**. Downstream `PropertyBaseline` +already has an override-aware "re-score" seam — `property_overrides` will feed it. + +## Decisions locked + +| # | Decision | +|---|----------| +| Name | Application is **`bulk_upload_finaliser`**, in `Model/applications/`. `Finalise` stays the Next.js action that triggers it. | +| DDD | Follow the DDD structure under `Model/domain`. Domain terms discovered as needed. | +| Schema ownership | **Drizzle (frontend) owns migrations** for both `property` and the new `property_overrides`. | +| Backend access | Backend gets a **`PropertyOverrideRow` SQLModel** (mirror, like `landlord_wall_type_override_table.py`) + a **repository** (see `Model/infrastructure/postgres` + `Model/repositories` for examples). `PropertyRow` must drop its "backend never inserts" invariant and gain insertable columns. | +| Next.js `/finalize` | **Delete it** — fully replaced by the Lambda. | +| `property_overrides` shape | **Single polymorphic table**, not per-component tables. Accepts losing DB-level pgEnum typing on `value`. | +| `value` | `text` — a **denormalised snapshot copy** of the resolved enum value from `landlord_*_overrides` at materialise time (lets us see the value per-property even if vocabulary later changes). | +| `building_part` | **`smallint NOT NULL`**, explicit index: `0 = main building, 1 = extension 1, 2 = extension 2, …` (matches ADR-0004 `multiEntryOrdering.permutations` indexing). | +| Whole-dwelling components | **No special case.** `property_type`/`built_form` are *per-part-capable* too (an extension — conservatory, summer house — can be a different built form / property type). Today's files only supply them once, so they'll usually be written at `building_part = 0` only, but the schema allows per-part with no future migration. | + +## `property_overrides` — columns so far + +Roughly (subject to remaining open questions): + +``` +property_overrides + id uuid pk (default random) -- match landlord_* tables + property_id bigint NOT NULL FK → property.id (FE-owned table) + portfolio_id bigint NOT NULL FK → portfolio.id + building_part smallint NOT NULL -- 0 = main, 1 = ext 1, 2 = ext 2, … + component NOT NULL -- Q6: pgEnum vs text; value set + value text NOT NULL -- snapshot copy of landlord_* resolved enum + source override_source NOT NULL -- 'classifier' | 'user' (reuse existing pgEnum) + description text? -- Q7: store raw landlord description for provenance? + created_at timestamptz NOT NULL default now() + updated_at timestamptz NOT NULL default now() + -- UNIQUE (property_id, component, building_part) -- Q8: confirm +``` + +## Open questions (resume here) + +- **Q6 — `component` discriminator (WE STOPPED HERE).** pgEnum vs text, and the + value set. *Recommendation:* pgEnum `property_component` (or + `override_component`) with the established category names + **`wall_type`, `roof_type`, `property_type`, `built_form_type`** (same keys as + `column_mapping` / `ClassifiableColumn.name`, so finaliser maps category → + component with no translation). pgEnum over text: small closed set, typos + caught at write time (matters more now `value` is free text); new component = + one-line `ALTER TYPE … ADD VALUE`. + +- **Q7 — store raw `description` per override row?** For provenance / re-resolution + / debugging vs redundancy (the description already lives in `landlord_*_overrides`). + *Lean:* store it — cheap, and it pins what text produced this snapshot. + +- **Q8 — uniqueness + FKs.** Confirm `UNIQUE (property_id, component, building_part)`. + `property_id` FK → FE-owned `property.id`; `portfolio_id` as `bigint` (mirror + the `landlord_*` note: FK enforced by Drizzle migration, not the SQLModel). + +- **Q9 — `source` semantics.** Reuse the existing `override_source` pgEnum + (`classifier`/`user`). At materialise time the finaliser copies the source from + the `landlord_*_overrides` row it resolved from. Confirm there's no *per-property* + override concept yet (today overrides are edited at the **vocabulary/portfolio** + level per ADR-0004; property_overrides just snapshots the outcome). + +- **Q-scope — v1 scope.** Confirm v1 = `property` (UPRN + address) only; + `property_overrides` table created but **not populated** until follow-up. + +### Application-flow questions not yet reached + +- **Trigger + orchestration.** How does Finalise dispatch the finaliser? Likely + the existing `TaskOrchestrator` / `subtask_handler` pattern (see ADR-0003, + `landlord_description_overrides` handler) — Next.js `/finalize` triggers a + subtask instead of inserting. Need the trigger body shape (cf. + `LandlordDescriptionOverridesTriggerBody`: `task_id`, `sub_task_id`, `s3_uri`, + `portfolio_id`, …). + +- **State machine / who writes `complete`.** Today Next.js writes `complete` + synchronously after the insert. If Finalise becomes async, **FastAPI/Lambda + writes the terminal status** (mirroring how it already owns + `combining`/`awaiting_review`). This is a CONTEXT.md "Two writers" change. + +- **Input — does the combiner output carry the raw description cells?** v1 only + needs address/UPRN columns (confirmed present: `address2uprn_uprn`, + `address2uprn_address`, `address2uprn_lexiscore`, `Internal Reference`, + address/postcode). For `property_overrides` population (later) the finaliser + also needs the raw `Walls`/`Roofs`/`Property Type` cells **plus** + `multiEntryOrdering` to split per building part — confirm these survive into + the combiner output, or that the finaliser reads them from another source. + +- **Idempotency / re-run.** Today's insert uses + `onConflictDoNothing((portfolio_id, uprn) where uprn is not null)`. Define the + finaliser's re-run behaviour for both tables (esp. that snapshot `value`s won't + refresh on re-run after a vocabulary edit unless we deliberately re-materialise). + +## Key code references (from exploration) + +**Frontend (`assessment-model`):** +- [finalize/route.ts](../../src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/finalize/route.ts) — today's synchronous property insert (to be deleted). +- [property.ts](../../src/app/db/schema/property.ts) — `property` table (`property_type`, `built_form` columns exist; `uq_property_portfolio_uprn`). +- [landlord_overrides.ts](../../src/app/db/schema/landlord_overrides.ts) — the four per-component override tables + all pgEnums (`wallTypeEnum`, `roofTypeEnum`, `propertyTypeEnum`, `builtFormTypeEnum`, `overrideSourceEnum`). +- [bulk_address_uploads.ts](../../src/app/db/schema/bulk_address_uploads.ts) — `multiEntryOrdering` (permutations, `0=main`), `multiEntrySummary`, `verifyAck`, `combinedOutputS3Uri`. +- [ADR-0004](../adr/0004-multi-entry-building-part-ordering.md) — defers exactly this fact layer; the building-part ordering model. +- [ADR-0002](../adr/0002-landlord-override-vocabulary.md) — vocabulary layer. + +**Backend (`Model`):** +- `applications/landlord_description_overrides/handler.py` — the worker pattern to mirror (`subtask_handler`, `TaskOrchestrator`, trigger body, `commit_scope`). +- `infrastructure/postgres/landlord_wall_type_override_table.py` — SQLModel mirror pattern for the new `PropertyOverrideRow`. +- `infrastructure/postgres/landlord_override_enums.py` — shared `override_source` SAEnum pattern. +- `infrastructure/postgres/property_table.py` — `PropertyRow` defensive view ("backend never inserts" — to change). +- `repositories/landlord_overrides/landlord_override_repository.py` — repository pattern for the new override repo. +- `orchestration/landlord_description_overrides_orchestrator.py` — orchestrator pattern; note it splits cells into an orderless set (discards part order — recovered via `multiEntryOrdering`). +- Downstream: `orchestration/property_baseline_orchestrator.py` (re-score-on-override seam), `orchestration/ingestion_orchestrator.py`. + +## Docs to update (when this lands) + +- **CONTEXT.md**: `Property Type` / `built_form` are **per-part-capable**, not + whole-dwelling. Add the per-Property fact layer (`property_overrides`) to the + glossary + relationships. Possibly a `building_part` index definition. +- **New ADR** (frontend) for `property_overrides` + finaliser; companion Model ADR + for the cross-repo write, citing ADR-0003/0004. diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx index 3bdfea47..3e64cca8 100644 --- a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx @@ -27,6 +27,7 @@ import { WallTypeValues, RoofTypeValues, } from "@/app/db/schema/landlord_overrides"; +import { CLASSIFIER_FIELDS } from "@/lib/bulkUpload/columnFields"; // Valid enum options per classifier category, for the editable dropdowns (#299). const CATEGORY_VALUES: Record = { @@ -36,6 +37,14 @@ const CATEGORY_VALUES: Record = { roof_type: RoofTypeValues, }; +// Our category label per classifier field (e.g. built_form_type → "Built Form"). +// Distinguishes the categories when several read from the same source column — +// Property Type and Built Form both come from one column, so labelling by the +// customer's header alone is ambiguous. +const FIELD_LABEL: Record = Object.fromEntries( + CLASSIFIER_FIELDS.map((f) => [f.value, f.label]), +); + interface Props { portfolioSlug: string; portfolioId: string; @@ -290,7 +299,10 @@ function VerifyClassificationPanel({ return (

- {column.header} + {FIELD_LABEL[column.field] ?? column.field} + + — from your “{column.header}” column +

{entries.map((entry) => { @@ -438,7 +450,10 @@ function MultiEntryOrderingPanel({ Entry {orderColumns.map((column) => ( - {column.header} + {FIELD_LABEL[column.field] ?? column.header} + + your “{column.header}” + ))} Building part From fc2664aeef7943ca8ce95be2b9cefbfbf3fbcdd3 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Thu, 4 Jun 2026 11:48:02 +0000 Subject: [PATCH 09/13] bulk upload finaliser --- .claude/settings.json | 16 +- CONTEXT.md | 17 +- docs/design/bulk-upload-finaliser.md | 186 +++++++++++++----- .../bulk-uploads/[uploadId]/finalize/route.ts | 166 +++------------- .../[uploadId]/OnboardingProgress.tsx | 10 + src/lib/bulkUpload/server.ts | 104 +++++++++- src/lib/bulkUpload/types.ts | 23 +++ 7 files changed, 318 insertions(+), 204 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 974af6a4..0343c161 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -49,7 +49,11 @@ "Bash(GIT_LITERAL_PATHSPECS=1 npx eslint src/lib/bulkUpload/server.ts src/lib/bulkUpload/client.ts \"src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/classifications/route.ts\" \"src/app/portfolio/[slug]/\\(portfolio\\)/bulk-upload/[uploadId]/OnboardingProgress.tsx\")", "Bash(python3 -c \"from backend.app.config import Settings; print\\('COMBINER_SQS_URL =', Settings\\(\\).COMBINER_SQS_URL\\)\")", "Bash(find /workspaces -name \"*.py\" -path \"*/domain/*\" -o -name \"subtasks.py\" 2>/dev/null | head -20)", - "Read(//workspaces/**)" + "Read(//workspaces/**)", + "Bash(grep -E '\\\\.sql$')", + "Bash(cd /home/vscode/po-migration *)", + "Read(//home/vscode/po-migration/**)", + "Bash(python -m py_compile applications/bulk_upload_finaliser/handler.py orchestration/bulk_upload_finaliser_orchestrator.py)" ], "deny": [ "Bash(npx drizzle-kit generate)", @@ -61,7 +65,15 @@ "/workspaces/home/github/Model/orchestration", "/workspaces/home/github/Model/backend/address2UPRN/local_handler", "/workspaces/home/github/Model/deployment/terraform/shared", - "/tmp/mig-wt" + "/tmp/mig-wt", + "/workspaces/home/github/Model/docs/adr", + "/workspaces/home/github/Model/infrastructure/postgres", + "/workspaces/home/github/Model/repositories/property", + "/workspaces/home/github/Model/applications/bulk_upload_finaliser", + "/workspaces/home/github/Model/deployment/terraform/lambda/bulkUploadFinaliser", + "/workspaces/home/github/Model/deployment/terraform/lambda/fast-api", + "/workspaces/home/github/Model/backend/app/db/functions", + "/workspaces/home/github/Model/repositories/bulk_upload" ] } } diff --git a/CONTEXT.md b/CONTEXT.md index 039b7c22..4e9234ef 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -38,9 +38,13 @@ The housing association supplying a Portfolio's BulkUploads. A Landlord knows fa _Avoid_: customer, client, owner, organisation (Organisation is a separate, broader entity) **Landlord override**: -A landlord-supplied fact about a property that takes precedence over EPC-derived defaults when computing an assessment. The end-to-end Landlord override journey has two layers — a **VocabularyMapping** layer (this glossary entry below) and a per-Property fact layer (not yet modelled). +A landlord-supplied fact about a property that takes precedence over EPC-derived defaults when computing an assessment. The end-to-end Landlord override journey has two layers — a **VocabularyMapping** layer (this glossary entry below) and a per-Property fact layer (the **Property override**, below). _Avoid_: customer data, manual override, landlord data +**Property override**: +The per-Property fact layer — one resolved fact per `(Property, Building part, component)`, where component is one of `wall_type`/`roof_type`/`property_type`/`built_form_type`. Holds a **snapshot** of the resolved enum value (a denormalised copy of the VocabularyMapping outcome at finalise time, so two Properties sharing a description can later diverge), plus the original spreadsheet text it resolved from. Materialised by the finaliser; see [ADR-0005](./docs/adr/0005-async-bulk-upload-finaliser.md). (Table created; population is follow-up work.) +_Avoid_: per-property mapping, property fact, override row + **VocabularyMapping**: The translation from a Landlord's free-text description in a BulkUpload column (e.g. `"cavity: filledcavity"`) to a canonical domain enum value (e.g. `WallType.CAVITY`). Produced by a `ColumnClassifier` (today an LLM, tomorrow possibly a lookup table or rules engine) in the Model service. Stored per-Portfolio, one row per `(category, description)`. A row carries provenance (`classifier` or `user`) so user overrides survive re-classification. _Avoid_: column mapping (that's a separate concept — see `ColumnMapping` above), classification, dictionary @@ -76,15 +80,18 @@ ready_for_processing → processing (Address matching triggered; Next.js writes) → combining (Combiner stage running; FastAPI writes directly) → awaiting_review (Combiner output in S3; FastAPI writes directly) - → complete (Finalise succeeded; Next.js writes) - → failed (FastAPI reports in-flight failure — schema only, not yet wired) + → finalising (Finalise dispatched; Next.js writes via compare-and-swap) + → complete (Finaliser succeeded; FastAPI/Lambda writes directly) + → failed (Finaliser failed; FastAPI/Lambda writes directly) ``` -`complete` and `failed` are terminal. +`complete` and `failed` are terminal. `finalising` is the in-flight state of the +async finaliser (mirrors `combining`); the UI renders it as "Uploading to ARA". See +[ADR-0005](./docs/adr/0005-async-bulk-upload-finaliser.md). Re-mapping (PATCHing `columnMapping`) is legal only in `ready_for_processing` and `mapping_complete`. Any later state rejects with 409. -**Two writers**: Next.js owns transitions out of `mapping_complete`, into `processing`, and the terminal Finalise outcomes. FastAPI owns `combining` and `awaiting_review` — writing them direct to the DB during the combiner run. The BulkUpload aggregate observes both. +**Two writers**: Next.js owns transitions out of `mapping_complete`, into `processing`, and the `awaiting_review → finalising` compare-and-swap at Finalise dispatch. FastAPI/Lambda owns `combining`, `awaiting_review`, and the terminal `finalising → complete`/`failed` — writing them direct to the DB during the combiner and finaliser runs. The BulkUpload aggregate observes both. See [ADR-0005](./docs/adr/0005-async-bulk-upload-finaliser.md). At `awaiting_review`, **Finalise is gated** (not a new status — a precondition on the action): when classifier columns were mapped the user must acknowledge the classification-verification step, and when the file is **Multi-entry** they must confirm the **Building-part ordering**. See [ADR-0004](./docs/adr/0004-multi-entry-building-part-ordering.md). diff --git a/docs/design/bulk-upload-finaliser.md b/docs/design/bulk-upload-finaliser.md index b56c7b75..3a41e28a 100644 --- a/docs/design/bulk-upload-finaliser.md +++ b/docs/design/bulk-upload-finaliser.md @@ -1,7 +1,8 @@ # Design WIP: `bulk_upload_finaliser` + `property_overrides` -> **Status:** In progress (grilling session paused 2026-06-03). Not an ADR yet. -> Resume from **Open question Q6** below. When decisions stabilise this should +> **Status:** v1 fully resolved (grilling 2026-06-04). Ready to graduate to ADR(s). +> v2 (`property_overrides` population) deferred to its own session — see the +> "Input" application-flow item for its entry point. When decisions stabilise this should > graduate into a new ADR in `docs/adr/` (frontend) and likely a companion ADR > in the Model repo, plus a CONTEXT.md update (see "Docs to update"). @@ -24,9 +25,20 @@ Two linked pieces of work: finalise"). One row per `(property, building_part, component)` carrying the resolved enum value + provenance. -**v1 scope:** the finaliser only needs to write `property` (UPRN + address), -matching today's frontend `/finalize`. The `property_overrides` *table* is -designed now; populating it is follow-up work. (Confirm scope — see Q-scope.) +**Split into two pieces (decided 2026-06-04):** + +- **v1 — async finaliser writes `property`.** Move today's synchronous Next.js + `/finalize` property-insert into a dispatched Lambda (`bulk_upload_finaliser`), + because a property list can be ~40,000 rows. Reproduces the exact 9-column insert + + `onConflictDoNothing`, adds the `finalising` status + async state machine, and + shifts terminal-status ownership to the backend. **Fully designed — ADR-ready.** +- **v2 — populate `property_overrides`.** The per-Property fact layer. The *table* + already shipped (migration 0221, PR #306), but population is a **separate + follow-up** with its own open input-plumbing questions (see the "Input" item + under application-flow questions). Not designed here. + +This doc resolves **v1 in full**; v2 gets its own grilling session against real +classifier-CSV / combiner-output samples. ## Where this sits in the existing pipeline @@ -57,7 +69,9 @@ already has an override-aware "re-score" seam — `property_overrides` will feed | Backend access | Backend gets a **`PropertyOverrideRow` SQLModel** (mirror, like `landlord_wall_type_override_table.py`) + a **repository** (see `Model/infrastructure/postgres` + `Model/repositories` for examples). `PropertyRow` must drop its "backend never inserts" invariant and gain insertable columns. | | Next.js `/finalize` | **Delete it** — fully replaced by the Lambda. | | `property_overrides` shape | **Single polymorphic table**, not per-component tables. Accepts losing DB-level pgEnum typing on `value`. | -| `value` | `text` — a **denormalised snapshot copy** of the resolved enum value from `landlord_*_overrides` at materialise time (lets us see the value per-property even if vocabulary later changes). | +| `override_value` | `text` — a **denormalised snapshot copy** (own value per row) of the resolved enum from `landlord_*_overrides` at materialise time. Own-value (not an FK to the vocabulary) is what lets two properties sharing a description later **diverge**, and lets re-run recalculate one property's value without touching its siblings. | +| Snapshot, **not** FK to vocabulary | `property_overrides` does **not** foreign-key the originating `landlord_*_overrides` row. An FK forces every property sharing a description to share one value (forbids divergence); is structurally impossible as a real FK (4 polymorphic target tables, each with its own value enum); and would risk cascade-deleting per-property facts when re-classification prunes a vocabulary row. Lineage is preserved as a **natural key** — `(portfolio_id, override_component, original_spreadsheet_description)` re-finds the vocabulary row (its `UNIQUE` is `(portfolio_id, description)`) — so deliberate re-sync needs no surrogate FK. | +| Re-run = **recalculate** | The finaliser write to `property_overrides` is `onConflictDoUpdate` on `(property_id, override_component, building_part)`, refreshing `override_value` + `original_spreadsheet_description` + `updated_at` to the latest resolution. Contrast `property`, which stays `onConflictDoNothing` (identity rows, don't churn). When per-property `source='user'` edits exist, the update must guard `WHERE source='classifier'` to preserve hand-edits (mirrors the Model classifier upsert). | | `building_part` | **`smallint NOT NULL`**, explicit index: `0 = main building, 1 = extension 1, 2 = extension 2, …` (matches ADR-0004 `multiEntryOrdering.permutations` indexing). | | Whole-dwelling components | **No special case.** `property_type`/`built_form` are *per-part-capable* too (an extension — conservatory, summer house — can be a different built form / property type). Today's files only supply them once, so they'll usually be written at `building_part = 0` only, but the schema allows per-part with no future migration. | @@ -70,70 +84,136 @@ property_overrides id uuid pk (default random) -- match landlord_* tables property_id bigint NOT NULL FK → property.id (FE-owned table) portfolio_id bigint NOT NULL FK → portfolio.id - building_part smallint NOT NULL -- 0 = main, 1 = ext 1, 2 = ext 2, … - component NOT NULL -- Q6: pgEnum vs text; value set - value text NOT NULL -- snapshot copy of landlord_* resolved enum - source override_source NOT NULL -- 'classifier' | 'user' (reuse existing pgEnum) - description text? -- Q7: store raw landlord description for provenance? + building_part smallint NOT NULL -- 0 = main, 1 = ext 1, 2 = ext 2, … + override_component override_component NOT NULL -- column name == enum type name; pgEnum {wall_type, roof_type, property_type, built_form_type} (Q6 ✓) + override_value text NOT NULL -- snapshot copy of landlord_* resolved enum (free text; `override_component` carries the typing) + -- (no `source`) — dropped Q9: pure value snapshot; add back as nullable column if/when a per-property edit path needs provenance + original_spreadsheet_description text NOT NULL -- raw spreadsheet cell text this snapshot resolved from (Q7 ✓) created_at timestamptz NOT NULL default now() updated_at timestamptz NOT NULL default now() - -- UNIQUE (property_id, component, building_part) -- Q8: confirm + -- UNIQUE (property_id, override_component, building_part) -- Q8 ✓ (source NOT in key — mirrors ADR-0004 single-row flip; portfolio_id implied by property_id) + -- FK property_id → property.id ON DELETE CASCADE; portfolio_id → portfolio.id (Drizzle only; bare bigint in SQLModel mirror); portfolio_id kept (matches property_details_epc / property_targets) ``` ## Open questions (resume here) -- **Q6 — `component` discriminator (WE STOPPED HERE).** pgEnum vs text, and the - value set. *Recommendation:* pgEnum `property_component` (or - `override_component`) with the established category names - **`wall_type`, `roof_type`, `property_type`, `built_form_type`** (same keys as - `column_mapping` / `ClassifiableColumn.name`, so finaliser maps category → - component with no translation). pgEnum over text: small closed set, typos - caught at write time (matters more now `value` is free text); new component = - one-line `ALTER TYPE … ADD VALUE`. +- **Q6 — `component` discriminator. RESOLVED 2026-06-04.** pgEnum + **`override_component`** (column `component`) with values + **`wall_type`, `roof_type`, `property_type`, `built_form_type`**. Verified these + are the *exact* keys used both in the frontend + ([columnFields.ts:30-33](../../src/lib/bulkUpload/columnFields.ts#L30-L33)) and + the backend (`ClassifiableColumn.name` / handler `_build_columns()`), so the + finaliser maps category → component with **no translation**. pgEnum over text: + small closed set, typos caught at write time — and this is now the *only* + DB-level typing left on a row, since `override_value` is free text. New component + = one-line `ALTER TYPE … ADD VALUE` (Drizzle-owned). Enum named `override_*` + (not `property_*`) to sit with `override_source` and stay visually distinct from + the existing *value* enum `property_type`. -- **Q7 — store raw `description` per override row?** For provenance / re-resolution - / debugging vs redundancy (the description already lives in `landlord_*_overrides`). - *Lean:* store it — cheap, and it pins what text produced this snapshot. +- **Q7 — store raw description per override row? RESOLVED 2026-06-04: yes, as + `original_spreadsheet_description text NOT NULL`.** Names the *source artifact* + (the spreadsheet cell), not an actor — sidesteps the Landlord-vs-User conflation + the glossary warns against, and aligns with CONTEXT.md's "the source file". Stored + because `override_value` is a denormalised snapshot that deliberately won't + refresh on later vocabulary edits; pinning the original text makes each row + self-explaining and re-resolvable even after the source `landlord_*_overrides` row + changes. `NOT NULL` is safe **iff** every `property_overrides` row is materialised + from a `landlord_*_overrides` row (whose `description` is itself `NOT NULL`) — + confirm when settling Q9/source semantics. -- **Q8 — uniqueness + FKs.** Confirm `UNIQUE (property_id, component, building_part)`. - `property_id` FK → FE-owned `property.id`; `portfolio_id` as `bigint` (mirror - the `landlord_*` note: FK enforced by Drizzle migration, not the SQLModel). +- **Q8 — uniqueness + FKs. RESOLVED 2026-06-04.** + `UNIQUE (property_id, override_component, building_part)`. `building_part` is in + the key (part 0 and part 1 both carry e.g. a `wall_type` row). `source` is + **deliberately not** in the key — mirrors ADR-0004's single-row-flip (one row, + flip `source` in place; the two-row model was rejected). `portfolio_id` is not in + the key (implied by `property_id`) but **is kept as a column** for query ergonomics + and consistency with `property_details_epc` / `property_targets`, which both + denormalise it. FKs: `property_id → property.id ON DELETE CASCADE`; + `portfolio_id → portfolio.id ON DELETE CASCADE` in the Drizzle migration, but a + bare `bigint` (no FK) in the backend `PropertyOverrideRow` SQLModel mirror — + matching `landlord_wall_type_override_table.py`. -- **Q9 — `source` semantics.** Reuse the existing `override_source` pgEnum - (`classifier`/`user`). At materialise time the finaliser copies the source from - the `landlord_*_overrides` row it resolved from. Confirm there's no *per-property* - override concept yet (today overrides are edited at the **vocabulary/portfolio** - level per ADR-0004; property_overrides just snapshots the outcome). +- **Q9 — `source` semantics. RESOLVED 2026-06-04: drop `source` entirely.** + `property_overrides` is a pure **snapshot of resolved values**. Rationale: there + is no per-property override concept today (per ADR-0004 edits happen at the + **vocabulary/portfolio** level, flipped in place), so a copied `source` would + describe the *vocabulary mapping's* provenance, not this property's — a footgun a + reader/re-score rule could misread, and no consumer needs it in v1. When a genuine + per-property edit path lands (the real use for per-property provenance), `source` + returns as an **additive nullable-column migration** — no need to carry it now. + This also confirms the Q7 `NOT NULL` contingency: every row is still materialised + from a `landlord_*_overrides` row (`description NOT NULL`). -- **Q-scope — v1 scope.** Confirm v1 = `property` (UPRN + address) only; - `property_overrides` table created but **not populated** until follow-up. +- **Q-scope — v1 scope. RESOLVED 2026-06-04.** v1 = the finaliser reproduces + today's **exact 9-column `property` insert** (`portfolio_id`, + `creation_status='READY'`, `uprn`, `landlord_property_id` ← `Internal Reference`, + `address` = matched ?? user-inputted, `postcode`, `user_inputted_address`, + `user_inputted_postcode`, `lexiscore`) **+** `onConflictDoNothing` on + `(portfolio_id, uprn) where uprn is not null` — not a reduced "UPRN + address". + This sizes the "PropertyRow gains insertable columns" decision to all nine + columns plus `creation_status`. The `property_overrides` *table* shipped ahead + (migration 0221, PR #306) but is **not populated** in v1 — population is + follow-up work (and needs a different input source; see combiner-output note + below). ### Application-flow questions not yet reached -- **Trigger + orchestration.** How does Finalise dispatch the finaliser? Likely - the existing `TaskOrchestrator` / `subtask_handler` pattern (see ADR-0003, - `landlord_description_overrides` handler) — Next.js `/finalize` triggers a - subtask instead of inserting. Need the trigger body shape (cf. - `LandlordDescriptionOverridesTriggerBody`: `task_id`, `sub_task_id`, `s3_uri`, - `portfolio_id`, …). +- **Trigger + orchestration. RESOLVED 2026-06-04.** Mirror the + `start-address-matching` path. Next.js creates a `SubTask` (`service: + "finaliser"`) under the BulkUpload's existing Task, then POSTs a new FastAPI + endpoint `POST /v1/bulk-uploads/trigger-finaliser` (auth via `validate_token`), + which enqueues to a **new SQS queue**; a Lambda runs the finaliser wrapped in + `@subtask_handler` (auto-injected `TaskOrchestrator`; `run_subtask` owns the + subtask start/complete/fail + Task cascade). Trigger body + `FinaliserTriggerBody { task_id, sub_task_id, s3_uri (combined output), portfolio_id }` + (extends `SubtaskTriggerBody`). Slow work stays outside the txn; persistence in a + `commit_scope`. The synchronous Next.js `/finalize` route is deleted (locked). -- **State machine / who writes `complete`.** Today Next.js writes `complete` - synchronously after the insert. If Finalise becomes async, **FastAPI/Lambda - writes the terminal status** (mirroring how it already owns - `combining`/`awaiting_review`). This is a CONTEXT.md "Two writers" change. +- **State machine / who writes `complete`. RESOLVED 2026-06-04.** New status + **`finalising`** between `awaiting_review` and `complete` (mirrors `combining` + before `awaiting_review`). Lifecycle: + `awaiting_review → finalising → complete` (↘ `failed`). + - **`finalising`** written by **Next.js at dispatch** via a **compare-and-swap**: + `UPDATE … SET status='finalising' WHERE id=? AND status='awaiting_review'` — + 0 rows ⇒ already dispatched ⇒ 409. This is the **double-dispatch guard** (closes + the simultaneous-click race under `loadForFinalize`'s existing precondition). + - **`complete` / `failed`** written by the **Lambda** directly to + `bulk_address_uploads` (new `set_finalized_status` / `set_failed_status`, + exactly like the combiner's `set_combining_status` / + `set_combined_output_s3_uri`). `markFinalized` + the Next.js `/finalize` route + are deleted. + - **CONTEXT.md "Two writers" change:** Next.js owns dispatch + the + `awaiting_review → finalising` CAS; the backend owns `finalising → complete` + and `→ failed` (in addition to `combining` / `awaiting_review`). + - **UI vs canonical:** persisted enum value is `finalising` (canonical; ties to + the **Finalise** action). The frontend renders it as **"Uploading to ARA"** — a + display-layer label only, **not** the enum name, so UX copy never needs a + migration. -- **Input — does the combiner output carry the raw description cells?** v1 only - needs address/UPRN columns (confirmed present: `address2uprn_uprn`, +- **Input — does the combiner output carry the raw description cells? RESOLVED + 2026-06-04: NO. This is a v2 problem (deferred).** v1 needs only address/UPRN + columns, all **confirmed present** in the combiner output (`address2uprn_uprn`, `address2uprn_address`, `address2uprn_lexiscore`, `Internal Reference`, - address/postcode). For `property_overrides` population (later) the finaliser - also needs the raw `Walls`/`Roofs`/`Property Type` cells **plus** - `multiEntryOrdering` to split per building part — confirm these survive into - the combiner output, or that the finaliser reads them from another source. + `Address 1/2/3`, `postcode`). The raw `Walls`/`Roofs`/`Property Type`/`Built Form` + cells are **not** in the combiner output — they survive only in (a) the + `{uploadId}-classifier.csv` on S3 (original headers) and (b) `landlord_*_overrides` + as *resolved* values keyed by description. So v2 population must assemble **four + inputs**, not one file: + - `property_id` (identity) ← combiner output `(portfolio_id, uprn)` — **but + no-UPRN rows have no such key**; + - raw cell text ← the classifier CSV (not the combiner output); + - cell → building-part split ← `multiEntryOrdering` on `bulk_address_uploads`; + - description → `override_value` ← `landlord_*_overrides` (normalized description). + - **Two open v2 hazards (entry point for the v2 session):** (1) the join key + between classifier CSV and combiner output — is there a stable per-row key + (`Internal Reference`?) and is row order preserved through postcode-split + + combine? (2) obtaining `property_id` for unmatched (no-UPRN) rows — v1's + `onConflictDoNothing` returns no ids, so v2 likely needs `RETURNING id` mapped + back to source rows. -- **Idempotency / re-run.** Today's insert uses - `onConflictDoNothing((portfolio_id, uprn) where uprn is not null)`. Define the - finaliser's re-run behaviour for both tables (esp. that snapshot `value`s won't - refresh on re-run after a vocabulary edit unless we deliberately re-materialise). +- **Idempotency / re-run. RESOLVED 2026-06-04 (per-table).** + - `property`: keep today's `onConflictDoNothing` on `(portfolio_id, uprn) where uprn is not null` — existing properties are not churned. + - `property_overrides`: `onConflictDoUpdate` on `(property_id, override_component, building_part)` — **recalculate** `override_value` + `original_spreadsheet_description` + `updated_at` to the latest resolution, so an existing property whose override changed is refreshed in place. Guard `WHERE source='classifier'` once a per-property user-edit path exists (until then every row is classifier-derived, so blind overwrite is correct). See the "Re-run = recalculate" and "Snapshot, not FK" locked decisions. ## Key code references (from exploration) diff --git a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/finalize/route.ts b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/finalize/route.ts index b6fd7ec9..f326c65c 100644 --- a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/finalize/route.ts +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/finalize/route.ts @@ -1,160 +1,46 @@ -import { db } from "@/app/db/db"; -import { property } from "@/app/db/schema/property"; -import { sql } from "drizzle-orm"; import { NextRequest, NextResponse } from "next/server"; -import { revalidatePath } from "next/cache"; import { getServerSession } from "next-auth"; import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; -import { createRetrofitDataS3Client } from "@/app/utils/s3"; -import * as XLSX from "xlsx"; -import { loadForFinalize, markFinalized } from "@/lib/bulkUpload/server"; - -const ADDRESS_COLS = ["Address 1", "Address 2", "Address 3"] as const; -const POSTCODE_COL = "postcode"; -const INTERNAL_REF_COL = "Internal Reference"; -const UPRN_COL = "address2uprn_uprn"; -const MATCHED_ADDRESS_COL = "address2uprn_address"; -const LEXISCORE_COL = "address2uprn_lexiscore"; -const MISSING_SENTINEL = "invalid postcode"; -const UK_POSTCODE_RE = /[A-Z]{1,2}\d[A-Z\d]?\s*\d[A-Z]{2}/i; - -function normalize(v: unknown): string { - if (v === null || v === undefined) return ""; - return String(v).trim(); -} - -function isMissing(v: string): boolean { - return v === "" || v.toLowerCase() === MISSING_SENTINEL; -} - -function parseUprn(raw: unknown): bigint | null { - const v = normalize(raw); - if (isMissing(v)) return null; - try { - return BigInt(v); - } catch { - return null; - } -} - -function parseLexiscore(raw: unknown): number | null { - const v = normalize(raw); - if (isMissing(v)) return null; - const n = Number(v); - return Number.isFinite(n) ? n : null; -} - -function extractPostcode(matched: string | null, fallback: string): string | null { - if (matched) { - const m = matched.match(UK_POSTCODE_RE); - if (m) return m[0].toUpperCase(); - } - return fallback || null; -} - -function parseS3Uri(uri: string): { bucket: string; key: string } | null { - if (!uri.startsWith("s3://")) return null; - const rest = uri.slice(5); - const slash = rest.indexOf("/"); - if (slash < 0) return null; - return { bucket: rest.slice(0, slash), key: rest.slice(slash + 1) }; -} +import { readSessionToken } from "@/lib/session"; +import { dispatchFinaliser } from "@/lib/bulkUpload/server"; +// Finalise is now asynchronous (ADR-0005). This route no longer inserts +// properties; it dispatches the bulk_upload_finaliser Lambda and flips the +// BulkUpload to `finalising` via a compare-and-swap (the double-dispatch guard). +// The Lambda reads the combiner output, inserts the property rows, and writes the +// terminal `complete`/`failed` status directly. The user sees "Uploading to ARA" +// while the row is `finalising`; the onboarding surface polls for the outcome. export async function POST( - _request: NextRequest, + request: NextRequest, { params }: { params: Promise<{ portfolioId: string; uploadId: string }> } ) { const session = await getServerSession(AuthOptions); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); const { uploadId } = await params; + const sessionToken = readSessionToken(request); - const guarded = await loadForFinalize(uploadId); - switch (guarded.kind) { + const result = await dispatchFinaliser({ uploadId, sessionToken }); + + switch (result.kind) { + case "ok": + // Accepted: the finaliser is running; the row is now `finalising`. + return NextResponse.json({ taskId: result.taskId }, { status: 202 }); case "not_found": return NextResponse.json({ error: "Not found" }, { status: 404 }); case "already_finalized": + // Idempotent: nothing to do. return new NextResponse(null, { status: 200 }); - case "wrong_state": - return NextResponse.json( - { error: `Upload not ready to finalize (state: ${guarded.current})` }, - { status: 409 } - ); case "not_yet_combined": return NextResponse.json({ error: "Combiner not finished" }, { status: 409 }); - } - const upload = guarded.upload; - - const parsed = parseS3Uri(upload.combinedOutputS3Uri!); - if (!parsed) { - return NextResponse.json({ error: "Invalid combined output S3 URI" }, { status: 500 }); - } - - const s3 = createRetrofitDataS3Client(); - - let rawRows: Record[]; - try { - const obj = await s3 - .getObject({ Bucket: parsed.bucket, Key: parsed.key }) - .promise(); - const buf = Buffer.from(obj.Body as Uint8Array); - const wb = XLSX.read(buf, { type: "buffer" }); - const sheet = wb.Sheets[wb.SheetNames[0]]; - rawRows = XLSX.utils.sheet_to_json>(sheet, { defval: "" }); - } catch (err) { - console.error("Failed to read combined CSV from S3:", err); - return NextResponse.json({ error: "Failed to read combined CSV" }, { status: 502 }); - } - - const portfolioIdBig = BigInt(upload.portfolioId); - - const values = rawRows.map((raw) => { - const userInputtedAddress = - ADDRESS_COLS.map((c) => normalize(raw[c])).filter(Boolean).join(", ") || null; - const userInputtedPostcode = normalize(raw[POSTCODE_COL]) || null; - - const uprn = parseUprn(raw[UPRN_COL]); - - const matchedAddressRaw = normalize(raw[MATCHED_ADDRESS_COL]); - const matchedAddress = isMissing(matchedAddressRaw) ? null : matchedAddressRaw; - - const address = matchedAddress ?? userInputtedAddress; - const postcode = extractPostcode(matchedAddress, userInputtedPostcode ?? ""); - - const internalRef = normalize(raw[INTERNAL_REF_COL]) || null; - const lexiscore = parseLexiscore(raw[LEXISCORE_COL]); - - return { - portfolioId: portfolioIdBig, - creationStatus: "READY" as const, - uprn, - landlordPropertyId: internalRef, - address, - postcode, - userInputtedAddress, - userInputtedPostcode, - lexiscore, - }; - }); - - try { - if (values.length > 0) { - await db - .insert(property) - .values(values) - .onConflictDoNothing({ - target: [property.portfolioId, property.uprn], - where: sql`${property.uprn} IS NOT NULL`, - }); - } - - await markFinalized(uploadId); - - revalidatePath("/portfolio/[slug]", "layout"); - - return new NextResponse(null, { status: 200 }); - } catch (err) { - console.error("Failed to finalize bulk upload:", err); - return NextResponse.json({ error: "Failed to import properties" }, { status: 500 }); + case "missing_task": + return NextResponse.json({ error: "Upload has no task to finalise" }, { status: 409 }); + case "wrong_state": + return NextResponse.json( + { error: `Upload not ready to finalize (state: ${result.current})` }, + { status: 409 } + ); + case "trigger_failed": + return NextResponse.json({ error: result.message }, { status: result.status }); } } diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx index 3e64cca8..749e6b7f 100644 --- a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx @@ -28,6 +28,7 @@ import { RoofTypeValues, } from "@/app/db/schema/landlord_overrides"; import { CLASSIFIER_FIELDS } from "@/lib/bulkUpload/columnFields"; +import { statusLabel } from "@/lib/bulkUpload/types"; // Valid enum options per classifier category, for the editable dropdowns (#299). const CATEGORY_VALUES: Record = { @@ -100,6 +101,9 @@ export default function OnboardingProgress({ const taskFailed = TASK_FAILED_STATUSES.has(taskStatus); const isCombining = upload.status === "combining"; const isImporting = upload.status === "awaiting_review"; + // Async finaliser in flight (ADR-0005). Polling continues (non-terminal) until + // the backend writes complete/failed. Surfaced to the user as "Uploading to ARA". + const isFinalising = upload.status === "finalising"; const canRunCombiner = taskDone && !taskFailed && upload.status === "processing"; const isAwaitingReview = upload.status === "awaiting_review"; @@ -178,6 +182,12 @@ export default function OnboardingProgress({ Awaiting import )} + {isFinalising && ( + + + {statusLabel("finalising")}… + + )}
{needsVerify && sample && ( diff --git a/src/lib/bulkUpload/server.ts b/src/lib/bulkUpload/server.ts index 09710eb2..2de69fff 100644 --- a/src/lib/bulkUpload/server.ts +++ b/src/lib/bulkUpload/server.ts @@ -562,9 +562,105 @@ export async function loadForFinalize(uploadId: string): Promise { - await db +export type DispatchFinaliserOutcome = + | { kind: "ok"; taskId: string; subTaskId: string } + | { kind: "not_found" } + | { kind: "already_finalized" } + | { kind: "not_yet_combined" } + | { kind: "wrong_state"; current: string } + | { kind: "missing_task" } + | { kind: "trigger_failed"; status: number; message: string }; + +// Dispatch the async bulk_upload_finaliser (ADR-0005). Replaces the old +// synchronous property insert + markFinalized. Order matters: +// 1. loadForFinalize — rich guards (combined output present, awaiting_review). +// 2. CAS claim `awaiting_review → finalising` — the double-dispatch guard: +// of two simultaneous clicks exactly one updates a row; the loser gets 409. +// 3. create the finaliser subtask under the upload's existing Task + POST the +// trigger. On trigger failure, revert the status so the user can retry and +// mark the subtask failed. The backend writes the terminal complete/failed. +export async function dispatchFinaliser(args: { + uploadId: string; + sessionToken: string | undefined; +}): Promise { + const guarded = await loadForFinalize(args.uploadId); + switch (guarded.kind) { + case "not_found": + return { kind: "not_found" }; + case "already_finalized": + return { kind: "already_finalized" }; + case "not_yet_combined": + return { kind: "not_yet_combined" }; + case "wrong_state": + return { kind: "wrong_state", current: guarded.current }; + } + const upload = guarded.upload; + if (!upload.taskId) return { kind: "missing_task" }; + + // CAS: atomically claim the dispatch. Only the request that flips + // awaiting_review → finalising proceeds; a concurrent one updates 0 rows. + const claimed = await db .update(bulkAddressUploads) - .set({ status: "complete" }) - .where(eq(bulkAddressUploads.id, uploadId)); + .set({ status: "finalising" }) + .where( + and( + eq(bulkAddressUploads.id, args.uploadId), + eq(bulkAddressUploads.status, "awaiting_review"), + ), + ) + .returning(); + if (claimed.length === 0) { + const current = await loadById(args.uploadId); + if (current?.status === "complete") return { kind: "already_finalized" }; + return { kind: "wrong_state", current: current?.status ?? "unknown" }; + } + + const [subTask] = await db + .insert(subTasks) + .values({ + taskId: upload.taskId, + status: "waiting", + service: SUBTASK_SERVICE.finaliser, + inputs: JSON.stringify({ bulk_upload_id: args.uploadId }), + }) + .returning(); + + const payload = { + task_id: upload.taskId, + sub_task_id: subTask.id, + s3_uri: upload.combinedOutputS3Uri, + portfolio_id: Number(upload.portfolioId), + bulk_upload_id: args.uploadId, + }; + + const trigger = await triggerFastApiPipeline({ + endpoint: "/v1/bulk-uploads/trigger-finaliser", + payload, + sessionToken: args.sessionToken, + }); + + if (!trigger.ok) { + // Roll the claim back so the user can retry, and fail the subtask. + await Promise.all([ + db + .update(bulkAddressUploads) + .set({ status: "awaiting_review" }) + .where(eq(bulkAddressUploads.id, args.uploadId)), + db + .update(subTasks) + .set({ + status: "failed", + outputs: JSON.stringify({ error: trigger.message }), + }) + .where(eq(subTasks.id, subTask.id)), + ]); + return { kind: "trigger_failed", status: trigger.status, message: trigger.message }; + } + + await db + .update(subTasks) + .set({ status: "in progress", inputs: JSON.stringify(payload) }) + .where(eq(subTasks.id, subTask.id)); + + return { kind: "ok", taskId: upload.taskId, subTaskId: subTask.id }; } diff --git a/src/lib/bulkUpload/types.ts b/src/lib/bulkUpload/types.ts index 38d8f125..9d6a6049 100644 --- a/src/lib/bulkUpload/types.ts +++ b/src/lib/bulkUpload/types.ts @@ -6,6 +6,10 @@ export const BULK_UPLOAD_STATUSES = [ "processing", "combining", "awaiting_review", + // In-flight state of the async finaliser (ADR-0005); mirrors `combining`. The + // status column is free text, so no enum migration is needed. UI renders this + // as "Uploading to ARA" — see STATUS_LABELS. + "finalising", "complete", "failed", ] as const; @@ -19,8 +23,27 @@ export type BulkUpload = typeof bulkAddressUploads.$inferSelect; export const SUBTASK_SERVICE = { address: "address2uprn", classifier: "landlord_description_overrides", + finaliser: "bulk_upload_finaliser", } as const; +// User-facing label for a BulkUpload status. The persisted enum value stays +// canonical (`finalising`); the product surface for that state is "Uploading to +// ARA" (ADR-0005 — a display-layer label, never the enum name). +export const STATUS_LABELS: Record = { + ready_for_processing: "Ready for processing", + mapping_complete: "Mapping complete", + processing: "Processing", + combining: "Combining", + awaiting_review: "Awaiting review", + finalising: "Uploading to ARA", + complete: "Complete", + failed: "Failed", +}; + +export function statusLabel(status: string): string { + return STATUS_LABELS[status] ?? status; +} + export type TaskSummary = { id: string; taskSource: string; From 5fe86f01d89c436deaa23fbcb294059f74e4c093 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Thu, 4 Jun 2026 18:21:35 +0000 Subject: [PATCH 10/13] Render `finalising` status on the bulk-upload page and auto-advance to complete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The async finaliser (ADR-0005) introduced the `finalising` status, but the server page's STATUS_CONFIG had no entry for it, so it fell through to the `ready_for_processing` fallback ("Awaiting column mapping") and never mounted the live poller — the page looked stuck even though the Lambda had inserted the properties and written `complete`. - Add the `finalising` card ("Uploading to ARA") to STATUS_CONFIG. - Render OnboardingProgress during `finalising` so it polls live. - Refresh the server page once when the poll first sees a terminal status (guarded by a new `serverStatus` prop to avoid a loop; uses react-query v4 onSuccess, no useEffect) so it advances to the "Processing complete" card. - Add a `finalising` → "Uploading to ARA" badge on the uploads list. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../[uploadId]/OnboardingProgress.tsx | 20 +++++++++++++++++-- .../bulk-upload/[uploadId]/page.tsx | 10 ++++++++++ .../[slug]/(portfolio)/bulk-upload/page.tsx | 1 + src/lib/bulkUpload/client.ts | 9 ++++++++- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx index 749e6b7f..041ca389 100644 --- a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx @@ -28,7 +28,7 @@ import { RoofTypeValues, } from "@/app/db/schema/landlord_overrides"; import { CLASSIFIER_FIELDS } from "@/lib/bulkUpload/columnFields"; -import { statusLabel } from "@/lib/bulkUpload/types"; +import { statusLabel, isTerminalStatus } from "@/lib/bulkUpload/types"; // Valid enum options per classifier category, for the editable dropdowns (#299). const CATEGORY_VALUES: Record = { @@ -50,6 +50,10 @@ interface Props { portfolioSlug: string; portfolioId: string; uploadId: string; + // The status at the last server render. Used to refresh the server page exactly + // once when polling first observes a terminal status (async finalise, ADR-0005), + // so the page advances from "Uploading to ARA" to the "Processing complete" card. + serverStatus: string; isDomnaUser: boolean; } @@ -60,10 +64,22 @@ export default function OnboardingProgress({ portfolioSlug, portfolioId, uploadId, + serverStatus, isDomnaUser, }: Props) { const router = useRouter(); - const progress = useBulkUploadProgress(portfolioId, uploadId); + const progress = useBulkUploadProgress(portfolioId, uploadId, { + // When the async finaliser finishes, the poll flips the status to a terminal + // value while the server page is still on `finalising`. Refresh once so the + // server re-renders the "Processing complete" / "failed" card. Guarding on the + // non-terminal serverStatus prevents a refresh loop: after the refresh the + // prop is terminal, so this no-ops. + onSuccess: (data) => { + if (!isTerminalStatus(serverStatus) && isTerminalStatus(data.upload.status)) { + router.refresh(); + } + }, + }); const combine = useRequestCombine(portfolioId, uploadId); const finalize = useFinalize(portfolioId, uploadId); diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/page.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/page.tsx index f28787ff..522fc8ce 100644 --- a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/page.tsx @@ -67,6 +67,14 @@ const STATUS_CONFIG = { body: "Matches ready, writing into your portfolio.", cta: false, }, + finalising: { + icon: ArrowPathIcon, + iconBg: "bg-blue-50", + iconColor: "text-blue-500", + title: "Uploading to ARA", + body: "Creating your properties from the matched addresses. This can take a little while for large files.", + cta: false, + }, complete: { icon: CheckCircleIcon, iconBg: "bg-green-50", @@ -167,6 +175,7 @@ export default async function BulkUploadDetailPage(props: { {(statusKey === "processing" || statusKey === "combining" || statusKey === "awaiting_review" || + statusKey === "finalising" || statusKey === "complete" || statusKey === "failed") && upload.taskId && ( @@ -174,6 +183,7 @@ export default async function BulkUploadDetailPage(props: { portfolioSlug={slug} portfolioId={upload.portfolioId} uploadId={uploadId} + serverStatus={upload.status} isDomnaUser={isDomnaUser} /> )} diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/page.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/page.tsx index 281f5744..6aaca9c2 100644 --- a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/page.tsx @@ -14,6 +14,7 @@ import { const STATUS_LABELS: Record = { ready_for_processing: { label: "Ready", classes: "bg-amber-100 text-amber-700" }, processing: { label: "Processing", classes: "bg-blue-100 text-blue-700" }, + finalising: { label: "Uploading to ARA", classes: "bg-blue-100 text-blue-700" }, complete: { label: "Complete", classes: "bg-green-100 text-green-700" }, failed: { label: "Failed", classes: "bg-red-100 text-red-700" }, }; diff --git a/src/lib/bulkUpload/client.ts b/src/lib/bulkUpload/client.ts index 44d1d158..2a50023a 100644 --- a/src/lib/bulkUpload/client.ts +++ b/src/lib/bulkUpload/client.ts @@ -197,7 +197,11 @@ export function useStartAddressMatching(portfolioId: string, uploadId: string) { }); } -export function useBulkUploadProgress(portfolioId: string, uploadId: string) { +export function useBulkUploadProgress( + portfolioId: string, + uploadId: string, + options?: { onSuccess?: (data: ProgressView) => void }, +) { return useQuery({ queryKey: bulkUploadKeys.progress(uploadId), queryFn: async () => { @@ -211,6 +215,9 @@ export function useBulkUploadProgress(portfolioId: string, uploadId: string) { const status = data?.upload.status; return status && isTerminalStatus(status) ? false : 3000; }, + // v4 onSuccess fires after each successful poll; callers use it to react to a + // status transition (e.g. refresh the server page once it goes terminal). + onSuccess: options?.onSuccess, }); } From 5716f8e5cc0c9442650404c6c1594a2584681be9 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Thu, 4 Jun 2026 18:21:42 +0000 Subject: [PATCH 11/13] Add v2 handover doc for property_overrides population MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-contained brief to start a fresh context implementing the per-Property fact layer in the bulk_upload_finaliser: locked decisions, the four-input assembly, the two open hazards (classifier-CSV↔combiner-output join key; property_id for no-UPRN rows), candidate architectures, and a first-steps list. v1 (async finalise writing `property`) is shipped and working. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../bulk-upload-finaliser-v2-handover.md | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 docs/design/bulk-upload-finaliser-v2-handover.md diff --git a/docs/design/bulk-upload-finaliser-v2-handover.md b/docs/design/bulk-upload-finaliser-v2-handover.md new file mode 100644 index 00000000..281d4ec4 --- /dev/null +++ b/docs/design/bulk-upload-finaliser-v2-handover.md @@ -0,0 +1,208 @@ +# Handover: `bulk_upload_finaliser` v2 — populate `property_overrides` + +> **Purpose.** Self-contained brief to start a fresh context implementing v2 (the +> per-Property fact layer) in the existing `bulk_upload_finaliser` Lambda. v1 +> (async finalise that writes `property`) is **shipped and working end-to-end**. +> This doc assumes no memory of the v1 session. + +## 1. Where v1 left things (read first) + +v1 made **Finalise** an async dispatched Lambda that writes `property` rows. The +full flow works in dev: dispatch `202` → SQS → Lambda inserts properties + writes +terminal status → UI advances to "Processing complete". + +Authoritative background — **read these before coding**: +- `docs/design/bulk-upload-finaliser.md` — the full grilling/design doc (schema Q6–Q9, snapshot-not-FK, recalculate-on-rerun, the v2 input hazards). +- `docs/adr/0005-async-bulk-upload-finaliser.md` (frontend) — state machine + `property_overrides` shape. +- `/workspaces/home/github/Model/docs/adr/0013-bulk-upload-finaliser-writes-properties.md` (backend) — the Lambda write path + DDD layering. +- `docs/adr/0004-multi-entry-building-part-ordering.md` — **critical for v2**: how building parts and `multiEntryOrdering` work. +- `docs/adr/0002-landlord-override-vocabulary.md` — the vocabulary (`landlord_*_overrides`) layer v2 resolves against. +- `CONTEXT.md` — glossary: **Property override**, **Building part**, **Main building**, **Extension**, **Multi-entry**, **Building-part ordering**, **VocabularyMapping**. + +**Convention that must hold (it was corrected hard in v1):** in the Model repo, +business logic lives in `orchestration/*_orchestrator.py`; the Lambda +`applications/*/handler.py` stays thin (parse trigger, wire infra, delegate). One +repository per aggregate; orchestrators never commit (the handler owns the +transaction via `commit_scope`). See memory `model-ddd-layering`. + +## 2. v2 goal + +Populate `property_overrides` during finalise: for each property, write one row per +`(building_part, override_component)` carrying the **resolved enum snapshot** of the +landlord's description for that part. + +## 3. The target table (already shipped — migration 0221, do NOT re-migrate) + +Drizzle: `src/app/db/schema/property_overrides.ts`. + +``` +property_overrides + id uuid pk + property_id bigint NOT NULL FK → property.id ON DELETE CASCADE + portfolio_id bigint NOT NULL FK → portfolio.id ON DELETE CASCADE + building_part smallint NOT NULL -- 0 = main, 1 = ext 1, 2 = ext 2, … + override_component override_component NOT NULL -- pgEnum {wall_type, roof_type, property_type, built_form_type} + override_value text NOT NULL -- snapshot of the resolved enum value + original_spreadsheet_description text NOT NULL -- raw cell text it resolved from + created_at / updated_at timestamptz NOT NULL + UNIQUE (property_id, override_component, building_part) +``` + +## 4. Design decisions already locked (do not relitigate) + +- **Snapshot, not FK.** `override_value` is a denormalised text copy of the resolved + enum, taken at materialise time — *not* an FK to `landlord_*_overrides`. This is + what lets two properties sharing a description diverge later, and is required + because there are four polymorphic vocabulary tables. Lineage is the natural key + `(portfolio_id, override_component, original_spreadsheet_description)`. +- **Re-run = recalculate.** Write with `onConflictDoUpdate` on + `(property_id, override_component, building_part)`, refreshing `override_value` + + `original_spreadsheet_description` + `updated_at`. (Contrast `property`, which is + `onConflictDoNothing`.) When a per-property user-edit path eventually exists, this + upsert will need a `WHERE source='classifier'` guard — but there is **no `source` + column in v1**; add it as a nullable column only when that path is built. +- **`override_component` values** are exactly the classifier category keys + (`wall_type`, `roof_type`, `property_type`, `built_form_type`) used in both + `src/lib/bulkUpload/columnFields.ts` and the Model `ClassifiableColumn.name` — no + translation. +- **`building_part` indexing**: `0 = Main building`, `1 = Extension 1`, … per ADR-0004. +- **Whole-dwelling components** (`property_type`, `built_form_type`) are per-part- + capable but today's files supply them once → usually written at `building_part = 0`. + +## 5. The hard part: assembling the inputs (this is the real v2 work) + +The combiner output (what the v1 finaliser reads) carries **only** address/UPRN +columns — `Address 1/2/3`, `postcode`, `Internal Reference`, `address2uprn_uprn`, +`address2uprn_address`, `address2uprn_lexiscore`. The **raw `Walls`/`Roofs`/ +`Property Type`/`Built Form` cells are NOT in it.** They live only in: +- the **classifier CSV** on S3 — `bulk_onboarding_inputs/{portfolioId}/{uploadId}-classifier.csv` (original landlord headers), and +- `landlord_*_overrides` in Postgres — the *resolved* values keyed by `(portfolio_id, normalized description)`. + +To write one `property_overrides` row, v2 must assemble **four inputs**: + +| Need | Source | +|---|---| +| `property_id` (identity) | combiner output → `(portfolio_id, uprn)` … **but no-UPRN rows have no key** | +| raw cell text per row | the classifier CSV (not the combiner output) | +| split a multi-valued cell → building parts | `multiEntryOrdering` on `bulk_address_uploads` | +| description → `override_value` | `landlord_*_overrides` (resolve by normalized description) | + +### Two open hazards to resolve first (do these before writing code) + +1. **Join key between the classifier CSV and the combiner output.** Both derive from + the same upload rows, but **row order is NOT preserved** through postcode-split + + combine. So you need a stable per-row key present in *both* files. `Internal + Reference` is the candidate — **verify it survives into both** the address CSV + (→ combiner output) and the classifier CSV. If it doesn't, this is the first thing + to fix. + +2. **`property_id` for unmatched (no-UPRN) rows.** v1's insert is `onConflictDoNothing` + and returns no ids. To attach overrides you need each row's `property.id`. For + UPRN rows you can re-select by `(portfolio_id, uprn)`; **no-UPRN rows can't be + re-found that way.** Likely fix: change the property insert to `RETURNING id` + mapped back to source rows (and decide the dedup/skip semantics for the RETURNING + path, since `onConflictDoNothing` returns nothing for conflicting rows). + +### Two candidate architectures (evaluate against real sample files) + +- **(A) Post-hoc join.** Keep the two files; the finaliser reads the combiner output + (UPRN/identity) and the classifier CSV (descriptions) and joins by `Internal + Reference`. Splits each multi-valued cell into parts via `multiEntryOrdering`, + resolves each part's description against `landlord_*_overrides`, and writes one row + per `(property, part, component)`. Lowest pipeline change; depends entirely on a + reliable join key. +- **(B) Carry descriptions through the pipeline.** Include the description columns in + the *address* CSV at `start-address-matching` so they flow through `address2uprn` + (which preserves input columns via `**row`) into the combiner output. Then the + finaliser reads **one** file with UPRN + descriptions in the same row — no join, no + key hazard. Costs a change to the address-CSV construction (frontend + `start-address-matching` route) and re-verifying `address2uprn`/combiner. Cleaner + long-term; bigger blast radius. **Recommended to seriously consider** — it deletes + hazard #1 entirely. + +## 6. `multiEntryOrdering` — how to split cells into parts + +Persisted on `bulk_address_uploads` (`src/app/db/schema/bulk_address_uploads.ts`): + +```ts +MultiEntryOrdering { permutations: Record; confirmed: boolean } +// permutations[count][k] = the 0-based FILE position holding building part k +// where 0 = Main building, 1..N-1 = Extension 1..N-1. +// e.g. { "2": [1, 0] } => for 2-entry rows, the main building is file position 1. +``` + +A multi-valued cell (e.g. `Walls = "Cavity: …, Solid brick: …"`) splits on commas +into entries by file position; `permutations[count]` maps file position → building +part. **Caveat (ADR-0004):** only the **largest count** permutation is captured this +iteration; other counts need a derivation rule — decide it in v2. +`multiEntrySummary` holds the detected multi-valued columns + **normalized** +description keys (the normalization that matches the classifier's stored keys: +`split → strip → lower`). + +## 7. Resolving description → value (`landlord_*_overrides`) + +Four per-component tables in `src/app/db/schema/landlord_overrides.ts` +(`landlord_wall_type_overrides`, `…_roof_type_…`, `…_property_type_…`, +`…_built_form_type_…`), each `UNIQUE (portfolio_id, description)`, value typed by the +component's pgEnum, plus a `source` (`classifier`|`user`). Resolve a normalized +description → `value`. The frontend already does this read in +`src/lib/bulkUpload/server.ts` (`lookupOverrides`) — mirror that mapping on the +backend. `UNKNOWN` is a legitimate stored value. + +## 8. Backend pieces to build (DDD, mirror v1) + +In `/workspaces/home/github/Model`: +- **`PropertyOverrideRow`** SQLModel mirror → `infrastructure/postgres/property_override_table.py` (mirror the pattern in `property_table.py` / `landlord_*_override_table.py`; reuse a shared `override_component` SAEnum like `landlord_override_enums.py`). +- **Repository** for the override write (one per aggregate): add to + `repositories/property/` (e.g. extend the property repo or a sibling + `property_override` repo), with an `upsert_all` using + `on_conflict_do_update(index_elements=[property_id, override_component, building_part], …)`. +- **Orchestrator logic** in `orchestration/bulk_upload_finaliser_orchestrator.py`: + extend `finalise(...)` (or add a step) to, after inserting properties and getting + ids, build the override rows (join → split by part → resolve) and persist them in + the **same** `commit_scope`. +- **Handler** stays thin — it already wires S3 + engine + repos. It will need the + extra input (classifier CSV and/or `multiEntryOrdering`); decide how those reach + the Lambda (extend `BulkUploadFinaliserTriggerBody`, or read `bulk_address_uploads` + for `multiEntryOrdering` + the classifier S3 URI). The trigger currently carries + `task_id, sub_task_id, s3_uri (combiner output), portfolio_id, bulk_upload_id`. + +Key v1 files to extend (all in the Model repo): +- `applications/bulk_upload_finaliser/handler.py` +- `orchestration/bulk_upload_finaliser_orchestrator.py` +- `repositories/property/property_repository.py` + `property_postgres_repository.py` +- `infrastructure/postgres/property_table.py` (reference for the new mirror) +- `infrastructure/s3/csv_s3_client.py` (`read_rows`) +- Packaging test: `tests/test_lambda_packaging.py` will flag any new top-level import + the Dockerfile doesn't `COPY` (v1 hit this with `datatypes/`). + +## 9. Open questions for v2 to decide + +- Join key confirmed (`Internal Reference` in both files) — or adopt architecture (B)? +- `property_id` for no-UPRN rows: `RETURNING id` strategy + dedup semantics. +- Non-largest-count `multiEntryOrdering` derivation rule (ADR-0004 deferred it). +- Does the trigger body grow, or does the handler read `bulk_address_uploads` + (`multiEntryOrdering`, classifier S3 URI) directly? +- Re-materialise semantics confirmed: recalculate overrides every finalise (snapshot + refreshes), `property` rows untouched. + +## 10. First steps in the new context + +1. Read §1 docs (esp. ADR-0004) + `CONTEXT.md`. +2. Get a **real sample**: the combiner output CSV and the `{uploadId}-classifier.csv` + for one dev upload, and inspect whether `Internal Reference` is in both → settle + hazard #1 / pick architecture (A) vs (B). +3. Decide the `property_id`-for-no-UPRN approach (hazard #2). +4. Build `PropertyOverrideRow` + repository + orchestrator step + handler wiring, + TDD against fakes (mirror `tests/orchestration/test_bulk_upload_finaliser_orchestrator.py`). +5. Update `CONTEXT.md` ("Property override" → populated) and add a v2 ADR if the + join/architecture choice is a real trade-off. + +## 11. Verification notes (environment) + +- Frontend: `npx tsc --noEmit` (was 0 errors at v1 close). +- Model repo: `mypy`/`pytest` need a deps-installed env (the v1 session couldn't run + them locally; `/app` Docker config runs the full suite). `terraform plan` needs the + CLI. Watch `tests/test_lambda_packaging.py` for Dockerfile COPY gaps. +- v1 is committed; dev Lambda + SQS queue are deployed and working + (`FINALISER_SQS_URL` wired in `backend/.env` for local, and in terraform/fast-api). From 8618a57310697768c73df379ce3ba6dba352ed7e Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Fri, 5 Jun 2026 12:19:06 +0000 Subject: [PATCH 12/13] property override --- .claude/settings.json | 9 +- CONTEXT.md | 8 +- .../bulk-upload-finaliser-v2-handover.md | 181 +++++++++++++----- .../[uploadId]/classifications/route.ts | 21 +- .../start-address-matching/route.ts | 37 +++- src/app/db/schema/bulk_address_uploads.ts | 19 +- .../[uploadId]/OnboardingProgress.tsx | 160 ++++++++++++++-- src/lib/bulkUpload/client.ts | 14 +- src/lib/bulkUpload/multiEntry.test.ts | 24 +++ src/lib/bulkUpload/multiEntry.ts | 60 ++++-- src/lib/bulkUpload/s3Keys.ts | 23 +++ src/lib/bulkUpload/server.ts | 132 +++++++++++-- 12 files changed, 564 insertions(+), 124 deletions(-) create mode 100644 src/lib/bulkUpload/s3Keys.ts diff --git a/.claude/settings.json b/.claude/settings.json index 0343c161..c425aebf 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -53,7 +53,10 @@ "Bash(grep -E '\\\\.sql$')", "Bash(cd /home/vscode/po-migration *)", "Read(//home/vscode/po-migration/**)", - "Bash(python -m py_compile applications/bulk_upload_finaliser/handler.py orchestration/bulk_upload_finaliser_orchestrator.py)" + "Bash(python -m py_compile applications/bulk_upload_finaliser/handler.py orchestration/bulk_upload_finaliser_orchestrator.py)", + "Bash(python -m py_compile repositories/property/property_repository.py repositories/property/property_postgres_repository.py orchestration/bulk_upload_finaliser_orchestrator.py applications/bulk_upload_finaliser/handler.py tests/orchestration/test_bulk_upload_finaliser_orchestrator.py)", + "Bash(python -m py_compile tests/orchestration/fakes.py)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" --max-time 30 http://localhost:3000/home)" ], "deny": [ "Bash(npx drizzle-kit generate)", @@ -73,7 +76,9 @@ "/workspaces/home/github/Model/deployment/terraform/lambda/bulkUploadFinaliser", "/workspaces/home/github/Model/deployment/terraform/lambda/fast-api", "/workspaces/home/github/Model/backend/app/db/functions", - "/workspaces/home/github/Model/repositories/bulk_upload" + "/workspaces/home/github/Model/repositories/bulk_upload", + "/workspaces/home/github/Model/tests/orchestration", + "/workspaces/home/github/Model/.github/workflows" ] } } diff --git a/CONTEXT.md b/CONTEXT.md index 4e9234ef..bfd17635 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -42,9 +42,13 @@ A landlord-supplied fact about a property that takes precedence over EPC-derived _Avoid_: customer data, manual override, landlord data **Property override**: -The per-Property fact layer — one resolved fact per `(Property, Building part, component)`, where component is one of `wall_type`/`roof_type`/`property_type`/`built_form_type`. Holds a **snapshot** of the resolved enum value (a denormalised copy of the VocabularyMapping outcome at finalise time, so two Properties sharing a description can later diverge), plus the original spreadsheet text it resolved from. Materialised by the finaliser; see [ADR-0005](./docs/adr/0005-async-bulk-upload-finaliser.md). (Table created; population is follow-up work.) +The per-Property fact layer — one resolved fact per `(Property, Building part, component)`, where component is one of `wall_type`/`roof_type`/`property_type`/`built_form_type`. Holds a **snapshot** of the resolved enum value (a denormalised copy of the VocabularyMapping outcome at finalise time, so two Properties sharing a description can later diverge), plus the original spreadsheet text it resolved from. Materialised by the finaliser **for UPRN-matched Properties only** (v2); the resolved value is never `UNKNOWN` — the Verify step forces every `UNKNOWN` to be mapped before Finalise, and an unresolved description fails the run. See [ADR-0005](./docs/adr/0005-async-bulk-upload-finaliser.md) (table) and [ADR-0006](./docs/adr/0006-property-overrides-join-and-no-uprn-defer.md) (population). _Avoid_: per-property mapping, property fact, override row +**Source row id**: +A synthetic UUID minted per source-file row at `start-address-matching` and written into **both** the address CSV and the classifier CSV. It is the stable join key that lets the finaliser tie a row's identity (combiner output → `property_id`) to that row's raw descriptions (classifier CSV), since neither file preserves row order and `Internal Reference` is absent from the classifier CSV. See [ADR-0006](./docs/adr/0006-property-overrides-join-and-no-uprn-defer.md). +_Avoid_: row index, internal reference (a separate, optional landlord field) + **VocabularyMapping**: The translation from a Landlord's free-text description in a BulkUpload column (e.g. `"cavity: filledcavity"`) to a canonical domain enum value (e.g. `WallType.CAVITY`). Produced by a `ColumnClassifier` (today an LLM, tomorrow possibly a lookup table or rules engine) in the Model service. Stored per-Portfolio, one row per `(category, description)`. A row carries provenance (`classifier` or `user`) so user overrides survive re-classification. _Avoid_: column mapping (that's a separate concept — see `ColumnMapping` above), classification, dictionary @@ -125,6 +129,8 @@ _Avoid_: override, adjustment, correction > > **Dev:** "And if **Finalise** runs and 30% of rows have no **UPRN**?" > **Domain expert:** "Those still get imported as **Properties** — just without a UPRN — and the BulkUpload moves to `complete`. Manual cleanup happens later in the property table." +> +> _(Planned change — v3 / [ADR-0006](./docs/adr/0006-property-overrides-join-and-no-uprn-defer.md): no-UPRN rows will move to a separate staging table to be re-matched, so `property` holds only matched rows. v2 does **not** change this yet — and v2 writes **Property overrides** only for the UPRN-matched rows.)_ ## Flagged ambiguities diff --git a/docs/design/bulk-upload-finaliser-v2-handover.md b/docs/design/bulk-upload-finaliser-v2-handover.md index 281d4ec4..0a0ef5b5 100644 --- a/docs/design/bulk-upload-finaliser-v2-handover.md +++ b/docs/design/bulk-upload-finaliser-v2-handover.md @@ -5,6 +5,85 @@ > (async finalise that writes `property`) is **shipped and working end-to-end**. > This doc assumes no memory of the v1 session. +## 0. Design resolved — grilling outcome (2026-06-05) + +> The open questions in §9 were resolved in a design session. **This section is now +> authoritative**; the later sections are kept for background but where they conflict +> with this one, this one wins. The new v2 ADR is +> [`docs/adr/0006-property-overrides-join-and-no-uprn-defer.md`](../adr/0006-property-overrides-join-and-no-uprn-defer.md); +> ADR-0004 was amended for per-count ordering capture. + +**Spine.** Populate `property_overrides` at finalise **for UPRN-matched rows only**. +Join the classifier descriptions to the combiner identity by a **synthetic UUID +`source_row_id`** — *not* `Internal Reference` (it is **absent from the classifier +CSV**, and optional anyway) and *not* by carrying description columns through +`address2uprn` (architecture B, rejected). This is architecture **(A)** with a +purpose-built key. + +**No-UPRN rows are deferred to v3.** v1 *currently* inserts them as `property` rows; +**v2 changes nothing in the property insert** and simply writes no overrides for +them. The eventual home for unmatched rows is a **separate staging table** (Model B): +`property` holds only matched rows; unmatched inputs (with their descriptions) live in +the staging table until a *different UPRN matcher* assigns a UPRN and promotes them. +"Found vs unfound" is a view across both tables, **not** a flag on `property`. v3 owns +the property-insert change + the staging table + the matcher-rerun UX together. + +**Frontend work** (`/workspaces/assessment-model`): +1. **Mint `source_row_id`** (UUID) in `start-address-matching` right after + `readRows()`, and **explicitly emit it as a column in both** `buildAddressCsv` and + `buildClassifierCsv` — both project a *fixed* column set, so attaching it to the + row object is not enough. It survives `address2uprn`→combiner like any input column + (carried as `additional_info`); **verify against a real combiner output**. +2. **Per-count ordering capture** (supersedes ADR-0004's largest-count-only): + `detectMultiEntry` keeps a sample **per distinct count**; `OnboardingProgress` + renders one ordering panel **per count ≥ 2**. The jsonb type and + `setMultiEntryOrdering` validation already accept all counts — **no migration, no + backend-validation change**. +3. **Verify gate hardened**: Finalise is blocked while **any** description is still + `UNKNOWN`. `UNKNOWN` is now a **transient "needs review" marker, never a final + value** (this retires the old "`UNKNOWN` is legitimate" line in §7). +4. **`dispatchFinaliser`** adds **two fields to the trigger body**: + `classifier_s3_uri` and `multi_entry_ordering` (it already reads the + `bulk_address_uploads` row, and dispatch happens *after* the user confirms + ordering, so the value is final). The classifier S3 key comes from a **shared + `classifierCsvKey(portfolioId, uploadId)` helper** used by both the writer and the + dispatcher (the key is not stored anywhere today — convention only). + +**Backend work** (`/workspaces/home/github/Model`): +5. Grow the trigger schema in two places — FastAPI `FinaliserTriggerRequest` and + Lambda `BulkUploadFinaliserTriggerBody` — with `classifier_s3_uri` + + `multi_entry_ordering`. Handler stays trigger-driven (no new `bulk_address_uploads` + coupling). +6. **`PropertyOverrideRow`** table mirror + a **sibling `PropertyOverrideRepository`** + (own aggregate; upsert on `(property_id, override_component, building_part)`), and a + **read-only `LandlordOverrideRepository`** that loads a portfolio's vocabulary + **per component into dicts once** (the vocabulary is deduplicated, not per-row). +7. **Orchestrator step**, in the same `commit_scope`: + - bulk `SELECT (portfolio_id, uprn) → id` for the run's UPRN rows → in-memory map; + - join classifier↔combiner rows by `source_row_id`; + - **uniform comma-split all four components** → `permutations[count]` → parts + (count-1 cell → `building_part = 0`); the finaliser needs **no fallback** because + every count ≥ 2 has a confirmed permutation; + - resolve each part's **normalized** description against the override dicts; + - `original_spreadsheet_description` = the **raw** entry text (un-normalized); + - **empty cell → write no row**; **non-empty but unresolved (or `UNKNOWN`) → raise** + → `commit_scope` rolls back → `_mark_failed` flips the upload to `failed` + (**fail loudly, no partial writes**); + - write only the classifier components actually **mapped** in `columnMapping`; + - **no `source` column in v2** — upsert is unconditional for now. + +**Locked assumptions (load-bearing — see ADR-0006).** +- **One real upload per user.** A re-upload only adds *new* properties (ones not + previously included), never re-describes existing ones → part-keys are append-only + across uploads → **upsert-only, no delete-orphans** is correct and complete. +- **Per-count consistency.** One ordering per count, confirmed from one sample, applies + to every cell of that count in the file (extends ADR-0004's bet to all counts). +- **Per-cell count.** `Walls` may split into 3 while `Roofs` splits into 2 in the same + row; each cell is ordered by *its own* entry count. +- **Classification completes before `awaiting_review`**, and the hardened verify gate + forces every `UNKNOWN` to be resolved — so an unresolved description at finalise is a + genuine defect, hence fail-loud. + ## 1. Where v1 left things (read first) v1 made **Finalise** an async dispatched Lambda that writes `property` rows. The @@ -87,38 +166,19 @@ To write one `property_overrides` row, v2 must assemble **four inputs**: | split a multi-valued cell → building parts | `multiEntryOrdering` on `bulk_address_uploads` | | description → `override_value` | `landlord_*_overrides` (resolve by normalized description) | -### Two open hazards to resolve first (do these before writing code) +### Two open hazards — both RESOLVED (see §0) -1. **Join key between the classifier CSV and the combiner output.** Both derive from - the same upload rows, but **row order is NOT preserved** through postcode-split + - combine. So you need a stable per-row key present in *both* files. `Internal - Reference` is the candidate — **verify it survives into both** the address CSV - (→ combiner output) and the classifier CSV. If it doesn't, this is the first thing - to fix. +1. **Join key (RESOLVED).** Investigation confirmed `Internal Reference` is in the + address CSV + combiner output but **NOT in the classifier CSV**, and is optional. + So architecture (A)-by-`Internal Reference` is dead. **Resolution: mint a synthetic + UUID `source_row_id`** in `start-address-matching` after `readRows()`, emitted as an + explicit column in *both* CSVs. It is the join key. (Architecture (A) with a + purpose-built key; (B) "carry descriptions through `address2uprn`" was rejected.) -2. **`property_id` for unmatched (no-UPRN) rows.** v1's insert is `onConflictDoNothing` - and returns no ids. To attach overrides you need each row's `property.id`. For - UPRN rows you can re-select by `(portfolio_id, uprn)`; **no-UPRN rows can't be - re-found that way.** Likely fix: change the property insert to `RETURNING id` - mapped back to source rows (and decide the dedup/skip semantics for the RETURNING - path, since `onConflictDoNothing` returns nothing for conflicting rows). - -### Two candidate architectures (evaluate against real sample files) - -- **(A) Post-hoc join.** Keep the two files; the finaliser reads the combiner output - (UPRN/identity) and the classifier CSV (descriptions) and joins by `Internal - Reference`. Splits each multi-valued cell into parts via `multiEntryOrdering`, - resolves each part's description against `landlord_*_overrides`, and writes one row - per `(property, part, component)`. Lowest pipeline change; depends entirely on a - reliable join key. -- **(B) Carry descriptions through the pipeline.** Include the description columns in - the *address* CSV at `start-address-matching` so they flow through `address2uprn` - (which preserves input columns via `**row`) into the combiner output. Then the - finaliser reads **one** file with UPRN + descriptions in the same row — no join, no - key hazard. Costs a change to the address-CSV construction (frontend - `start-address-matching` route) and re-verifying `address2uprn`/combiner. Cleaner - long-term; bigger blast radius. **Recommended to seriously consider** — it deletes - hazard #1 entirely. +2. **`property_id` for no-UPRN rows (RESOLVED by descoping).** v2 writes overrides + **only for UPRN rows**, whose `property.id` is re-found by `(portfolio_id, uprn)` + — so **no `RETURNING` correlation is needed**. No-UPRN rows are deferred to v3 + (Model B staging table); v2 leaves the property insert untouched. ## 6. `multiEntryOrdering` — how to split cells into parts @@ -147,7 +207,10 @@ Four per-component tables in `src/app/db/schema/landlord_overrides.ts` component's pgEnum, plus a `source` (`classifier`|`user`). Resolve a normalized description → `value`. The frontend already does this read in `src/lib/bulkUpload/server.ts` (`lookupOverrides`) — mirror that mapping on the -backend. `UNKNOWN` is a legitimate stored value. +backend. **`UNKNOWN` is now a transient "needs review" marker, never a final +value** (resolved in §0): the verify gate forces the user to map every `UNKNOWN` +before Finalise, so a `UNKNOWN` (or unresolvable description) reaching the finaliser +is a defect and **fails the run loudly**. ## 8. Backend pieces to build (DDD, mirror v1) @@ -176,27 +239,47 @@ Key v1 files to extend (all in the Model repo): - Packaging test: `tests/test_lambda_packaging.py` will flag any new top-level import the Dockerfile doesn't `COPY` (v1 hit this with `datatypes/`). -## 9. Open questions for v2 to decide +## 9. Open questions — all RESOLVED (see §0 + ADR-0006) -- Join key confirmed (`Internal Reference` in both files) — or adopt architecture (B)? -- `property_id` for no-UPRN rows: `RETURNING id` strategy + dedup semantics. -- Non-largest-count `multiEntryOrdering` derivation rule (ADR-0004 deferred it). -- Does the trigger body grow, or does the handler read `bulk_address_uploads` - (`multiEntryOrdering`, classifier S3 URI) directly? -- Re-materialise semantics confirmed: recalculate overrides every finalise (snapshot - refreshes), `property` rows untouched. +- **Join key** → synthetic UUID `source_row_id` in both CSVs (not `Internal + Reference`, not architecture B). +- **`property_id` for no-UPRN rows** → out of scope; v2 is UPRN-only, no-UPRN deferred + to v3 (Model B). UPRN rows re-found by `(portfolio_id, uprn)`; no `RETURNING`. +- **Non-largest-count `multiEntryOrdering`** → capture a confirmed permutation for + **every** count ≥ 2 in the UI (supersedes ADR-0004); finaliser needs no fallback. +- **Trigger body vs handler-reads-DB** → **grow the trigger body** (`classifier_s3_uri` + + `multi_entry_ordering`), built in `dispatchFinaliser`. +- **Re-materialise** → recalculate every finalise via **upsert-only** on + `(property_id, override_component, building_part)`; **no delete-orphans** (justified + by the one-real-upload assumption); `property` rows untouched. -## 10. First steps in the new context +## 10. Implementation order (design is settled — build it) -1. Read §1 docs (esp. ADR-0004) + `CONTEXT.md`. -2. Get a **real sample**: the combiner output CSV and the `{uploadId}-classifier.csv` - for one dev upload, and inspect whether `Internal Reference` is in both → settle - hazard #1 / pick architecture (A) vs (B). -3. Decide the `property_id`-for-no-UPRN approach (hazard #2). -4. Build `PropertyOverrideRow` + repository + orchestrator step + handler wiring, - TDD against fakes (mirror `tests/orchestration/test_bulk_upload_finaliser_orchestrator.py`). -5. Update `CONTEXT.md` ("Property override" → populated) and add a v2 ADR if the - join/architecture choice is a real trade-off. +Frontend first (the finaliser depends on `source_row_id` + per-count ordering): + +1. **`source_row_id`**: shared `classifierCsvKey` helper; mint the UUID in + `start-address-matching` after `readRows()`; emit it as an explicit column in both + `buildAddressCsv` and `buildClassifierCsv`. Verify it lands in a real combiner + output. +2. **Per-count ordering**: `detectMultiEntry` keeps a sample per count; + `OnboardingProgress` renders one ordering panel per count ≥ 2. Drop the + largest-count-only assumption in `setMultiEntryOrdering` if it requires the largest. +3. **Verify gate**: block Finalise while any classification is `UNKNOWN`. +4. **`dispatchFinaliser`**: add `classifier_s3_uri` + `multi_entry_ordering` to the + trigger payload. + +Backend: + +5. Grow `FinaliserTriggerRequest` (FastAPI) + `BulkUploadFinaliserTriggerBody` (Lambda). +6. `PropertyOverrideRow` mirror + sibling `PropertyOverrideRepository` (upsert) + + read-only `LandlordOverrideRepository`. +7. Orchestrator step (join → split → resolve → upsert; fail-loud on unresolved), + TDD against fakes (mirror + `tests/orchestration/test_bulk_upload_finaliser_orchestrator.py`). +8. Handler wiring; watch `tests/test_lambda_packaging.py` for Dockerfile COPY gaps. + +Docs (done in this session): ADR-0004 amended, ADR-0006 added, `CONTEXT.md` +"Property override" updated. ## 11. Verification notes (environment) diff --git a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/classifications/route.ts b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/classifications/route.ts index e897319a..5be47873 100644 --- a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/classifications/route.ts +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/classifications/route.ts @@ -1,11 +1,17 @@ -import { getSampleClassifications, setClassificationOverride } from "@/lib/bulkUpload/server"; +import { + getSampleClassifications, + getUnknownOverrides, + setClassificationOverride, +} from "@/lib/bulkUpload/server"; import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth"; import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { z } from "zod"; -// Read-only: the classifier's resolved enums for the multi-entry sample's -// entries, keyed by field -> description -> value (ADR-0004, issue #298). +// Read-only: the classifier's resolved enums for the review sample's entries +// (field -> description -> value), plus the descriptions still classified +// `Unknown` portfolio-wide — the Finalise gate blocks until that list is empty +// and the user can resolve each via PATCH below (ADR-0004 #298, ADR-0006). export async function GET( _request: NextRequest, { params }: { params: Promise<{ portfolioId: string; uploadId: string }> } @@ -13,9 +19,12 @@ export async function GET( const session = await getServerSession(AuthOptions); if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - const { uploadId } = await params; - const classifications = await getSampleClassifications(uploadId); - return NextResponse.json({ classifications }, { status: 200 }); + const { portfolioId, uploadId } = await params; + const [classifications, unknown] = await Promise.all([ + getSampleClassifications(uploadId), + getUnknownOverrides(portfolioId), + ]); + return NextResponse.json({ classifications, unknown }, { status: 200 }); } const PatchSchema = z.object({ diff --git a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/start-address-matching/route.ts b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/start-address-matching/route.ts index 22bfa8b9..e7e6ae50 100644 --- a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/start-address-matching/route.ts +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/start-address-matching/route.ts @@ -1,11 +1,13 @@ import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth"; +import { randomUUID } from "node:crypto"; import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { createS3Client, createRetrofitDataS3Client, retrofitDataS3Bucket } from "@/app/utils/s3"; import * as XLSX from "xlsx"; import { loadForAddressMatching, saveMultiEntrySummary, triggerAddressMatching, triggerClassifier } from "@/lib/bulkUpload/server"; import { readSessionToken } from "@/lib/session"; import { ADDRESS_FIELDS, classifierMapping } from "@/lib/bulkUpload/columnFields"; +import { addressCsvKey, classifierCsvKey, SOURCE_ROW_ID_COLUMN } from "@/lib/bulkUpload/s3Keys"; import { detectMultiEntry } from "@/lib/bulkUpload/multiEntry"; type SheetRow = Record; @@ -35,11 +37,17 @@ function buildAddressCsv( if (!outputHeaders.includes("postcode")) return { error: 'Mapping must include "postcode"' }; + // Carry the synthetic per-row join key through to the combiner output, so the + // finaliser can re-associate a UPRN-matched row with its classifier + // descriptions (ADR-0006). It rides `address2uprn` as a preserved input column. + outputHeaders.push(SOURCE_ROW_ID_COLUMN); + const outputRows = rows.map((row) => { const out: SheetRow = {}; for (const [outName, src] of Object.entries(outputToSource)) { out[outName] = row[src] ?? ""; } + out[SOURCE_ROW_ID_COLUMN] = row[SOURCE_ROW_ID_COLUMN] ?? ""; return out; }); @@ -56,10 +64,17 @@ function buildClassifierCsv( rows: SheetRow[], classifierMap: Record // category → source header ): string { - const headers = [...new Set(Object.values(classifierMap))]; + const sourceHeaders = [...new Set(Object.values(classifierMap))]; + // Emit the synthetic join key alongside the classifier columns so the + // finaliser can join this row's descriptions to its combiner identity by + // `source_row_id` (ADR-0006). `buildClassifierCsv` projects a fixed column + // set, so the key must be added explicitly — attaching it to the row is not + // enough. + const headers = [...sourceHeaders, SOURCE_ROW_ID_COLUMN]; const outputRows = rows.map((row) => { const out: SheetRow = {}; - for (const h of headers) out[h] = row[h] ?? ""; + for (const h of sourceHeaders) out[h] = row[h] ?? ""; + out[SOURCE_ROW_ID_COLUMN] = row[SOURCE_ROW_ID_COLUMN] ?? ""; return out; }); const outSheet = XLSX.utils.json_to_sheet(outputRows, { header: headers }); @@ -104,10 +119,20 @@ export async function POST( return NextResponse.json({ error: "Failed to read source file" }, { status: 500 }); } - const rows = readRows(fileBuffer); - if (rows.length === 0) + const parsedRows = readRows(fileBuffer); + if (parsedRows.length === 0) return NextResponse.json({ error: "Empty file" }, { status: 422 }); + // Mint a stable synthetic id per source row, here at the one point both CSVs + // are built from the same array, and write it into both. It is the finaliser's + // join key between the combiner output (identity) and the classifier CSV + // (descriptions) — see ADR-0006. Deterministic ordering is not required: both + // CSVs are produced together in this handler, so they always share values. + const rows = parsedRows.map((row) => ({ + ...row, + [SOURCE_ROW_ID_COLUMN]: randomUUID(), + })); + // Detect multi-entry building parts now, while the whole file is parsed in // memory, so the awaiting_review surface never re-reads it (ADR-0004). await saveMultiEntrySummary(uploadId, detectMultiEntry(rows, upload.columnMapping!)); @@ -116,7 +141,7 @@ export async function POST( if (transformed.error) return NextResponse.json({ error: transformed.error }, { status: 422 }); - const transformedKey = `bulk_onboarding_inputs/${portfolioId}/${uploadId}.csv`; + const transformedKey = addressCsvKey(portfolioId, uploadId); try { await outputS3 .putObject({ @@ -139,7 +164,7 @@ export async function POST( const classifierMap = classifierMapping(upload.columnMapping!); let classifierS3Uri: string | undefined; if (Object.keys(classifierMap).length > 0) { - const classifierKey = `bulk_onboarding_inputs/${portfolioId}/${uploadId}-classifier.csv`; + const classifierKey = classifierCsvKey(portfolioId, uploadId); try { await outputS3 .putObject({ diff --git a/src/app/db/schema/bulk_address_uploads.ts b/src/app/db/schema/bulk_address_uploads.ts index 3e3c1dc9..2a99a4d6 100644 --- a/src/app/db/schema/bulk_address_uploads.ts +++ b/src/app/db/schema/bulk_address_uploads.ts @@ -22,17 +22,26 @@ export interface MultiEntrySummary { multiValuedFields: string[]; countDistribution: Record; largestCount: number; + // Step 1 (verify) sample: the largest-count row when multi-entry, else the + // first classified row. `null` ⇒ nothing to verify. sample: MultiEntrySample | null; + // Step 2 (order): one sample per distinct entry-count ≥ 2 present in the file, + // keyed by count. Each count needs its OWN confirmed permutation — a smaller + // count's ordering can't be derived from a larger one (ADR-0004, amended + // 2026-06-05). Absent on uploads detected before that amendment. + samplesByCount?: Record; } -// User-confirmed building-part ordering (ADR-0004). Keyed by entry-count so it -// can hold more than one count later; this iteration populates only the -// largest. permutations[count][k] = the 0-based file position holding building -// part k, where 0 = Main building, 1..N-1 = Extension 1..N-1. +// User-confirmed building-part ordering (ADR-0004, amended 2026-06-05). Keyed by +// entry-count: a permutation is captured for EVERY distinct count ≥ 2 in the +// file (the v2 fact layer can't derive one count's order from another). +// permutations[count][k] = the 0-based file position holding building part k, +// where 0 = Main building, 1..N-1 = Extension 1..N-1. // e.g. { "2": [1, 0] } => for 2-part rows the main building is file position 1. export interface MultiEntryOrdering { permutations: Record; - // Set once the user confirms; gates Finalise when the upload is multi-entry. + // True once EVERY detected count ≥ 2 has a permutation; gates Finalise when the + // upload is multi-entry. confirmed: boolean; } diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx index 041ca389..f5dc7cd0 100644 --- a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx @@ -134,11 +134,29 @@ export default function OnboardingProgress({ const orderingConfirmed = upload.multiEntryOrdering?.confirmed ?? false; const needsVerify = !!sample; const needsOrdering = !!sample && isMultiEntry; + // One ordering panel per distinct count ≥ 2, ascending (ADR-0004 amendment). + // Fall back to the single Step-1 sample for uploads detected before per-count + // capture existed (samplesByCount absent). + const samplesByCount = upload.multiEntrySummary?.samplesByCount; + const orderingSamples: Array<[string, MultiEntrySample]> = + samplesByCount && Object.keys(samplesByCount).length > 0 + ? Object.entries(samplesByCount).sort(([a], [b]) => Number(a) - Number(b)) + : sample && isMultiEntry + ? [[String(sample.count), sample]] + : []; const showStepNumbers = needsVerify && needsOrdering; + // Descriptions still classified `Unknown` block Finalise — the user must map + // every one to a real value, else the finaliser fails loudly (ADR-0006). + const unknownByField = classifications.data?.unknown ?? {}; + const unknownTotal = Object.values(unknownByField).reduce( + (n, descriptions) => n + descriptions.length, + 0, + ); const canFinalize = isAwaitingReview && (!needsVerify || verifyAck) && - (!needsOrdering || orderingConfirmed); + (!needsOrdering || orderingConfirmed) && + unknownTotal === 0; return (
@@ -209,7 +227,7 @@ export default function OnboardingProgress({ {needsVerify && sample && ( )} - {needsOrdering && sample && ( - 0 && ( + )} + {needsOrdering && orderingSamples.length > 0 && ( +
+ {orderingSamples.map(([count, orderSample], i) => ( + 1 + ? `Part group ${i + 1}` + : undefined + } + portfolioId={portfolioId} + uploadId={uploadId} + /> + ))} +
+ )} + {(canRunCombiner || isAwaitingReview) && (
{canRunCombiner && ( @@ -245,9 +284,11 @@ export default function OnboardingProgress({ isPending={finalize.isPending} disabled={!canFinalize} disabledReason={ - needsVerify && !verifyAck - ? "Verify the classification first" - : "Confirm the building-part order first" + unknownTotal > 0 + ? `Resolve ${unknownTotal} unclassified description${unknownTotal === 1 ? "" : "s"} first` + : needsVerify && !verifyAck + ? "Verify the classification first" + : "Confirm the building-part order first" } onClick={() => finalize.mutate(undefined, { onSuccess: () => router.refresh() }) @@ -405,10 +446,12 @@ function VerifyClassificationPanel({ ); } -// Interactive building-part ordering for the largest-count multi-entry sample -// (ADR-0004). The user labels each file position with a building part (one Main -// building + Extensions); the labels must form a permutation. Confirming -// persists the ordering and unlocks Finalise. +// Interactive building-part ordering for ONE entry-count's sample (ADR-0004, +// amended 2026-06-05 — one panel per distinct count). The user labels each file +// position with a building part (one Main building + Extensions); the labels +// must form a permutation. Confirming persists this count's ordering (merged +// server-side with the other counts'); Finalise unlocks once every count is +// confirmed. function MultiEntryOrderingPanel({ sample, ordering, @@ -444,7 +487,10 @@ function MultiEntryOrderingPanel({ return Array.from({ length: count }, (_, i) => i); }); - const confirmed = ordering?.confirmed ?? false; + // Per-panel confirmation reflects whether THIS count's permutation is stored, + // not the global all-counts-confirmed flag — so each panel gives its own + // feedback as the user works through them. + const confirmed = Array.isArray(ordering?.permutations?.[String(count)]); const valid = isPermutation(assignment); const setSlot = (position: number, slot: number) => @@ -558,6 +604,88 @@ function MultiEntryOrderingPanel({ ); } +// Unresolved-classification gate (ADR-0006). Lists every description still +// classified `Unknown` portfolio-wide and lets the user map each to a real value +// via the same per-description override path as Step 1 (it applies portfolio- +// wide). Finalise stays blocked until this list is empty — `Unknown` is never a +// final value, and an unresolved one would fail the import loudly. +function UnresolvedClassificationsPanel({ + unknown, + portfolioId, + uploadId, +}: { + unknown: Record; + portfolioId: string; + uploadId: string; +}) { + const editClassification = useEditClassification(portfolioId, uploadId); + const total = Object.values(unknown).reduce((n, d) => n + d.length, 0); + + return ( +
+

+ Resolve unclassified descriptions ({total}) +

+

+ We couldn't classify these automatically. Map each to a category + before finalising — an unresolved value would fail the import. Edits apply + to every row across the portfolio. +

+ +
+ {Object.entries(unknown).map(([field, descriptions]) => { + const options = (CATEGORY_VALUES[field] ?? []).filter((o) => o !== "Unknown"); + return ( +
+

+ {FIELD_LABEL[field] ?? field} +

+
+ {descriptions.map((description) => ( +
+ + {description} + + + +
+ ))} +
+
+ ); + })} +
+ {editClassification.error && ( +

{editClassification.error.message}

+ )} +
+ ); +} + function StageButton({ label, activeLabel, diff --git a/src/lib/bulkUpload/client.ts b/src/lib/bulkUpload/client.ts index 2a50023a..3f3a34e9 100644 --- a/src/lib/bulkUpload/client.ts +++ b/src/lib/bulkUpload/client.ts @@ -121,12 +121,19 @@ export function useEditClassification(portfolioId: string, uploadId: string) { }); } +// Sample classifications for the review panels PLUS the still-`Unknown` +// descriptions that gate Finalise (ADR-0006). +export interface ClassificationsView { + classifications: SampleClassifications; + unknown: Record; +} + export function useSampleClassifications( portfolioId: string, uploadId: string, enabled: boolean, ) { - return useQuery({ + return useQuery({ queryKey: [...bulkUploadKeys.progress(uploadId), "classifications"], enabled, queryFn: async () => { @@ -135,7 +142,10 @@ export function useSampleClassifications( ); if (!res.ok) throw await parseError(res, "Failed to load classifications."); const body = await res.json(); - return body.classifications as SampleClassifications; + return { + classifications: (body.classifications ?? {}) as SampleClassifications, + unknown: (body.unknown ?? {}) as Record, + }; }, }); } diff --git a/src/lib/bulkUpload/multiEntry.test.ts b/src/lib/bulkUpload/multiEntry.test.ts index e7ff6a9b..da0eb253 100644 --- a/src/lib/bulkUpload/multiEntry.test.ts +++ b/src/lib/bulkUpload/multiEntry.test.ts @@ -60,6 +60,30 @@ describe("detectMultiEntry", () => { expect(wallCol?.entries.map((e) => e.raw)).toEqual(["Cavity: AsBuilt", "Cavity: Filled"]); }); + it("captures one ordering sample per distinct count (ADR-0004 amendment)", () => { + const rows = [ + { Addr: "1 High St", PC: "AB1 2CD", "Property Type": "House: Detached", Walls: "Cavity: AsBuilt", Roofs: "Pitched: 200mm" }, // count 1 + { Addr: "2 Low St", PC: "AB3 4EF", "Property Type": "House: Semi", Walls: "Cavity, Solid", Roofs: "Flat, Pitched" }, // count 2 + { Addr: "3 Mid Rd", PC: "AB5 6GH", "Property Type": "House: Mid", Walls: "Cavity, Solid, Render", Roofs: "Flat, Pitched, Slate" }, // count 3 + { Addr: "4 Side Ln", PC: "AB7 8IJ", "Property Type": "House: Other", Walls: "Brick, Stone", Roofs: "Tile, Slate" }, // count 2 again + ]; + const summary = detectMultiEntry(rows, MAPPING); + + expect(summary.largestCount).toBe(3); + expect(summary.countDistribution).toEqual({ "2": 2, "3": 1 }); + + // A sample for every count >= 2 — and only those. + expect(Object.keys(summary.samplesByCount ?? {}).sort()).toEqual(["2", "3"]); + expect(summary.samplesByCount!["2"].count).toBe(2); + expect(summary.samplesByCount!["3"].count).toBe(3); + // The count-2 sample is the FIRST count-2 row, not the count-3 one. + expect(summary.samplesByCount!["2"].address).toBe("2 Low St, AB3 4EF"); + const wall2 = summary.samplesByCount!["2"].columns.find((c) => c.field === "wall_type"); + expect(wall2?.entries.map((e) => e.raw)).toEqual(["Cavity", "Solid"]); + const wall3 = summary.samplesByCount!["3"].columns.find((c) => c.field === "wall_type"); + expect(wall3?.entries.map((e) => e.raw)).toEqual(["Cavity", "Solid", "Render"]); + }); + it("normalizes descriptions to lower-case (matching the classifier's key)", () => { const rows = [{ Addr: "1 High St", PC: "AB1 2CD", "Property Type": "House: EndTerrace", Walls: "", Roofs: "" }]; const summary = detectMultiEntry(rows, MAPPING); diff --git a/src/lib/bulkUpload/multiEntry.ts b/src/lib/bulkUpload/multiEntry.ts index 3bbd053e..d537360a 100644 --- a/src/lib/bulkUpload/multiEntry.ts +++ b/src/lib/bulkUpload/multiEntry.ts @@ -13,6 +13,7 @@ import { ADDRESS_FIELDS, classifierMapping } from "./columnFields"; import type { MultiEntryEntry, MultiEntryColumn, + MultiEntrySample, MultiEntrySummary, } from "@/app/db/schema/bulk_address_uploads"; @@ -61,6 +62,7 @@ export const EMPTY_MULTI_ENTRY_SUMMARY: MultiEntrySummary = { countDistribution: {}, largestCount: 0, sample: null, + samplesByCount: {}, }; // Split a cell into building-part entries. Mirrors the classifier's @@ -115,6 +117,9 @@ export function detectMultiEntry( // Fallback sample for Step 1 when no row is multi-entry: the first row that // carries any classifier value. let firstClassifiedRowIndex = -1; + // First row index seen at each distinct count ≥ 2 — one ordering sample per + // count (ADR-0004 amendment): each count needs its own confirmed permutation. + const sampleRowIndexByCount: Record = {}; rows.forEach((row, index) => { let rowMax = 0; @@ -129,7 +134,8 @@ export function detectMultiEntry( if (rowMax >= 2) { const key = String(rowMax); countDistribution[key] = (countDistribution[key] ?? 0) + 1; - // First row at a new maximum becomes the multi-entry sample. + if (sampleRowIndexByCount[key] === undefined) sampleRowIndexByCount[key] = index; + // First row at a new maximum becomes the multi-entry (Step 1) sample. if (rowMax > largestCount) { largestCount = rowMax; multiEntryRowIndex = index; @@ -140,29 +146,47 @@ export function detectMultiEntry( const sampleRowIndex = multiEntryRowIndex !== -1 ? multiEntryRowIndex : firstClassifiedRowIndex; if (sampleRowIndex === -1) { - return { multiValuedFields: [...multiValued], countDistribution, largestCount, sample: null }; + return { + multiValuedFields: [...multiValued], + countDistribution, + largestCount, + sample: null, + samplesByCount: {}, + }; } - const sampleRow = rows[sampleRowIndex]; - // Every mapped classifier column with a value in the sample row. Step 1 lists - // them all; Step 2's ordering table filters to the multi-valued ones - // (single-value columns are whole-dwelling facts, not building parts). - const columns: MultiEntryColumn[] = classifierCols - .map(([field, header]) => ({ - field, - header, - entries: splitEntries(sampleRow[header]), - })) - .filter((column) => column.entries.length > 0); + // One ordering sample per distinct count, so the UI can render a panel per + // count and the user confirms each independently. + const samplesByCount: Record = {}; + for (const [count, rowIndex] of Object.entries(sampleRowIndexByCount)) { + samplesByCount[count] = sampleFromRow(rows[rowIndex], columnMapping, classifierCols, Number(count)); + } return { multiValuedFields: [...multiValued], countDistribution, largestCount, - sample: { - address: buildAddress(sampleRow, columnMapping), - count: largestCount >= 2 ? largestCount : 1, - columns, - }, + sample: sampleFromRow( + rows[sampleRowIndex], + columnMapping, + classifierCols, + largestCount >= 2 ? largestCount : 1, + ), + samplesByCount, }; } + +// Build the sample for one row: its display address plus every mapped classifier +// column carrying a value. Step 1 lists all columns; Step 2's order table filters +// to the multi-valued ones (single-value columns are whole-dwelling facts). +function sampleFromRow( + row: Record, + columnMapping: Record, + classifierCols: Array<[string, string]>, + count: number, +): MultiEntrySample { + const columns: MultiEntryColumn[] = classifierCols + .map(([field, header]) => ({ field, header, entries: splitEntries(row[header]) })) + .filter((column) => column.entries.length > 0); + return { address: buildAddress(row, columnMapping), count, columns }; +} diff --git a/src/lib/bulkUpload/s3Keys.ts b/src/lib/bulkUpload/s3Keys.ts new file mode 100644 index 00000000..8188fcf4 --- /dev/null +++ b/src/lib/bulkUpload/s3Keys.ts @@ -0,0 +1,23 @@ +// Shared S3 key conventions + the synthetic join-column name for bulk-upload +// artifacts. The finaliser join (ADR-0006) depends on the classifier CSV key +// being built *identically* in two places — where the CSV is written +// (start-address-matching) and where the finaliser is dispatched +// (dispatchFinaliser) — and on the `source_row_id` column appearing in both the +// address CSV and the classifier CSV. Keeping the convention here is the single +// source of truth that stops those two callers drifting. + +export const BULK_UPLOAD_INPUT_PREFIX = "bulk_onboarding_inputs"; + +export function addressCsvKey(portfolioId: string, uploadId: string): string { + return `${BULK_UPLOAD_INPUT_PREFIX}/${portfolioId}/${uploadId}.csv`; +} + +export function classifierCsvKey(portfolioId: string, uploadId: string): string { + return `${BULK_UPLOAD_INPUT_PREFIX}/${portfolioId}/${uploadId}-classifier.csv`; +} + +// The synthetic per-row UUID column. Minted at start-address-matching and +// emitted into both CSVs so the finaliser can join a row's identity (combiner +// output) to its raw descriptions (classifier CSV). The Model finaliser reads +// this exact header — keep the two in sync. +export const SOURCE_ROW_ID_COLUMN = "source_row_id"; diff --git a/src/lib/bulkUpload/server.ts b/src/lib/bulkUpload/server.ts index 2de69fff..95df4df5 100644 --- a/src/lib/bulkUpload/server.ts +++ b/src/lib/bulkUpload/server.ts @@ -15,6 +15,8 @@ import { subTasks } from "@/app/db/schema/tasks/subtask"; import { and, count, desc, eq, inArray, sql } from "drizzle-orm"; import type { BulkUpload, BulkUploadStatus, ProgressView, TaskSummary } from "./types"; import { validateColumnMapping, classifierMapping } from "./columnFields"; +import { classifierCsvKey } from "./s3Keys"; +import { retrofitDataS3Bucket } from "@/app/utils/s3"; import { SUBTASK_SERVICE } from "./types"; import type { MultiEntrySummary } from "./multiEntry"; import { isPermutation } from "./multiEntry"; @@ -166,24 +168,91 @@ async function lookupOverrides( } } -// The classifier's enums for the multi-entry sample's entries, joined by the +// The classifier's enums for the review samples' entries, joined by the // normalized description (exact match — the summary stored it the way the -// classifier persists it, so no re-normalization here). Read-only. +// classifier persists it, so no re-normalization here). Read-only. Covers the +// Step 1 verify sample AND every per-count ordering sample, since smaller-count +// panels may show descriptions the largest-count sample doesn't (ADR-0004 +// amendment). export async function getSampleClassifications( uploadId: string, ): Promise { const upload = await loadById(uploadId); - const sample = upload?.multiEntrySummary?.sample; - if (!upload || !sample) return {}; + const summary = upload?.multiEntrySummary; + if (!upload || !summary || !summary.sample) return {}; + + // Gather distinct descriptions per field across all samples. + const allSamples = [summary.sample, ...Object.values(summary.samplesByCount ?? {})]; + const descriptionsByField: Record> = {}; + for (const sample of allSamples) { + for (const column of sample.columns) { + const set = (descriptionsByField[column.field] ??= new Set()); + for (const e of column.entries) set.add(e.description); + } + } const portfolioId = BigInt(upload.portfolioId); const result: SampleClassifications = {}; - for (const column of sample.columns) { - const descriptions = [...new Set(column.entries.map((e) => e.description))]; + for (const [field, descSet] of Object.entries(descriptionsByField)) { + const descriptions = [...descSet]; if (descriptions.length === 0) continue; - const rows = await lookupOverrides(column.field, portfolioId, descriptions); + const rows = await lookupOverrides(field, portfolioId, descriptions); if (!rows) continue; - result[column.field] = Object.fromEntries(rows.map((r) => [r.description, r.value])); + result[field] = Object.fromEntries(rows.map((r) => [r.description, r.value])); + } + return result; +} + +// Descriptions still classified `Unknown` per field, portfolio-wide (ADR-0006). +// `Unknown` is the classifier's "couldn't decide" marker; v2 treats it as +// never-final, so the Finalise gate blocks until the user maps every one to a +// real value (and the finaliser fails loudly if any slips through). Portfolio- +// wide is the right scope under the one-real-upload assumption (ADR-0006). +export type UnknownOverrides = Record; + +const UNKNOWN_VALUE = "Unknown"; + +async function unknownForField(field: string, portfolioId: bigint): Promise { + switch (field) { + case "property_type": + return ( + await db + .select({ description: landlordPropertyTypeOverrides.description }) + .from(landlordPropertyTypeOverrides) + .where(and(eq(landlordPropertyTypeOverrides.portfolioId, portfolioId), eq(landlordPropertyTypeOverrides.value, UNKNOWN_VALUE))) + ).map((r) => r.description); + case "built_form_type": + return ( + await db + .select({ description: landlordBuiltFormTypeOverrides.description }) + .from(landlordBuiltFormTypeOverrides) + .where(and(eq(landlordBuiltFormTypeOverrides.portfolioId, portfolioId), eq(landlordBuiltFormTypeOverrides.value, UNKNOWN_VALUE))) + ).map((r) => r.description); + case "wall_type": + return ( + await db + .select({ description: landlordWallTypeOverrides.description }) + .from(landlordWallTypeOverrides) + .where(and(eq(landlordWallTypeOverrides.portfolioId, portfolioId), eq(landlordWallTypeOverrides.value, UNKNOWN_VALUE))) + ).map((r) => r.description); + case "roof_type": + return ( + await db + .select({ description: landlordRoofTypeOverrides.description }) + .from(landlordRoofTypeOverrides) + .where(and(eq(landlordRoofTypeOverrides.portfolioId, portfolioId), eq(landlordRoofTypeOverrides.value, UNKNOWN_VALUE))) + ).map((r) => r.description); + default: + return []; + } +} + +export async function getUnknownOverrides(portfolioId: string): Promise { + const pid = BigInt(portfolioId); + const result: UnknownOverrides = {}; + for (const field of ["property_type", "built_form_type", "wall_type", "roof_type"]) { + const descriptions = await unknownForField(field, pid); + if (descriptions.length > 0) result[field] = descriptions; } return result; } @@ -276,10 +345,12 @@ export type SetOrderingOutcome = | { kind: "not_multi_entry" } | { kind: "invalid_ordering"; reason: string }; -// Persist the user-confirmed building-part ordering (ADR-0004). Allowed only at -// awaiting_review and only when the upload is multi-entry. Validates that the -// largest count is provided and every supplied permutation is a bijection of -// its positions, then marks it confirmed (which gates Finalise). +// Persist the user-confirmed building-part ordering (ADR-0004, amended +// 2026-06-05). Allowed only at awaiting_review and only when the upload is +// multi-entry. Each distinct count ≥ 2 needs its own permutation; the UI confirms +// one count at a time, so we MERGE the supplied permutations into any already +// stored, validate each is a bijection, and only mark `confirmed` once EVERY +// detected count has a permutation (which gates Finalise). export async function setMultiEntryOrdering( uploadId: string, permutations: Record, @@ -292,22 +363,25 @@ export async function setMultiEntryOrdering( const summary = upload.multiEntrySummary; // A sample now exists for non-multi-entry uploads too (Step 1's verify // sample), so "is multi-entry" is largestCount >= 2, not "has a sample". - if (!summary || summary.largestCount < 2 || !summary.sample) + if (!summary || summary.largestCount < 2) return { kind: "not_multi_entry" }; - const sample = summary.sample; - - const largest = String(sample.count); - if (!permutations[largest]) - return { kind: "invalid_ordering", reason: `Missing ordering for ${sample.count} parts.` }; for (const [count, permutation] of Object.entries(permutations)) { if (permutation.length !== Number(count) || !isPermutation(permutation)) return { kind: "invalid_ordering", reason: `Ordering for ${count} parts is not a valid arrangement.` }; } + // Merge with any counts confirmed earlier, then decide whether every detected + // count (the keys of countDistribution, all ≥ 2) now has a permutation. + const merged = { ...(upload.multiEntryOrdering?.permutations ?? {}), ...permutations }; + const requiredCounts = Object.keys(summary.countDistribution); + const confirmed = requiredCounts.every( + (c) => Array.isArray(merged[c]) && merged[c].length === Number(c), + ); + const [updated] = await db .update(bulkAddressUploads) - .set({ multiEntryOrdering: { permutations, confirmed: true } }) + .set({ multiEntryOrdering: { permutations: merged, confirmed } }) .where(eq(bulkAddressUploads.id, uploadId)) .returning(); if (!updated) return { kind: "not_found" }; @@ -625,12 +699,32 @@ export async function dispatchFinaliser(args: { }) .returning(); + // v2 (ADR-0006): the finaliser also writes property_overrides for UPRN-matched + // rows, which needs the classifier CSV (raw descriptions, joined to the + // combiner output by `source_row_id`) and the confirmed building-part ordering. + // Both are derivable here — we already hold the upload row, and dispatch runs + // after the user confirms ordering, so the value is final. + // - classifier_s3_uri: null when no classifier columns were mapped (no + // classifier CSV was written; the finaliser then writes no overrides). + // - multi_entry_ordering: permutations keyed by entry-count; {} when the + // upload is not multi-entry (every cell is a single building part → part 0). + const classifierMap = classifierMapping(upload.columnMapping ?? {}); + const classifierS3Uri = + Object.keys(classifierMap).length > 0 + ? `s3://${retrofitDataS3Bucket()}/${classifierCsvKey(upload.portfolioId, args.uploadId)}` + : null; + const payload = { task_id: upload.taskId, sub_task_id: subTask.id, s3_uri: upload.combinedOutputS3Uri, portfolio_id: Number(upload.portfolioId), bulk_upload_id: args.uploadId, + classifier_s3_uri: classifierS3Uri, + multi_entry_ordering: upload.multiEntryOrdering?.permutations ?? {}, + // classifier category → source CSV header, so the finaliser knows which + // classifier-CSV column feeds each override_component (ADR-0006). + column_mapping: classifierMap, }; const trigger = await triggerFastApiPipeline({ From 3f277f13c25f75b82bbcbe4b9b6056de497a354b Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Fri, 5 Jun 2026 19:02:35 +0000 Subject: [PATCH 13/13] route of the address2uprn problem of float value --- .claude/settings.json | 3 +- .../bulk-uploads/[uploadId]/combine/route.ts | 4 +++ src/lib/bulkUpload/server.ts | 36 ++++++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index c425aebf..e1550d21 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -56,7 +56,8 @@ "Bash(python -m py_compile applications/bulk_upload_finaliser/handler.py orchestration/bulk_upload_finaliser_orchestrator.py)", "Bash(python -m py_compile repositories/property/property_repository.py repositories/property/property_postgres_repository.py orchestration/bulk_upload_finaliser_orchestrator.py applications/bulk_upload_finaliser/handler.py tests/orchestration/test_bulk_upload_finaliser_orchestrator.py)", "Bash(python -m py_compile tests/orchestration/fakes.py)", - "Bash(curl -s -o /dev/null -w \"%{http_code}\" --max-time 30 http://localhost:3000/home)" + "Bash(curl -s -o /dev/null -w \"%{http_code}\" --max-time 30 http://localhost:3000/home)", + "Bash(python -m py_compile orchestration/bulk_upload_finaliser_orchestrator.py tests/orchestration/test_bulk_upload_finaliser_orchestrator.py)" ], "deny": [ "Bash(npx drizzle-kit generate)", diff --git a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/combine/route.ts b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/combine/route.ts index afee7f0e..bc2f8438 100644 --- a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/combine/route.ts +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/combine/route.ts @@ -25,6 +25,10 @@ export async function POST( ); case "already_combined": return NextResponse.json({ alreadyCombined: true }, { status: 200 }); + case "already_dispatched": + // Lost the double-dispatch CAS (or the combiner is already running) — a + // benign no-op; the client just keeps polling and sees `combining`. + return NextResponse.json({ alreadyDispatched: true }, { status: 200 }); case "not_found": return NextResponse.json({ error: "Not found" }, { status: 404 }); case "missing_task": diff --git a/src/lib/bulkUpload/server.ts b/src/lib/bulkUpload/server.ts index 95df4df5..ca303f9b 100644 --- a/src/lib/bulkUpload/server.ts +++ b/src/lib/bulkUpload/server.ts @@ -583,6 +583,7 @@ export async function triggerClassifier(args: { export type CombineRetriggerOutcome = | { kind: "triggered"; taskId: string; subTaskId: string } | { kind: "already_combined" } + | { kind: "already_dispatched" } | { kind: "not_found" } | { kind: "missing_task" } | { kind: "trigger_failed"; status: number; message: string }; @@ -596,6 +597,24 @@ export async function requestCombineRetrigger(args: { if (!upload.taskId) return { kind: "missing_task" }; if (upload.combinedOutputS3Uri) return { kind: "already_combined" }; + // CAS: atomically claim `processing → combining` — the double-dispatch guard + // (mirrors dispatchFinaliser's awaiting_review → finalising, ADR-0005). Of two + // rapid "Run Combiner" clicks exactly one flips the row; the loser updates 0 + // rows and bails, so only one combiner subtask is ever dispatched. It also + // closes the window where status is still `processing` because the backend + // hasn't written `combining` yet. + const claimed = await db + .update(bulkAddressUploads) + .set({ status: "combining" }) + .where( + and( + eq(bulkAddressUploads.id, args.uploadId), + eq(bulkAddressUploads.status, "processing"), + ), + ) + .returning(); + if (claimed.length === 0) return { kind: "already_dispatched" }; + const [subTask] = await db .insert(subTasks) .values({ taskId: upload.taskId, status: "waiting" }) @@ -608,8 +627,23 @@ export async function requestCombineRetrigger(args: { payload, sessionToken: args.sessionToken, }); - if (!trigger.ok) + if (!trigger.ok) { + // Roll the claim back so the user can retry, and fail the subtask. + await Promise.all([ + db + .update(bulkAddressUploads) + .set({ status: "processing" }) + .where(eq(bulkAddressUploads.id, args.uploadId)), + db + .update(subTasks) + .set({ + status: "failed", + outputs: JSON.stringify({ error: trigger.message }), + }) + .where(eq(subTasks.id, subTask.id)), + ]); return { kind: "trigger_failed", status: trigger.status, message: trigger.message }; + } await db .update(subTasks)