No filters resolves the whole portfolio minus deletions 🟥

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-07 11:32:10 +00:00
parent b008bcc6ba
commit 84fb9884d6
7 changed files with 101 additions and 0 deletions

View file

View 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

View file

@ -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)

View file

View file

View file

View 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]