mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
Merge pull request #276 from Hestia-Homes/faeture/landlord_overrides
Some checks are pending
Test Suite / unit-tests (push) Waiting to run
Some checks are pending
Test Suite / unit-tests (push) Waiting to run
Faeture/landlord overrides
This commit is contained in:
commit
41d33ff399
5 changed files with 9773 additions and 0 deletions
15
CONTEXT.md
15
CONTEXT.md
|
|
@ -31,6 +31,20 @@ _Avoid_: aggregator, merger
|
|||
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
|
||||
|
||||
### Landlord overrides
|
||||
|
||||
**Landlord**:
|
||||
The housing association supplying a Portfolio's BulkUploads. A Landlord knows facts about their properties that EPC data doesn't (e.g. that a cavity has been filled), and those facts take precedence when computing an assessment.
|
||||
_Avoid_: customer, client, owner, organisation (Organisation is a separate, broader entity)
|
||||
|
||||
**Landlord override**:
|
||||
A landlord-supplied fact about a property that takes precedence over EPC-derived defaults when computing an assessment. The end-to-end Landlord override journey has two layers — a **VocabularyMapping** layer (this glossary entry below) and a per-Property fact layer (not yet modelled).
|
||||
_Avoid_: customer data, manual override, landlord data
|
||||
|
||||
**VocabularyMapping**:
|
||||
The translation from a Landlord's free-text description in a BulkUpload column (e.g. `"cavity: filledcavity"`) to a canonical domain enum value (e.g. `WallType.CAVITY`). Produced by a `ColumnClassifier` (today an LLM, tomorrow possibly a lookup table or rules engine) in the Model service. Stored per-Portfolio, one row per `(category, description)`. A row carries provenance (`classifier` or `user`) so user overrides survive re-classification.
|
||||
_Avoid_: column mapping (that's a separate concept — see `ColumnMapping` above), classification, dictionary
|
||||
|
||||
## Lifecycle
|
||||
|
||||
A **BulkUpload** moves through these statuses:
|
||||
|
|
@ -58,6 +72,7 @@ See [ADR-0001](./docs/adr/0001-bulk-upload-state-machine.md) for the deliberate
|
|||
- 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).
|
||||
- A **Portfolio** has many **VocabularyMappings** — one row per `(category, description)` it has ever encountered across all its BulkUploads. See [ADR-0002](./docs/adr/0002-landlord-override-vocabulary.md).
|
||||
|
||||
## Example dialogue
|
||||
|
||||
|
|
|
|||
33
src/app/db/migrations/0205_wonderful_pixie.sql
Normal file
33
src/app/db/migrations/0205_wonderful_pixie.sql
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
-- ENUM definition
|
||||
|
||||
CREATE TYPE "public"."override_source" AS ENUM('classifier', 'user');--> statement-breakpoint
|
||||
CREATE TYPE "public"."property_type" AS ENUM('House', 'Bungalow', 'Flat', 'Maisonette', 'Park home', 'Unknown');--> statement-breakpoint
|
||||
CREATE TYPE "public"."wall_type" AS ENUM('Cavity', 'Solid Brick', 'Timber frame', 'Sandstone', 'Unknown');--> statement-breakpoint
|
||||
|
||||
-- Lanlord Property Type overides
|
||||
CREATE TABLE "landlord_property_type_overrides" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"portfolio_id" bigint NOT NULL,
|
||||
"description" text NOT NULL,
|
||||
"value" "property_type" NOT NULL,
|
||||
"source" "override_source" NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "landlord_property_type_overrides_portfolio_description_unique" UNIQUE("portfolio_id","description")
|
||||
);
|
||||
|
||||
-- Lanlodrd Wall type overrides
|
||||
CREATE TABLE "landlord_wall_type_overrides" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"portfolio_id" bigint NOT NULL,
|
||||
"description" text NOT NULL,
|
||||
"value" "wall_type" NOT NULL,
|
||||
"source" "override_source" NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "landlord_wall_type_overrides_portfolio_description_unique" UNIQUE("portfolio_id","description")
|
||||
);
|
||||
|
||||
-- Add portfolio_id, so we know which portfolio its from
|
||||
ALTER TABLE "landlord_property_type_overrides" ADD CONSTRAINT "landlord_property_type_overrides_portfolio_id_portfolio_id_fk" FOREIGN KEY ("portfolio_id") REFERENCES "public"."portfolio"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "landlord_wall_type_overrides" ADD CONSTRAINT "landlord_wall_type_overrides_portfolio_id_portfolio_id_fk" FOREIGN KEY ("portfolio_id") REFERENCES "public"."portfolio"("id") ON DELETE cascade ON UPDATE no action;
|
||||
9626
src/app/db/migrations/meta/0205_snapshot.json
Normal file
9626
src/app/db/migrations/meta/0205_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1436,6 +1436,13 @@
|
|||
"when": 1779092260406,
|
||||
"tag": "0204_bizarre_black_bird",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 205,
|
||||
"version": "7",
|
||||
"when": 1779791011744,
|
||||
"tag": "0205_wonderful_pixie",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
92
src/app/db/schema/landlord_overrides.ts
Normal file
92
src/app/db/schema/landlord_overrides.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import {
|
||||
bigint,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
unique,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { portfolio } from "./portfolio";
|
||||
|
||||
// Enum string values mirror /workspaces/home/github/Model/domain/sal/*.py
|
||||
// exactly (PropertyType.value, WallType.value). Keep in sync — see
|
||||
// docs/adr/0002-landlord-override-vocabulary.md.
|
||||
export const PropertyTypeValues: [string, ...string[]] = [
|
||||
"House",
|
||||
"Bungalow",
|
||||
"Flat",
|
||||
"Maisonette",
|
||||
"Park home",
|
||||
"Unknown",
|
||||
];
|
||||
|
||||
export const WallTypeValues: [string, ...string[]] = [
|
||||
"Cavity",
|
||||
"Solid Brick",
|
||||
"Timber frame",
|
||||
"Sandstone",
|
||||
"Unknown",
|
||||
];
|
||||
|
||||
export const OverrideSourceValues: [string, ...string[]] = [
|
||||
"classifier",
|
||||
"user",
|
||||
];
|
||||
|
||||
export const propertyTypeEnum = pgEnum("property_type", PropertyTypeValues);
|
||||
export const wallTypeEnum = pgEnum("wall_type", WallTypeValues);
|
||||
export const overrideSourceEnum = pgEnum(
|
||||
"override_source",
|
||||
OverrideSourceValues,
|
||||
);
|
||||
|
||||
export const landlordPropertyTypeOverrides = pgTable(
|
||||
"landlord_property_type_overrides",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
portfolioId: bigint("portfolio_id", { mode: "bigint" })
|
||||
.notNull()
|
||||
.references(() => portfolio.id, { onDelete: "cascade" }),
|
||||
description: text("description").notNull(),
|
||||
value: propertyTypeEnum("value").notNull(),
|
||||
source: overrideSourceEnum("source").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date()),
|
||||
},
|
||||
(table) => ({
|
||||
portfolioDescriptionUnique: unique(
|
||||
"landlord_property_type_overrides_portfolio_description_unique",
|
||||
).on(table.portfolioId, table.description),
|
||||
}),
|
||||
);
|
||||
|
||||
export const landlordWallTypeOverrides = pgTable(
|
||||
"landlord_wall_type_overrides",
|
||||
{
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
portfolioId: bigint("portfolio_id", { mode: "bigint" })
|
||||
.notNull()
|
||||
.references(() => portfolio.id, { onDelete: "cascade" }),
|
||||
description: text("description").notNull(),
|
||||
value: wallTypeEnum("value").notNull(),
|
||||
source: overrideSourceEnum("source").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date()),
|
||||
},
|
||||
(table) => ({
|
||||
portfolioDescriptionUnique: unique(
|
||||
"landlord_wall_type_overrides_portfolio_description_unique",
|
||||
).on(table.portfolioId, table.description),
|
||||
}),
|
||||
);
|
||||
Loading…
Add table
Reference in a new issue