diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 4c9690f3..296beb9b 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -23,6 +23,7 @@ services: - GITHUB_TOKEN=${GITHUB_TOKEN:-} networks: - frontend-net + - shared-dev pgadmin: image: dpage/pgadmin4 @@ -38,3 +39,6 @@ services: networks: frontend-net: driver: bridge + shared-dev: + external: true + name: shared-dev diff --git a/.gitignore b/.gitignore index 6fcf0a35..c2157ee6 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ cypress.env.json # typescript *.tsbuildinfo next-env.d.ts + +backlog/** diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..5fd0594b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,16 @@ +# Claude guidance for this project + +## Project conventions + +- **Domain language lives in [`CONTEXT.md`](./CONTEXT.md).** Read it before naming or discussing BulkUpload, Portfolio, Property, etc. concepts. +- **Architectural decisions live in [`docs/adr/`](./docs/adr/).** Read existing ADRs before proposing changes that touch state machines or core flows. Write a new ADR for any decision that's hard to reverse, surprising without context, and the result of a real trade-off. + +## React + +- **Avoid `useEffect` and `useMemo`.** Derive values inline, prefer Server Components + Route Handlers, prefer event handlers. If a hook is genuinely the only option, flag it and ask before using it. +- **Use TanStack Query (`@tanstack/react-query`), not raw `fetch`, for client-side HTTP.** Reads use `useQuery`; writes use `useMutation`. This project is on **v4** — note that `refetchInterval`'s callback signature is `(data, query)`, not v5's `(query)`. + +## Next.js 15 route handlers + +- `params` is a `Promise` — type as `{ params: Promise<{ ... }> }` and `await params` before destructuring. +- Error responses use `{ error: string }` consistently. Don't drift to `{ msg }` or other shapes. diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 00000000..4a956966 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,73 @@ +# Context + +This document captures the domain language used in this project. Terms here are the **canonical** ones — when more than one word exists for a concept, we pick one and treat the others as aliases to avoid. + +This file grows as terms are resolved during design conversations. Concepts that haven't been examined yet are not listed. + +## Language + +### Bulk upload + +**BulkUpload**: +A user-supplied spreadsheet of addresses for a Portfolio, transformed and matched to UPRNs before being inserted as Properties. Has an explicit lifecycle from upload through finalisation. +_Avoid_: import, batch, file upload, ingest + +**ColumnMapping**: +The user's declaration of which spreadsheet column means what (e.g. column "Property Address" means `address_1`). Stored as JSON on the BulkUpload row. +_Avoid_: schema, header map, field mapping + +**UPRN**: +Unique Property Reference Number — the UK national identifier for an address. Address matching attaches a UPRN to each row where possible. + +**Address matching**: +The pipeline stage that splits the source file by postcode, looks up UPRNs, and produces matched-address output. Triggered via FastAPI. +_Avoid_: postcode lookup, address resolution, address lookup + +**Combiner**: +The pipeline stage that aggregates the per-postcode address-matching outputs into a single combined CSV in S3, ready for review. +_Avoid_: aggregator, merger + +**Finalise**: +The terminal action that reads the combiner output, inserts rows as Properties on the Portfolio, and decides whether the BulkUpload needs further review. +_Avoid_: import, commit, ingest + +## Lifecycle + +A **BulkUpload** moves through these statuses: + +``` +ready_for_processing + → mapping_complete (user submits ColumnMapping; Next.js writes) + → 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) +``` + +`complete` and `failed` are terminal. + +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. + +See [ADR-0001](./docs/adr/0001-bulk-upload-state-machine.md) for the deliberate "not yet" decisions baked into this lifecycle. + +## Relationships + +- A **Portfolio** has many **BulkUploads**. +- A **BulkUpload** produces zero or more **Properties** when finalised. +- A **BulkUpload** has at most one **Task** (the orchestration handle for the FastAPI pipeline run); a Task has many **SubTasks** (one per pipeline stage: address matching, combiner). + +## Example dialogue + +> **Dev:** "If the **Combiner** finishes but the user hasn't clicked Finalise, what does the user see?" +> **Domain expert:** "The BulkUpload sits in `awaiting_review`. The frontend polls and shows a 'review and confirm' button. Nothing's been written to **Properties** yet." +> +> **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." + +## Flagged ambiguities + +- "Upload" is used in the codebase to mean both the file-on-S3 and the BulkUpload row. We standardise on **BulkUpload** for the row; the file is just "the source file." +- "Onboarding" appears in some route paths (`bulk_onboarding_inputs/...`) but isn't part of this glossary — we use **BulkUpload** end-to-end. diff --git a/docs/adr/0001-bulk-upload-state-machine.md b/docs/adr/0001-bulk-upload-state-machine.md new file mode 100644 index 00000000..741e4a36 --- /dev/null +++ b/docs/adr/0001-bulk-upload-state-machine.md @@ -0,0 +1,20 @@ +# BulkUpload state machine (v1) + +The BulkUpload lifecycle is: + +``` +ready_for_processing → mapping_complete → processing → combining → awaiting_review + → complete | needs_review | failed +``` + +Two writers: Next.js writes the user-driven transitions (`mapping_complete`, `processing`, `complete`, `needs_review`); the FastAPI `bulk_address2uprn_combiner` worker writes `combining` and `awaiting_review` directly to the DB during its run. Any aggregate enforcing the state machine on the Next.js side must treat those two as observed-not-owned. + +Three deliberate "not yet" decisions are baked into this version, each likely to be re-suggested without a record: + +1. **`needs_review` is terminal — no recovery flow.** A finalised upload with missing or duplicate UPRNs ends up here, and that's it. The imported Properties show up in the property table; a proper review/recovery UI is planned but out of scope. Without this note, every future architecture pass will surface "needs_review has no exit" as a bug. + +2. **Re-mapping is rejected after `mapping_complete`.** PATCHing `columnMapping` once Address matching has been triggered returns 409. We considered (B) reset-and-rerun (clear the combiner output, return to `mapping_complete`, force the user to re-trigger) but rejected it for now: it requires cleaning up abandoned Tasks/SubTasks and re-charging a FastAPI run. (A) is the smallest correct thing — the DB column never drifts from what produced the combined output. Revisit when re-run-from-review lands. + +3. **`failed` exists in the schema but is not yet written by any route.** Synchronous trigger failures (route can't reach FastAPI) are surfaced as React Query toasts on 5xx; the BulkUpload stays in its prior status and the user retries. In-flight failures (Combiner crashes silently) currently leave uploads stuck in `processing` — this is known-incomplete, waiting on FastAPI-side callback work to report failure. The status is in the schema now so the seam is ready when that work is scheduled. + +These are the only "no" decisions in v1; everything else (concurrency guards on stage triggers, persisting `failed`, etc.) is intended to be added. diff --git a/docs/adr/0002-bulk-upload-browser-driven-orchestration.md b/docs/adr/0002-bulk-upload-browser-driven-orchestration.md new file mode 100644 index 00000000..f27a5e07 --- /dev/null +++ b/docs/adr/0002-bulk-upload-browser-driven-orchestration.md @@ -0,0 +1,17 @@ +# BulkUpload pipeline stays browser-driven for now + +The BulkUpload pipeline chains three stages — address matching, combiner trigger, finalise — and the chain is currently driven by the **frontend polling** in `OnboardingProgress.tsx`. The browser fires `POST /combine` when the Task looks done, then `POST /finalize` when the upload reaches `awaiting_review`. Close the tab → upload gets stuck. + +A server-driven alternative (FastAPI callback into a Next.js webhook, or a sweeper route) would be more robust, but the entire bulk-upload flow is scheduled for redesign. This code path is being kept as **internal tooling for the Domna tech team** to onboard portfolios while the new flow is built — not for end-user use. + +So the cleanup invests only in things that survive the redesign: +- A real state machine on the BulkUpload aggregate (so the tech team can debug from the row alone). +- Deduplicated FastAPI trigger logic. +- Concurrency guards on stage triggers. + +It deliberately does **not** invest in: +- Server-side stage orchestration. +- Recovery from `failed` / stuck uploads. +- A real `awaiting_review` review UI. + +When the redesign lands, browser-driven chaining and the auto-finalise behaviour go with it. Future readers asking "why didn't they fix this" — that's why. diff --git a/docs/adr/0003-task-creation-inside-bulk-upload.md b/docs/adr/0003-task-creation-inside-bulk-upload.md new file mode 100644 index 00000000..2915346a --- /dev/null +++ b/docs/adr/0003-task-creation-inside-bulk-upload.md @@ -0,0 +1,12 @@ +# Task creation lives in BulkUpload, not behind a generic Task endpoint + +A `Task` is the orchestration handle for a FastAPI pipeline run; per `CONTEXT.md`, **a BulkUpload has at most one Task**. Today BulkUpload is the only feature that creates Tasks. + +We're collapsing the previous two-step client seam — `POST /api/tasks` to create a Task and SubTask, then `POST /bulk-uploads/[uploadId]/start-address-matching` with the resulting IDs — into a single route. Task + SubTask creation moves inside `triggerAddressMatching` in `src/lib/bulkUpload/server.ts`. The generic `POST /api/tasks` endpoint is deleted; the `GET /api/tasks` listing (admin Tasks UI) stays. + +The trade-off was between: + +- **(rejected) Keep `POST /api/tasks` for future generic use.** No other feature creates Tasks today. By the deletion test, the generic endpoint wasn't earning its keep — every line was overhead carried for hypothetical callers, while the only real caller had to perform a leaky two-call dance to satisfy a contract that didn't reflect the domain. +- **(chosen) Collapse Task creation into the BulkUpload module.** The seam matches the domain: BulkUpload owns its Task's lifecycle, including the moment of creation. One route handles the full transition into `processing`. The client surface (`useStartAddressMatching`) is one mutation, not a chain. + +A future reader will see `/api/tasks` exposing GET-only and reasonably wonder where Tasks are created. The answer is: inside whichever feature owns the Task. Today that's BulkUpload only. If a second feature ever needs to create Tasks, re-extract a generic creator at that point — extracting from two real consumers is straightforward, whereas keeping a speculative endpoint live without consumers leaves a stale-by-default seam that drifts from how Tasks are actually created. diff --git a/docs/adr/0004-bulk-upload-explicit-stage-buttons.md b/docs/adr/0004-bulk-upload-explicit-stage-buttons.md new file mode 100644 index 00000000..de7a98c4 --- /dev/null +++ b/docs/adr/0004-bulk-upload-explicit-stage-buttons.md @@ -0,0 +1,19 @@ +# Bulk-upload pipeline advances via explicit Run Combiner / Finalise buttons + +[ADR-0002](./0002-bulk-upload-browser-driven-orchestration.md) described the bulk-upload pipeline as **browser-driven and auto-firing**: a polling loop in `OnboardingProgress.tsx` watched the Task summary and fired `POST /combine` when the Task looked done, then `POST /finalize` when the upload reached `awaiting_review`, with mutable `combineFired` / `finalizeFired` / `refreshed` flags gating the side effects. + +This decision keeps the pipeline browser-driven (per ADR-0002) but replaces the auto-fire behaviour with **explicit buttons** the Domna tech team clicks to advance each stage. The state machine in [ADR-0001](./0001-bulk-upload-state-machine.md) is unchanged. + +Concretely: + +- The progress screen polls a single `GET /bulk-uploads/[uploadId]/progress` snapshot (Task summary + BulkUpload row in one query). +- "Run Combiner" appears when the Task reaches terminal-non-failed and the Combiner hasn't been triggered yet. +- "Finalise" appears when the BulkUpload reaches `awaiting_review`. +- Polling stops once the BulkUpload reaches a terminal status; on `useFinalize` success the caller runs `router.refresh()`. + +The trade-off was between: + +- **(rejected) Keep auto-fire and translate the orchestration to react-query with `useEffect`s.** Functional, but the auto-fire UX hides where the pipeline gets stuck — which is the failure mode the Domna tech team needs to debug. CLAUDE.md's "avoid `useEffect`" rule also gets stretched: three effects, one per side-effecting transition, each watching derived eligibility flags. +- **(chosen) Explicit buttons.** The "fire once" mutable flags collapse into react-query mutation state (`isIdle` / `isPending` / `isSuccess`) — pure derivations from the cache, no `useEffect`. Each stage is observable, retryable, and reflects the current BulkUpload status directly. Tab close still leaves uploads stuck (browser-driven progression, per ADR-0002), but "stuck" is now legible: the team sees which button is pending. + +When the bulk-upload redesign lands (per ADR-0002, "out of scope"), this entire screen and its buttons go away in favour of server-driven progression. Until then, the buttons are the right level of investment: they survive the redesign as discoverable artifacts of the prior flow, and they make the current flow tractable for the team using it. diff --git a/docs/adr/0005-retire-needs-review-status.md b/docs/adr/0005-retire-needs-review-status.md new file mode 100644 index 00000000..f12ebb67 --- /dev/null +++ b/docs/adr/0005-retire-needs-review-status.md @@ -0,0 +1,14 @@ +# Retire the `needs_review` BulkUpload status + +[ADR-0001](./0001-bulk-upload-state-machine.md) preserved `needs_review` as a terminal status (deliberate decision #1): a finalised upload with missing or duplicate UPRNs ended up there, distinct from `complete`, even though no recovery flow existed. The reasoning was that the status flag still carried a UI signal — "Imported with issues" copy on the upload detail page — that told the team manual cleanup was needed in the property table. + +This decision retires `needs_review` entirely. Finalise now always sets `complete`, regardless of whether any rows were missing a UPRN or shared one with another row. The status enum, the `markFinalized` signature, and the per-status UI copy are all simplified accordingly. + +The terminal set is now `complete` and `failed` only. The state machine in ADR-0001 otherwise stands; only the post-Finalise branching changes. + +The trade-off was between: + +- **(rejected) Keep `needs_review` for the UI distinction.** The flag was the only consumer of the missing/duplicate-UPRN counts during Finalise, and the counts themselves were already dead in the response payload. Keeping the flag meant maintaining a state-machine branch and a STATUS_CONFIG entry for a signal that — per ADR-0001 — has no follow-up action. Future readers would continue to surface "needs_review has no exit" as friction. +- **(chosen) Always `complete`.** One terminal success state. The Finalise route loses the missing/duplicate-UPRN booleans entirely; rows are inserted with whatever UPRN they have (or none), and the team handles cleanup in the property table directly. If a recovery flow ever lands, it can re-introduce a more meaningful intermediate status at that point — driven by the actual recovery UX rather than a defensive flag set today. + +Legacy `bulk_address_uploads` rows with `status = 'needs_review'` (if any exist) will fall through `STATUS_CONFIG` lookup and render as the default `ready_for_processing` card. This is acceptable because (a) the flow is internal Domna tooling per ADR-0002, (b) any such rows pre-date this change and represent uploads the team has already triaged manually, and (c) the records still link to imported properties via the BulkUpload row's relationships. No migration is run. 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 new file mode 100644 index 00000000..afee7f0e --- /dev/null +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/combine/route.ts @@ -0,0 +1,35 @@ +import { requestCombineRetrigger } from "@/lib/bulkUpload/server"; +import { readSessionToken } from "@/lib/session"; +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; + +export async function POST( + 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 result = await requestCombineRetrigger({ + uploadId, + sessionToken: readSessionToken(request), + }); + + switch (result.kind) { + case "triggered": + return NextResponse.json( + { taskId: result.taskId, subTaskId: result.subTaskId }, + { status: 200 } + ); + case "already_combined": + return NextResponse.json({ alreadyCombined: true }, { status: 200 }); + case "not_found": + return NextResponse.json({ error: "Not found" }, { status: 404 }); + case "missing_task": + return NextResponse.json({ error: "Upload has no task" }, { status: 422 }); + case "trigger_failed": + return NextResponse.json({ error: result.message }, { status: result.status }); + } +} 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 new file mode 100644 index 00000000..b6fd7ec9 --- /dev/null +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/finalize/route.ts @@ -0,0 +1,160 @@ +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) }; +} + +export async function POST( + _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 guarded = await loadForFinalize(uploadId); + switch (guarded.kind) { + case "not_found": + return NextResponse.json({ error: "Not found" }, { status: 404 }); + case "already_finalized": + 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 }); + } +} diff --git a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/progress/route.ts b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/progress/route.ts new file mode 100644 index 00000000..f5ac9969 --- /dev/null +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/progress/route.ts @@ -0,0 +1,17 @@ +import { getProgressView } from "@/lib/bulkUpload/server"; +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; + +export async function GET( + _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 view = await getProgressView(uploadId); + if (!view) return NextResponse.json({ error: "Not found" }, { status: 404 }); + return NextResponse.json(view, { status: 200 }); +} diff --git a/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/route.ts b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/route.ts new file mode 100644 index 00000000..2b452991 --- /dev/null +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/route.ts @@ -0,0 +1,41 @@ +import { setColumnMapping } from "@/lib/bulkUpload/server"; +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +const PatchSchema = z.object({ + columnMapping: z.record(z.string(), z.string()), +}); + +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ portfolioId: string; uploadId: string }> } +) { + 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 setColumnMapping(uploadId, body.columnMapping); + switch (result.kind) { + case "ok": + return NextResponse.json(result.upload, { status: 200 }); + case "not_found": + return NextResponse.json({ error: "Not found" }, { status: 404 }); + case "invalid_status": + return NextResponse.json( + { error: `Cannot remap upload in state '${result.current}'` }, + { status: 409 } + ); + case "invalid_mapping": + return NextResponse.json({ error: result.reason }, { status: 422 }); + } + } catch (error) { + console.error("Failed to save column mapping:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} 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 new file mode 100644 index 00000000..5fd282fa --- /dev/null +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/[uploadId]/start-address-matching/route.ts @@ -0,0 +1,124 @@ +import { NextRequest, NextResponse } from "next/server"; +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 { 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", +}; + +function transformFile( + buffer: Buffer, + columnMapping: Record +): { csv: string; error?: never } | { csv?: never; error: string } { + 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" }; + + 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; + } + + if (!outputHeaders.includes("Address 1")) + return { error: 'Mapping must include "Address 1"' }; + if (!outputHeaders.includes("postcode")) + return { error: 'Mapping must include "postcode"' }; + + const outputRows = rows.map((row) => { + const out: Record = {}; + for (const [src, renamed] of Object.entries(sourceToOutput)) { + out[renamed] = row[src] ?? ""; + } + return out; + }); + + const outSheet = XLSX.utils.json_to_sheet(outputRows, { header: outputHeaders }); + return { csv: XLSX.utils.sheet_to_csv(outSheet) }; +} + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ portfolioId: string; uploadId: string }> } +) { + const session = await getServerSession(AuthOptions); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + + const { portfolioId, uploadId } = await params; + + const guarded = await loadForAddressMatching(uploadId); + switch (guarded.kind) { + case "not_found": + return NextResponse.json({ error: "Not found" }, { status: 404 }); + case "wrong_state": + return NextResponse.json( + { error: `Upload not ready for onboarding (state: ${guarded.current})` }, + { status: 409 } + ); + case "missing_mapping": + return NextResponse.json({ error: "Column mapping missing" }, { status: 422 }); + } + const upload = guarded.upload; + + const s3 = createS3Client(); + const outputS3 = createRetrofitDataS3Client(); + const outputBucket = retrofitDataS3Bucket(); + + let fileBuffer: Buffer; + try { + const obj = await s3 + .getObject({ Bucket: upload.s3Bucket, Key: upload.s3Key }) + .promise(); + fileBuffer = Buffer.from(obj.Body as Uint8Array); + } catch (err) { + console.error("Failed to read source file from S3:", err); + return NextResponse.json({ error: "Failed to read source file" }, { status: 500 }); + } + + const transformed = transformFile(fileBuffer, upload.columnMapping!); + if (transformed.error) + return NextResponse.json({ error: transformed.error }, { status: 422 }); + + const transformedKey = `bulk_onboarding_inputs/${portfolioId}/${uploadId}.csv`; + try { + await outputS3 + .putObject({ + Bucket: outputBucket, + Key: transformedKey, + Body: transformed.csv, + ContentType: "text/csv", + }) + .promise(); + } catch (err) { + console.error("Failed to upload transformed CSV:", err); + return NextResponse.json({ error: "Failed to store transformed file" }, { status: 500 }); + } + + const s3Uri = `s3://${outputBucket}/${transformedKey}`; + + const trigger = await triggerAddressMatching({ + uploadId, + s3Uri, + sessionToken: readSessionToken(request), + }); + if (trigger.kind === "trigger_failed") + return NextResponse.json({ error: trigger.message }, { status: trigger.status }); + + return NextResponse.json({ taskId: trigger.taskId }, { status: 200 }); +} diff --git a/src/app/api/portfolio/[portfolioId]/bulk-uploads/route.ts b/src/app/api/portfolio/[portfolioId]/bulk-uploads/route.ts new file mode 100644 index 00000000..a6ebbc15 --- /dev/null +++ b/src/app/api/portfolio/[portfolioId]/bulk-uploads/route.ts @@ -0,0 +1,17 @@ +import { listForPortfolio } from "@/lib/bulkUpload/server"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ portfolioId: string }> } +) { + const { portfolioId } = await params; + + try { + const uploads = await listForPortfolio(portfolioId); + return NextResponse.json(uploads, { status: 200 }); + } catch (error) { + console.error("Failed to fetch bulk uploads:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/src/app/api/tasks/[taskId]/summary/route.ts b/src/app/api/tasks/[taskId]/summary/route.ts new file mode 100644 index 00000000..6e5cb5c2 --- /dev/null +++ b/src/app/api/tasks/[taskId]/summary/route.ts @@ -0,0 +1,40 @@ +import { db } from "@/app/db/db"; +import { tasks } from "@/app/db/schema/tasks/tasks"; +import { subTasks } from "@/app/db/schema/tasks/subtask"; +import { eq, count, sql } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ taskId: string }> } +) { + const { taskId } = await params; + + try { + const [row] = await db + .select({ + id: tasks.id, + taskSource: tasks.taskSource, + status: tasks.status, + service: tasks.service, + jobStarted: tasks.jobStarted, + jobCompleted: tasks.jobCompleted, + updatedAt: tasks.updatedAt, + 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`, + }) + .from(tasks) + .leftJoin(subTasks, eq(subTasks.taskId, tasks.id)) + .where(eq(tasks.id, taskId)) + .groupBy(tasks.id) + .limit(1); + + if (!row) return NextResponse.json({ error: "Not found" }, { status: 404 }); + + return NextResponse.json(row); + } catch (error) { + console.error("Error fetching task summary:", error); + return NextResponse.json({ error: "Failed to fetch task summary" }, { status: 500 }); + } +} diff --git a/src/app/api/upload/bulk-addresses/confirm/route.ts b/src/app/api/upload/bulk-addresses/confirm/route.ts new file mode 100644 index 00000000..80e94e4b --- /dev/null +++ b/src/app/api/upload/bulk-addresses/confirm/route.ts @@ -0,0 +1,55 @@ +import { db } from "@/app/db/db"; +import { bulkAddressUploads } from "@/app/db/schema/bulk_address_uploads"; +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +const BodySchema = z.object({ + fileKey: z.string(), + filename: z.string(), + portfolioId: z.string(), + userId: z.string(), + sourceHeaders: z + .array(z.union([z.string(), z.null(), z.undefined()])) + .default([]) + .transform((arr) => + arr.filter((h): h is string => typeof h === "string" && h.trim().length > 0) + ), +}); + +export async function POST(request: NextRequest) { + let body; + try { + body = BodySchema.parse(await request.json()); + } catch (error) { + console.error("Invalid input:", error); + return NextResponse.json({ error: "Invalid input" }, { status: 400 }); + } + + const bucket = process.env.RETROFIT_PLAN_INPUT_BUCKET_NAME; + if (!bucket) { + console.error("RETROFIT_PLAN_INPUT_BUCKET_NAME not set"); + return NextResponse.json({ error: "Server misconfiguration" }, { status: 500 }); + } + + try { + const [record] = await db + .insert(bulkAddressUploads) + .values({ + portfolioId: body.portfolioId, + userId: body.userId, + s3Bucket: bucket, + s3Key: body.fileKey, + filename: body.filename, + sourceHeaders: body.sourceHeaders, + }) + .returning(); + + return NextResponse.json( + { id: record.id, s3Key: record.s3Key, s3Bucket: record.s3Bucket, status: record.status }, + { status: 201 } + ); + } catch (error) { + console.error("Failed to record upload:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/src/app/api/upload/bulk-addresses/route.ts b/src/app/api/upload/bulk-addresses/route.ts new file mode 100644 index 00000000..a54966f5 --- /dev/null +++ b/src/app/api/upload/bulk-addresses/route.ts @@ -0,0 +1,36 @@ +import { createS3Client } from "@/app/utils/s3"; +import { NextRequest, NextResponse } from "next/server"; +import { z } from "zod"; + +const BodySchema = z.object({ + userId: z.string(), + portfolioId: z.string(), + fileKey: z.string(), + contentType: z.string(), +}); + +export async function POST(request: NextRequest) { + let body; + try { + body = BodySchema.parse(await request.json()); + } catch (error) { + console.error("Invalid input:", error); + return NextResponse.json({ error: "Invalid input" }, { status: 400 }); + } + + try { + const s3 = createS3Client(); + + const preSignedUrl = await s3.getSignedUrlPromise("putObject", { + Bucket: process.env.RETROFIT_PLAN_INPUT_BUCKET_NAME, + Key: body.fileKey, + ContentType: body.contentType, + Expires: 5 * 60, + }); + + return NextResponse.json({ url: preSignedUrl }, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/src/app/components/portfolio/AddNew.tsx b/src/app/components/portfolio/AddNew.tsx index e6f91be7..cef42cb9 100644 --- a/src/app/components/portfolio/AddNew.tsx +++ b/src/app/components/portfolio/AddNew.tsx @@ -35,6 +35,16 @@ export default function AddNew({ router.push(`/portfolio/${portfolioId}/remote-assessment`); } + function handleBulkUploadClick() { + const pw = window.prompt("Enter password to access bulk upload"); + if (pw === null) return; + if (pw === "domnatechteamonly") { + setIsBulkUploadOpen(true); + } else { + window.alert("Incorrect password"); + } + } + return ( <> {({ active }) => ( - -
-
- -
+ + {/* Header */} +
- - Coming Soon - -

- Bulk Address Upload -

-

- Upload multiple addresses in one go. This feature is currently in development - and will be available soon. + + Bulk Upload: New Properties + +

+ This workflow is designed for adding new residential or commercial + assets to your portfolio. Upload your dataset to begin the + transformation.

-
+ + {/* Content */} +
+ + {/* Template section */} +
+
+
+ +
+
+

Required Template Format

+

+ Must contain:{" "} + + Address, Postcode + +

+
+
+ +
+ + {/* Dropzone */} +
!uploading && fileInputRef.current?.click()} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + className={`border-2 border-dashed rounded-2xl p-12 flex flex-col items-center justify-center transition-colors ${ + uploading || validating + ? "border-gray-200 bg-gray-50 cursor-default" + : validationError + ? "border-red-300 bg-red-50 cursor-pointer" + : isDragging + ? "border-midblue bg-blue-50 cursor-copy" + : selectedFile + ? "border-green-400 bg-green-50 cursor-pointer" + : "border-gray-200 hover:border-gray-300 hover:bg-gray-50 cursor-pointer" + }`} + > + e.stopPropagation()} + /> + + {validating ? ( + <> +
+ +
+

Checking headers…

+

Validating column structure

+ + ) : uploading ? ( + <> +
+ +
+

Uploading…

+

{selectedFile?.name}

+
+
+
+

{uploadProgress ?? 0}%

+ + ) : validationError ? ( + <> + +

{validationError}

+

Click to choose a different file

+ + ) : selectedFile ? ( + <> + +

{selectedFile.name}

+

Click to change file

+ + ) : ( + <> +
+ +
+

+ Drag and drop CSV or XLSX +

+

+ or click to browse · Max {MAX_FILE_SIZE_MB}MB +

+ + )} +
+ + {/* Upload error */} + {uploadError && ( +

+ + {uploadError} +

+ )} + + {/* Info strip */} +
+ + + Properties will be automatically validated against national + architectural databases. + +
+
+ + {/* Footer */} +
+
+ + +
+
+ +
+
+
diff --git a/src/app/db/migrations/0179_mighty_cardiac.sql b/src/app/db/migrations/0179_mighty_cardiac.sql deleted file mode 100644 index 75889c22..00000000 --- a/src/app/db/migrations/0179_mighty_cardiac.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE "bulk_address_uploads" ADD COLUMN "task_id" uuid;--> statement-breakpoint -ALTER TABLE "bulk_address_uploads" ADD COLUMN "combined_output_s3_uri" text; \ No newline at end of file diff --git a/src/app/db/migrations/meta/0178_snapshot.json b/src/app/db/migrations/meta/0178_snapshot.json index d380b0dd..c793404d 100644 --- a/src/app/db/migrations/meta/0178_snapshot.json +++ b/src/app/db/migrations/meta/0178_snapshot.json @@ -303,6 +303,18 @@ "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", @@ -6467,8 +6479,8 @@ "schema": "public", "values": [ "none", - "cladded with “sufficient space to fill the wall”", - "cladded with “insufficient space to fill the wall”" + "cladded with \u201csufficient space to fill the wall\u201d", + "cladded with \u201cinsufficient space to fill the wall\u201d" ] }, "public.inspections_insulation_material": { @@ -6495,8 +6507,8 @@ "schema": "public", "values": [ "no render", - "rendered with “insufficient” space between dpc and render", - "rendered with “sufficient” space between dpc and render" + "rendered with \u201cinsufficient\u201d space between dpc and render", + "rendered with \u201csufficient\u201d space between dpc and render" ] }, "public.inspections_roof_orientation": { @@ -6850,13 +6862,13 @@ "schema": "public", "values": [ "1", - "2–5", - "6–20", + "2\u20135", + "6\u201320", "21+", - "1–50", - "51–100", - "101–300", - "301–1000", + "1\u201350", + "51\u2013100", + "101\u2013300", + "301\u20131000", "1000+" ] }, diff --git a/src/app/db/migrations/meta/0179_snapshot.json b/src/app/db/migrations/meta/0179_snapshot.json deleted file mode 100644 index 69e16da1..00000000 --- a/src/app/db/migrations/meta/0179_snapshot.json +++ /dev/null @@ -1,6910 +0,0 @@ -{ - "id": "351c4142-1926-4103-b56a-33f8530eafef", - "prevId": "eed32c53-4a51-451e-9898-5b2bd962bae7", - "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_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 - }, - "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 - }, - "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 - }, - "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.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.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.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_unique": { - "name": "portfolio_organisation_portfolio_id_unique", - "nullsNotDistinct": false, - "columns": [ - "portfolio_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.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.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 - }, - "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 - }, - "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 - } - }, - "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.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", - "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.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.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 - } - }, - "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.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.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" - ] - }, - "public.file_source": { - "name": "file_source", - "schema": "public", - "values": [ - "pas hub", - "sharepoint", - "hubspot", - "ecmk", - "contractor" - ] - }, - "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" - ] - }, - "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/schema/property.ts b/src/app/db/schema/property.ts index 258dd028..f5a24331 100644 --- a/src/app/db/schema/property.ts +++ b/src/app/db/schema/property.ts @@ -386,6 +386,7 @@ export interface PropertyWithRelations extends Record { totalFloorArea: number | null; co2Emissions: number | null; mainfuel: string | null; + lexiscore: number | null; } export type NonIntrusiveSurveyNotes = InferModel< diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx new file mode 100644 index 00000000..3824b1b4 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/OnboardingProgress.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { ArrowRightIcon } from "@heroicons/react/24/outline"; +import { + useBulkUploadProgress, + useFinalize, + useRequestCombine, +} from "@/lib/bulkUpload/client"; + +interface Props { + portfolioSlug: string; + portfolioId: string; + uploadId: string; + isDomnaUser: boolean; +} + +const TASK_TERMINAL_STATUSES = new Set(["complete", "completed", "failed", "failure", "error"]); +const TASK_FAILED_STATUSES = new Set(["failed", "failure", "error"]); + +export default function OnboardingProgress({ + portfolioSlug, + portfolioId, + uploadId, + isDomnaUser, +}: Props) { + const router = useRouter(); + const progress = useBulkUploadProgress(portfolioId, uploadId); + const combine = useRequestCombine(portfolioId, uploadId); + const finalize = useFinalize(portfolioId, uploadId); + + if (progress.isError) return null; + if (!progress.data) { + return ( +
+ + Loading progress… +
+ ); + } + + const { task, upload } = progress.data; + const total = task?.totalSubtasks ?? 0; + const completedSubtasks = task?.completedSubtasks ?? 0; + const failedSubtasks = task?.failedSubtasks ?? 0; + const percent = total > 0 ? Math.round((completedSubtasks / total) * 100) : 0; + + const taskStatus = task?.status.toLowerCase() ?? ""; + const taskDone = TASK_TERMINAL_STATUSES.has(taskStatus); + const taskFailed = TASK_FAILED_STATUSES.has(taskStatus); + const isCombining = upload.status === "combining"; + const isImporting = upload.status === "awaiting_review"; + + const canRunCombiner = taskDone && !taskFailed && upload.status === "processing"; + const canFinalize = upload.status === "awaiting_review"; + + return ( +
+
+
0 ? `${percent}%` : "4%" }} + /> +
+ +
+ {total > 0 && ( + + {completedSubtasks} / {total} batches complete + + )} + {failedSubtasks > 0 && ( + + + {failedSubtasks} failed + + )} + {!taskDone && ( + + + Running + + )} + {isCombining && ( + + + Combining results… + + )} + {isImporting && ( + + + Awaiting import + + )} +
+ + {(canRunCombiner || canFinalize) && ( +
+ {canRunCombiner && ( + combine.mutate()} + /> + )} + {canFinalize && ( + + finalize.mutate(undefined, { onSuccess: () => router.refresh() }) + } + /> + )} +
+ )} + + {combine.error && ( +

{combine.error.message}

+ )} + {finalize.error && ( +
+

Import failed

+

{finalize.error.message}

+
+ )} + + {isDomnaUser && ( + + View detailed logs + + )} +
+ ); +} + +function StageButton({ + label, + activeLabel, + isPending, + onClick, +}: { + label: string; + activeLabel: string; + isPending: boolean; + onClick: () => void; +}) { + return ( + + ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/StartAddressMatchingButton.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/StartAddressMatchingButton.tsx new file mode 100644 index 00000000..afd6117c --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/StartAddressMatchingButton.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { ArrowRightIcon } from "@heroicons/react/24/outline"; +import { useStartAddressMatching } from "@/lib/bulkUpload/client"; + +interface Props { + portfolioId: string; + uploadId: string; + filename: string; +} + +export default function StartAddressMatchingButton({ portfolioId, uploadId }: Props) { + const router = useRouter(); + const { mutate, isPending, error } = useStartAddressMatching(portfolioId, uploadId); + + return ( +
+ + {error && ( +

+ {error.message} +

+ )} +
+ ); +} 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 new file mode 100644 index 00000000..adda7d38 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/map-columns/MapColumnsClient.tsx @@ -0,0 +1,258 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { + ArrowLeftIcon, + ArrowRightIcon, + TableCellsIcon, + 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; +} + +interface Props { + portfolioId: string; + uploadId: string; + filename: string; + sourceHeaders: string[]; + existingMapping?: Record; +} + +export default function MapColumnsClient({ + portfolioId, + uploadId, + filename, + sourceHeaders, + existingMapping, +}: Props) { + const router = useRouter(); + 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 submitting = setMappingMutation.isPending; + const error = setMappingMutation.error?.message ?? null; + const canSubmit = missingRequired.length === 0 && !submitting; + + function setField(header: string, value: string) { + setMapping((prev) => ({ ...prev, [header]: value })); + } + + function handleSubmit() { + if (!canSubmit) return; + setMappingMutation.mutate( + { columnMapping: mapping }, + { + onSuccess: () => { + router.push(`/portfolio/${portfolioId}/bulk-upload/${uploadId}`); + }, + } + ); + } + + return ( +
+ {/* Breadcrumb + step */} +
+

+ Bulk Uploads › Column Remapper +

+
+ + Step 2 of 3 + +
+ {[1, 2, 3].map((s) => ( +
+ ))} +
+
+
+ + {/* Header */} +
+

+ Column Remapper +

+

+ Align your spreadsheet headers with our internal property data structure to + ensure accurate address processing. +

+
+ + {/* 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(", ")} +

+ )} + {error &&

{error}

} + + {/* Footer */} +
+ + + Back + + +
+ + Cancel + + +
+
+ + {/* Pro tip */} +
+

+ Pro Tip +

+

+ “Ensure your source file doesn't have blank headers. Any column mapped to + “Skip” will be ignored during import.” +

+
+
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/map-columns/page.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/map-columns/page.tsx new file mode 100644 index 00000000..3ce0ac62 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/map-columns/page.tsx @@ -0,0 +1,33 @@ +import { db } from "@/app/db/db"; +import { bulkAddressUploads } from "@/app/db/schema/bulk_address_uploads"; +import { eq } from "drizzle-orm"; +import { getServerSession } from "next-auth"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { redirect, notFound } from "next/navigation"; +import MapColumnsClient from "./MapColumnsClient"; + +export default async function MapColumnsPage(props: { + params: Promise<{ slug: string; uploadId: string }>; +}) { + const { slug, uploadId } = await props.params; + const session = await getServerSession(AuthOptions); + if (!session) redirect("/login"); + + const [upload] = await db + .select() + .from(bulkAddressUploads) + .where(eq(bulkAddressUploads.id, uploadId)) + .limit(1); + + if (!upload) notFound(); + + return ( + + ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/page.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/page.tsx new file mode 100644 index 00000000..f28787ff --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/page.tsx @@ -0,0 +1,196 @@ +import { db } from "@/app/db/db"; +import { bulkAddressUploads } from "@/app/db/schema/bulk_address_uploads"; +import { eq } from "drizzle-orm"; +import { getServerSession } from "next-auth"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { redirect, notFound } from "next/navigation"; +import Link from "next/link"; +import { + ArrowLeftIcon, + ArrowRightIcon, + CheckCircleIcon, + ClockIcon, + ExclamationCircleIcon, + ArrowPathIcon, +} from "@heroicons/react/24/outline"; +import StartAddressMatchingButton from "./StartAddressMatchingButton"; +import OnboardingProgress from "./OnboardingProgress"; + +function formatDate(date: Date) { + return new Intl.DateTimeFormat("en-GB", { + day: "2-digit", + month: "short", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(date); +} + +const STATUS_CONFIG = { + ready_for_processing: { + icon: ClockIcon, + iconBg: "bg-amber-50", + iconColor: "text-amber-500", + title: "Awaiting column mapping", + body: "Map your spreadsheet columns to our internal fields before processing can begin.", + cta: true, + }, + mapping_complete: { + icon: CheckCircleIcon, + iconBg: "bg-blue-50", + iconColor: "text-blue-500", + title: "Mapping complete", + body: "Column mapping saved. Start onboarding to begin matching your addresses to UPRNs.", + cta: true, + }, + processing: { + icon: ArrowPathIcon, + iconBg: "bg-blue-50", + iconColor: "text-blue-500", + title: "Processing…", + body: "Your file is currently being processed. This may take a few minutes.", + cta: false, + }, + combining: { + icon: ArrowPathIcon, + iconBg: "bg-blue-50", + iconColor: "text-blue-500", + title: "Combining results…", + body: "Your matched addresses are being assembled. Almost ready for review.", + cta: false, + }, + awaiting_review: { + icon: ArrowPathIcon, + iconBg: "bg-blue-50", + iconColor: "text-blue-500", + title: "Importing addresses…", + body: "Matches ready, writing into your portfolio.", + cta: false, + }, + complete: { + icon: CheckCircleIcon, + iconBg: "bg-green-50", + iconColor: "text-green-500", + title: "Processing complete", + body: "All addresses have been imported into your portfolio.", + cta: false, + }, + failed: { + icon: ExclamationCircleIcon, + iconBg: "bg-red-50", + iconColor: "text-red-500", + title: "Processing failed", + body: "Something went wrong during processing. Contact a Domna representative for assistance.", + cta: false, + }, +} as const; + +export default async function BulkUploadDetailPage(props: { + params: Promise<{ slug: string; uploadId: string }>; +}) { + const { slug, uploadId } = await props.params; + const session = await getServerSession(AuthOptions); + if (!session) redirect("/login"); + const isDomnaUser = !!session.user?.email?.endsWith("@domna.homes"); + + const [upload] = await db + .select() + .from(bulkAddressUploads) + .where(eq(bulkAddressUploads.id, uploadId)) + .limit(1); + + if (!upload) notFound(); + + const statusKey = upload.status as keyof typeof STATUS_CONFIG; + const config = STATUS_CONFIG[statusKey] ?? STATUS_CONFIG.ready_for_processing; + const Icon = config.icon; + + return ( +
+ {/* Back */} + + + Back to uploads + + + {/* Header */} +
+

+ Bulk Upload +

+

+ {upload.filename} +

+

Uploaded {formatDate(upload.createdAt)}

+
+ + {/* Status card */} +
+
+
+ +
+
+

{config.title}

+

{config.body}

+ + {statusKey === "ready_for_processing" && ( + + Map Columns + + + )} + + {statusKey === "mapping_complete" && ( +
+ + Edit column mapping + + + +
+ )} + + {(statusKey === "processing" || + statusKey === "combining" || + statusKey === "awaiting_review" || + statusKey === "complete" || + statusKey === "failed") && + upload.taskId && ( + + )} + + {statusKey === "complete" && ( + + Open properties + + + )} +
+
+
+ +
+ ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/page.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/page.tsx new file mode 100644 index 00000000..281f5744 --- /dev/null +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/page.tsx @@ -0,0 +1,141 @@ +import { db } from "@/app/db/db"; +import { bulkAddressUploads } from "@/app/db/schema/bulk_address_uploads"; +import { eq, desc } from "drizzle-orm"; +import { getServerSession } from "next-auth"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { redirect } from "next/navigation"; +import Link from "next/link"; +import { + ArrowRightIcon, + DocumentTextIcon, + CloudArrowUpIcon, +} from "@heroicons/react/24/outline"; + +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" }, + complete: { label: "Complete", classes: "bg-green-100 text-green-700" }, + failed: { label: "Failed", classes: "bg-red-100 text-red-700" }, +}; + +function formatDate(date: Date) { + return new Intl.DateTimeFormat("en-GB", { + day: "2-digit", + month: "short", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(date); +} + +export default async function BulkUploadListPage(props: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await props.params; + const session = await getServerSession(AuthOptions); + if (!session) redirect("/login"); + + const uploads = await db + .select() + .from(bulkAddressUploads) + .where(eq(bulkAddressUploads.portfolioId, slug)) + .orderBy(desc(bulkAddressUploads.createdAt)); + + return ( +
+ {/* Header */} +
+

+ Portfolio › Bulk Uploads +

+

+ Batch Uploads +

+

+ Select an upload to continue processing, or start a new import. +

+
+ + {uploads.length === 0 ? ( + /* Empty state */ +
+
+ +
+

No uploads yet

+

+ Use the Bulk Upload button on your portfolio to get started. +

+
+ ) : ( + /* Upload list */ +
+ {/* Column headers */} +
+ + File + + + Uploaded + + + Status + + +
+ + {uploads.map((upload) => { + const status = STATUS_LABELS[upload.status] ?? { + label: upload.status, + classes: "bg-gray-100 text-gray-600", + }; + return ( + + {/* Filename */} +
+
+ +
+
+

+ {upload.filename} +

+

+ {upload.s3Key.split("/").pop()} +

+
+
+ + {/* Date */} +
+

+ {formatDate(upload.createdAt)} +

+
+ + {/* Status badge */} +
+ + + {status.label} + +
+ + {/* Arrow */} +
+ +
+ + ); + })} +
+ )} +
+ ); +} diff --git a/src/app/portfolio/[slug]/utils.ts b/src/app/portfolio/[slug]/utils.ts index 8d7ff899..e3676c89 100644 --- a/src/app/portfolio/[slug]/utils.ts +++ b/src/app/portfolio/[slug]/utils.ts @@ -710,7 +710,8 @@ export async function getProperties( epc.is_expired AS "epcIsExpired", epc.total_floor_area AS "totalFloorArea", epc.co2_emissions AS "co2Emissions", - epc.mainfuel AS mainfuel + epc.mainfuel AS mainfuel, + p.lexiscore AS lexiscore FROM property p LEFT JOIN property_targets t ON t.property_id = p.id @@ -751,7 +752,8 @@ export async function getProperties( epc.is_expired, epc.total_floor_area, epc.co2_emissions, - epc.mainfuel + epc.mainfuel, + p.lexiscore LIMIT ${limit} OFFSET ${offset}; `); diff --git a/src/app/utils/s3.ts b/src/app/utils/s3.ts index 236c412f..2c034156 100644 --- a/src/app/utils/s3.ts +++ b/src/app/utils/s3.ts @@ -20,6 +20,20 @@ export function createS3Client(config?: S3Config) { }); } +export function createRetrofitDataS3Client() { + return new S3({ + region: process.env.RETROFIT_DATA_DEV_REGION, + accessKeyId: process.env.RETROFIT_DATA_DEV_ACCESS_KEY, + secretAccessKey: process.env.RETROFIT_DATA_DEV_SECRET_KEY, + }); +} + +export function retrofitDataS3Bucket(): string { + const bucket = process.env.RETROFIT_DATA_DEV_S3_BUCKET_NAME; + if (!bucket) throw new Error("RETROFIT_DATA_DEV_S3_BUCKET_NAME not set"); + return bucket; +} + // Get presigned url from s3 export type PresignGetOptions = { diff --git a/src/app/utils/sqs.ts b/src/app/utils/sqs.ts index e653fee2..285fbd90 100644 --- a/src/app/utils/sqs.ts +++ b/src/app/utils/sqs.ts @@ -16,19 +16,20 @@ const sqsClient = new SQSClient({ }, }); -let cachedQueueUrl: string | null = null; +const queueUrlCache = new Map(); // Export if you want to reuse elsewhere export async function getQueueUrl(queueName: string): Promise { - if (cachedQueueUrl) return cachedQueueUrl; + const cached = queueUrlCache.get(queueName); + if (cached) return cached; const resp = await sqsClient.send( new GetQueueUrlCommand({ QueueName: queueName }) ); if (!resp.QueueUrl) throw new Error(`Could not resolve SQS URL for queue: ${queueName}`); - cachedQueueUrl = resp.QueueUrl; - return cachedQueueUrl; + queueUrlCache.set(queueName, resp.QueueUrl); + return resp.QueueUrl; } type SendOptions = { diff --git a/src/lib/bulkUpload/client.ts b/src/lib/bulkUpload/client.ts new file mode 100644 index 00000000..536bd07f --- /dev/null +++ b/src/lib/bulkUpload/client.ts @@ -0,0 +1,164 @@ +"use client"; + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { bulkUploadKeys } from "./keys"; +import { isTerminalStatus, type BulkUpload, type ProgressView } from "./types"; + +async function parseError(res: Response, fallback: string): Promise { + const body = await res.json().catch(() => ({})); + return new Error(body?.error ?? fallback); +} + +export type CreateBulkUploadInput = { + file: File; + portfolioId: string; + userId: string; + sourceHeaders: string[]; + contentType: string; + fileKey: string; + onProgress?: (percent: number) => void; +}; + +export type CreateBulkUploadResult = { + id: string; + s3Key: string; + s3Bucket: string; + status: string; +}; + +export function useCreateBulkUpload() { + return useMutation({ + mutationFn: async (input) => { + const presignRes = await fetch("/api/upload/bulk-addresses", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + userId: input.userId, + portfolioId: input.portfolioId, + fileKey: input.fileKey, + contentType: input.contentType, + }), + }); + if (!presignRes.ok) throw await parseError(presignRes, "Failed to generate upload URL."); + const { url: presignedUrl } = await presignRes.json(); + + await new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open("PUT", presignedUrl); + xhr.setRequestHeader("Content-Type", input.contentType); + if (input.onProgress) { + xhr.upload.addEventListener("progress", (e) => { + if (e.lengthComputable) { + input.onProgress!(Math.round((e.loaded / e.total) * 100)); + } + }); + } + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) resolve(); + else reject(new Error(`S3 upload failed: ${xhr.status}`)); + }; + xhr.onerror = () => reject(new Error("Network error during upload")); + xhr.send(input.file); + }); + + const confirmRes = await fetch("/api/upload/bulk-addresses/confirm", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + fileKey: input.fileKey, + filename: input.file.name, + portfolioId: input.portfolioId, + userId: input.userId, + sourceHeaders: input.sourceHeaders, + }), + }); + if (!confirmRes.ok) throw await parseError(confirmRes, "Failed to record upload."); + return confirmRes.json(); + }, + }); +} + +export function useSetColumnMapping(portfolioId: string, uploadId: string) { + const queryClient = useQueryClient(); + return useMutation }>({ + mutationFn: async (input) => { + const res = await fetch(`/api/portfolio/${portfolioId}/bulk-uploads/${uploadId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(input), + }); + if (!res.ok) throw await parseError(res, "Failed to save mapping."); + 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>({ + mutationFn: async () => { + const res = await fetch( + `/api/portfolio/${portfolioId}/bulk-uploads/${uploadId}/start-address-matching`, + { method: "POST" }, + ); + if (!res.ok) throw await parseError(res, "Failed to start address matching."); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: bulkUploadKeys.progress(uploadId) }); + }, + }); +} + +export function useBulkUploadProgress(portfolioId: string, uploadId: string) { + return useQuery({ + queryKey: bulkUploadKeys.progress(uploadId), + queryFn: async () => { + const res = await fetch( + `/api/portfolio/${portfolioId}/bulk-uploads/${uploadId}/progress`, + ); + if (!res.ok) throw await parseError(res, "Failed to load progress."); + return res.json(); + }, + refetchInterval: (data) => { + const status = data?.upload.status; + return status && isTerminalStatus(status) ? false : 3000; + }, + }); +} + +export function useRequestCombine(portfolioId: string, uploadId: string) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async () => { + const res = await fetch( + `/api/portfolio/${portfolioId}/bulk-uploads/${uploadId}/combine`, + { method: "POST" }, + ); + if (!res.ok) throw await parseError(res, "Failed to start combiner."); + return res.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: bulkUploadKeys.progress(uploadId) }); + }, + }); +} + +export function useFinalize(portfolioId: string, uploadId: string) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async () => { + const res = await fetch( + `/api/portfolio/${portfolioId}/bulk-uploads/${uploadId}/finalize`, + { method: "POST" }, + ); + if (!res.ok) throw await parseError(res, "Finalize failed."); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: bulkUploadKeys.progress(uploadId) }); + }, + }); +} diff --git a/src/lib/bulkUpload/keys.ts b/src/lib/bulkUpload/keys.ts new file mode 100644 index 00000000..1f77cb93 --- /dev/null +++ b/src/lib/bulkUpload/keys.ts @@ -0,0 +1,4 @@ +export const bulkUploadKeys = { + all: ["bulkUpload"] as const, + progress: (uploadId: string) => ["bulkUpload", uploadId, "progress"] as const, +}; diff --git a/src/lib/bulkUpload/server.ts b/src/lib/bulkUpload/server.ts new file mode 100644 index 00000000..a5785c39 --- /dev/null +++ b/src/lib/bulkUpload/server.ts @@ -0,0 +1,273 @@ +import { db } from "@/app/db/db"; +import { bulkAddressUploads } from "@/app/db/schema/bulk_address_uploads"; +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"; + +const REMAP_ALLOWED: ReadonlySet = new Set([ + "ready_for_processing", + "mapping_complete", +]); + +type FastApiTriggerArgs = { + endpoint: string; + payload: Record; + sessionToken: string | undefined; +}; + +type FastApiTriggerResult = { ok: true } | { ok: false; status: number; message: string }; + +async function triggerFastApiPipeline(args: FastApiTriggerArgs): Promise { + const url = process.env.FASTAPI_API_URL; + const key = process.env.FASTAPI_API_KEY; + if (!url || !key) { + console.error("FASTAPI_API_URL or FASTAPI_API_KEY not set"); + return { ok: false, status: 500, message: "Server misconfiguration" }; + } + + try { + const res = await fetch(`${url}${args.endpoint}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": key, + Authorization: `Bearer ${args.sessionToken}`, + }, + body: JSON.stringify(args.payload), + }); + if (!res.ok) { + const errText = await res.text().catch(() => ""); + console.error(`FastAPI ${args.endpoint} failed:`, res.status, errText); + return { ok: false, status: 502, message: "Pipeline trigger failed" }; + } + return { ok: true }; + } catch (err) { + console.error(`Failed to reach FastAPI ${args.endpoint}:`, err); + return { ok: false, status: 502, message: "Pipeline trigger failed" }; + } +} + +async function loadById(uploadId: string): Promise { + const [row] = await db + .select() + .from(bulkAddressUploads) + .where(eq(bulkAddressUploads.id, uploadId)) + .limit(1); + return row ?? null; +} + +export async function listForPortfolio(portfolioId: string): Promise { + return db + .select() + .from(bulkAddressUploads) + .where(eq(bulkAddressUploads.portfolioId, portfolioId)) + .orderBy(desc(bulkAddressUploads.createdAt)); +} + +async function loadTaskSummary(taskId: string): Promise { + const [row] = await db + .select({ + id: tasks.id, + taskSource: tasks.taskSource, + status: tasks.status, + service: tasks.service, + jobStarted: tasks.jobStarted, + jobCompleted: tasks.jobCompleted, + updatedAt: tasks.updatedAt, + 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`, + }) + .from(tasks) + .leftJoin(subTasks, eq(subTasks.taskId, tasks.id)) + .where(eq(tasks.id, taskId)) + .groupBy(tasks.id) + .limit(1); + return row ?? null; +} + +export async function getProgressView(uploadId: string): Promise { + const upload = await loadById(uploadId); + if (!upload) return null; + const task = upload.taskId ? await loadTaskSummary(upload.taskId) : null; + return { upload, task }; +} + +function validateMapping(mapping: Record): 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" } + | { kind: "invalid_status"; current: string } + | { kind: "invalid_mapping"; reason: string }; + +export async function setColumnMapping( + uploadId: string, + mapping: Record, +): Promise { + const upload = await loadById(uploadId); + if (!upload) return { kind: "not_found" }; + if (!REMAP_ALLOWED.has(upload.status as BulkUploadStatus)) + return { kind: "invalid_status", current: upload.status }; + + const reason = validateMapping(mapping); + if (reason) return { kind: "invalid_mapping", reason }; + + const [updated] = await db + .update(bulkAddressUploads) + .set({ columnMapping: mapping, status: "mapping_complete" }) + .where(eq(bulkAddressUploads.id, uploadId)) + .returning(); + if (!updated) return { kind: "not_found" }; + return { kind: "ok", upload: updated }; +} + +export type LoadForAddressMatchingOutcome = + | { kind: "ok"; upload: BulkUpload } + | { kind: "not_found" } + | { kind: "wrong_state"; current: string } + | { kind: "missing_mapping" }; + +export async function loadForAddressMatching( + uploadId: string, +): Promise { + const upload = await loadById(uploadId); + if (!upload) return { kind: "not_found" }; + if (upload.status !== "mapping_complete") + return { kind: "wrong_state", current: upload.status }; + if (!upload.columnMapping) return { kind: "missing_mapping" }; + return { kind: "ok", upload }; +} + +export type TriggerAddressMatchingOutcome = + | { kind: "ok"; taskId: string } + | { kind: "trigger_failed"; status: number; message: string }; + +export async function triggerAddressMatching(args: { + uploadId: string; + s3Uri: string; + sessionToken: string | undefined; +}): Promise { + const upload = await loadById(args.uploadId); + if (!upload) return { kind: "trigger_failed", status: 404, message: "Upload not found" }; + + const now = new Date(); + const [task] = await db + .insert(tasks) + .values({ + taskSource: `Address Onboarding – ${upload.filename}`, + service: "address2uprn", + source: "portfolio_id", + sourceId: upload.portfolioId, + status: "waiting", + jobStarted: now, + }) + .returning(); + const [subTask] = await db + .insert(subTasks) + .values({ + taskId: task.id, + status: "waiting", + inputs: JSON.stringify({ bulk_upload_id: args.uploadId }), + }) + .returning(); + + const payload = { + task_id: task.id, + sub_task_id: subTask.id, + s3_uri: args.s3Uri, + }; + + const trigger = await triggerFastApiPipeline({ + endpoint: "/v1/bulk-uploads/trigger-postcode-splitter", + payload, + sessionToken: args.sessionToken, + }); + if (!trigger.ok) + return { kind: "trigger_failed", status: trigger.status, message: trigger.message }; + + await Promise.all([ + db + .update(bulkAddressUploads) + .set({ status: "processing", taskId: task.id }) + .where(eq(bulkAddressUploads.id, args.uploadId)), + db + .update(tasks) + .set({ status: "in progress" }) + .where(eq(tasks.id, task.id)), + db + .update(subTasks) + .set({ inputs: JSON.stringify(payload) }) + .where(eq(subTasks.id, subTask.id)), + ]); + return { kind: "ok", taskId: task.id }; +} + +export type CombineRetriggerOutcome = + | { kind: "triggered"; taskId: string; subTaskId: string } + | { kind: "already_combined" } + | { kind: "not_found" } + | { kind: "missing_task" } + | { kind: "trigger_failed"; status: number; message: string }; + +export async function requestCombineRetrigger(args: { + uploadId: string; + sessionToken: string | undefined; +}): Promise { + const upload = await loadById(args.uploadId); + if (!upload) return { kind: "not_found" }; + if (!upload.taskId) return { kind: "missing_task" }; + if (upload.combinedOutputS3Uri) return { kind: "already_combined" }; + + const [subTask] = await db + .insert(subTasks) + .values({ taskId: upload.taskId, status: "waiting" }) + .returning(); + + const payload = { task_id: upload.taskId, sub_task_id: subTask.id }; + + const trigger = await triggerFastApiPipeline({ + endpoint: "/v1/bulk-uploads/trigger-combiner", + payload, + sessionToken: args.sessionToken, + }); + if (!trigger.ok) + return { kind: "trigger_failed", status: trigger.status, message: trigger.message }; + + await db + .update(subTasks) + .set({ inputs: JSON.stringify(payload) }) + .where(eq(subTasks.id, subTask.id)); + + return { kind: "triggered", taskId: upload.taskId, subTaskId: subTask.id }; +} + +export type LoadForFinalizeOutcome = + | { kind: "ready"; upload: BulkUpload } + | { kind: "already_finalized" } + | { kind: "not_found" } + | { kind: "not_yet_combined" } + | { kind: "wrong_state"; current: string }; + +export async function loadForFinalize(uploadId: string): Promise { + const upload = await loadById(uploadId); + if (!upload) return { kind: "not_found" }; + if (upload.status === "complete") return { kind: "already_finalized" }; + if (upload.status !== "awaiting_review") + return { kind: "wrong_state", current: upload.status }; + if (!upload.combinedOutputS3Uri) return { kind: "not_yet_combined" }; + return { kind: "ready", upload }; +} + +export async function markFinalized(uploadId: string): Promise { + await db + .update(bulkAddressUploads) + .set({ status: "complete" }) + .where(eq(bulkAddressUploads.id, uploadId)); +} diff --git a/src/lib/bulkUpload/types.ts b/src/lib/bulkUpload/types.ts new file mode 100644 index 00000000..c2b1cb63 --- /dev/null +++ b/src/lib/bulkUpload/types.ts @@ -0,0 +1,42 @@ +import type { bulkAddressUploads } from "@/app/db/schema/bulk_address_uploads"; + +export const BULK_UPLOAD_STATUSES = [ + "ready_for_processing", + "mapping_complete", + "processing", + "combining", + "awaiting_review", + "complete", + "failed", +] as const; + +export type BulkUploadStatus = (typeof BULK_UPLOAD_STATUSES)[number]; + +export type BulkUpload = typeof bulkAddressUploads.$inferSelect; + +export type TaskSummary = { + id: string; + taskSource: string; + status: string; + service: string | null; + jobStarted: Date | null; + jobCompleted: Date | null; + updatedAt: Date; + totalSubtasks: number; + completedSubtasks: number; + failedSubtasks: number; +}; + +export type ProgressView = { + upload: BulkUpload; + task: TaskSummary | null; +}; + +const TERMINAL_UPLOAD_STATUSES: ReadonlySet = new Set([ + "complete", + "failed", +]); + +export function isTerminalStatus(status: string): boolean { + return TERMINAL_UPLOAD_STATUSES.has(status as BulkUploadStatus); +} diff --git a/src/lib/session.ts b/src/lib/session.ts new file mode 100644 index 00000000..92f04ddf --- /dev/null +++ b/src/lib/session.ts @@ -0,0 +1,8 @@ +import { NextRequest } from "next/server"; + +export function readSessionToken(request: NextRequest): string | undefined { + return ( + request.cookies.get("__Secure-next-auth.session-token")?.value ?? + request.cookies.get("next-auth.session-token")?.value + ); +}