mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
No filters resolves the whole portfolio minus deletions 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b008bcc6ba
commit
84fb9884d6
7 changed files with 101 additions and 0 deletions
0
backend/app/modelling/__init__.py
Normal file
0
backend/app/modelling/__init__.py
Normal file
44
backend/app/modelling/property_filters.py
Normal file
44
backend/app/modelling/property_filters.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""Resolve a Modelling Run's property-group filters to a concrete property set.
|
||||
|
||||
The resolution rule is a shared contract with the app's preview (ADR-0056):
|
||||
the "N properties will be modelled" count the user approves is computed by the
|
||||
Next.js app from this same rule, so any change here must land in both
|
||||
codebases and amend the ADR.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from sqlmodel import Session
|
||||
|
||||
|
||||
class PropertyGroupFilters(BaseModel):
|
||||
"""The filters key of a trigger-run request. An absent key is
|
||||
unconstrained; present keys combine with AND."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
postcodes: Optional[list[str]] = None
|
||||
property_types: Optional[list[str]] = None
|
||||
built_forms: Optional[list[str]] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FilteredProperty:
|
||||
"""One property the filters resolved: the id to model and the postcode the
|
||||
distributor batches by (postcode grouping keeps the workers' prediction
|
||||
cohort cache effective)."""
|
||||
|
||||
property_id: int
|
||||
postcode: Optional[str]
|
||||
|
||||
|
||||
def resolve_filtered_property_ids(
|
||||
session: Session, portfolio_id: int, filters: PropertyGroupFilters
|
||||
) -> list[FilteredProperty]:
|
||||
"""Resolve *filters* against *portfolio_id* per ADR-0056: base set is the
|
||||
portfolio's rows with ``marked_for_deletion = false``; property_type /
|
||||
built_form resolve override → lodged EPC → predicted EPC → legacy columns
|
||||
→ "Unknown"."""
|
||||
raise NotImplementedError
|
||||
|
|
@ -56,3 +56,11 @@ class PropertyRow(SQLModel, table=True):
|
|||
# `updated_at >= 2026-06-01`, the cutoff the old pipeline predates).
|
||||
has_recommendations: Optional[bool] = Field(default=None)
|
||||
updated_at: Optional[datetime] = Field(default=None)
|
||||
|
||||
# Columns the Modelling Run filter resolution reads (ADR-0056): the base-set
|
||||
# exclusion flag and the legacy type/form columns (the last resort before
|
||||
# "Unknown" in the resolution precedence). NOT NULL / defaulted in the FE
|
||||
# schema; nullable mirrors are safe — prod enforces the real constraints.
|
||||
marked_for_deletion: Optional[bool] = Field(default=None)
|
||||
property_type: Optional[str] = Field(default=None)
|
||||
built_form: Optional[str] = Field(default=None)
|
||||
|
|
|
|||
0
tests/backend/__init__.py
Normal file
0
tests/backend/__init__.py
Normal file
0
tests/backend/app/__init__.py
Normal file
0
tests/backend/app/__init__.py
Normal file
0
tests/backend/app/modelling/__init__.py
Normal file
0
tests/backend/app/modelling/__init__.py
Normal file
49
tests/backend/app/modelling/test_property_filters.py
Normal file
49
tests/backend/app/modelling/test_property_filters.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from sqlalchemy import Engine
|
||||
from sqlmodel import Session
|
||||
|
||||
from backend.app.modelling.property_filters import (
|
||||
FilteredProperty,
|
||||
PropertyGroupFilters,
|
||||
resolve_filtered_property_ids,
|
||||
)
|
||||
from infrastructure.postgres.property_table import PropertyRow
|
||||
|
||||
|
||||
def _seed_property(
|
||||
session: Session,
|
||||
*,
|
||||
portfolio_id: int,
|
||||
postcode: str = "B93 8SU",
|
||||
marked_for_deletion: bool = False,
|
||||
) -> int:
|
||||
row = PropertyRow(
|
||||
portfolio_id=portfolio_id,
|
||||
postcode=postcode,
|
||||
marked_for_deletion=marked_for_deletion,
|
||||
)
|
||||
session.add(row)
|
||||
session.commit()
|
||||
session.refresh(row)
|
||||
assert row.id is not None
|
||||
return row.id
|
||||
|
||||
|
||||
def test_no_filters_resolves_the_whole_portfolio_minus_deletions(
|
||||
db_engine: Engine,
|
||||
) -> None:
|
||||
# arrange
|
||||
with Session(db_engine) as session:
|
||||
kept = _seed_property(session, portfolio_id=814, postcode="B93 8SU")
|
||||
deleted = _seed_property(
|
||||
session, portfolio_id=814, postcode="M20 4TF", marked_for_deletion=True
|
||||
)
|
||||
_other_portfolio = _seed_property(session, portfolio_id=999)
|
||||
|
||||
# act
|
||||
resolved = resolve_filtered_property_ids(
|
||||
session, portfolio_id=814, filters=PropertyGroupFilters()
|
||||
)
|
||||
|
||||
# assert
|
||||
assert resolved == [FilteredProperty(property_id=kept, postcode="B93 8SU")]
|
||||
assert deleted not in [p.property_id for p in resolved]
|
||||
Loading…
Add table
Reference in a new issue