mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-19 17:03:02 +00:00
The new pipeline left no per-Property record of a run (the old engine set property.has_recommendations and populated property_details_epc). Restore the marker: PropertyRepository.mark_modelled sets has_recommendations (true when the Plan carries measures, mirroring the old engine) and bumps updated_at, so a first-run under the new process is identifiable as updated_at >= 2026-06-01. ModellingOrchestrator marks each Property after its Scenarios (true if any Scenario yielded a measure); run_modelling_e2e's --persist path marks it too (its compute runs on in-memory fakes, so the DB UoW sets it directly). Adds the has_recommendations/updated_at columns to the PropertyRow mirror. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
76 lines
3 KiB
Python
76 lines
3 KiB
Python
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
|
|
from domain.property.properties import Properties
|
|
from domain.property.property import Property
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PropertyIdentityInsert:
|
|
"""One row inserted into the FE-owned ``property`` table at Finalise (ADR-0013).
|
|
|
|
Mirrors the exact column set today's Next.js ``/finalize`` writes: nine fields
|
|
plus ``creation_status='READY'``. ``address``/``postcode`` are the resolved
|
|
(matched ?? user-inputted) values and may be ``None``.
|
|
"""
|
|
|
|
portfolio_id: int
|
|
uprn: Optional[int]
|
|
landlord_property_id: Optional[str]
|
|
address: Optional[str]
|
|
postcode: Optional[str]
|
|
user_inputted_address: Optional[str]
|
|
user_inputted_postcode: Optional[str]
|
|
lexiscore: Optional[float]
|
|
creation_status: str = "READY"
|
|
|
|
|
|
class PropertyRepository(ABC):
|
|
"""Reads and writes the FE-owned ``property`` table.
|
|
|
|
Reads hydrate the Property aggregate whole — never half a Property — from the
|
|
identity row plus its source-data slices (EPC today; Site Notes / enrichments
|
|
as later slices land) (ADR-0002, ADR-0012). Writes bulk-insert identity rows at
|
|
Finalise (ADR-0013). One repository per aggregate.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def get(self, property_id: int) -> Property: ...
|
|
|
|
@abstractmethod
|
|
def get_many(self, property_ids: list[int]) -> Properties:
|
|
"""Load a batch of Properties whole, in a handful of per-table queries
|
|
rather than one round-trip per property (ADR-0012). Order follows the
|
|
input ids."""
|
|
...
|
|
|
|
@abstractmethod
|
|
def mark_modelled(self, property_id: int, *, has_recommendations: bool) -> None:
|
|
"""Record that a Property has been run through the modelling pipeline:
|
|
set ``has_recommendations`` (the old engine's per-Property marker — true
|
|
when the Plan carries measures) and bump ``updated_at`` so the run is
|
|
datable (a first-run under the new process is ``updated_at >=
|
|
2026-06-01``). Idempotent — re-running overwrites the same row."""
|
|
...
|
|
|
|
@abstractmethod
|
|
def insert_all(self, rows: list[PropertyIdentityInsert]) -> int:
|
|
"""Bulk-insert identity rows, skipping any whose ``(portfolio_id, uprn)``
|
|
already exists (the FE partial unique index, ``WHERE uprn IS NOT NULL``).
|
|
Rows with no UPRN are always inserted. Returns the number actually
|
|
inserted; an empty list is a no-op returning 0 (ADR-0013)."""
|
|
...
|
|
|
|
@abstractmethod
|
|
def ids_by_uprn(self, portfolio_id: int, uprns: list[int]) -> dict[int, int]:
|
|
"""Map each given UPRN to its ``property.id`` within a portfolio.
|
|
|
|
Used by the finaliser to attach ``property_overrides`` to UPRN-matched
|
|
rows (ADR-0006) without a query per row — covers both rows just inserted
|
|
and pre-existing ones (re-found by ``(portfolio_id, uprn)``). UPRNs with
|
|
no matching property are simply absent from the result; an empty input is
|
|
a no-op returning ``{}``."""
|
|
...
|