refactor(property-baseline): rename baseline → property_baseline aggregate (PR #1139 review)

Wholesale rename of the Baseline aggregate to PropertyBaseline for clarity /
to disambiguate from baselines that appear elsewhere in Modelling. Scoped to
this aggregate only — the distinct Rebaselining term (rebaseline_reason,
StubRebaseliner, RebaselineNotImplemented) is deliberately untouched.

- domain/baseline → domain/property_baseline; BaselinePerformance →
  PropertyBaselinePerformance.
- repositories/baseline → repositories/property_baseline; BaselineRepository
  / BaselinePostgresRepository → PropertyBaseline*.
- orchestration/baseline_orchestrator.py → property_baseline_orchestrator.py;
  BaselineOrchestrator → PropertyBaselineOrchestrator. BaselineStage →
  PropertyBaselineStage.
- infrastructure/postgres: baseline_performance_table.py →
  property_baseline_performance_table.py; table `baseline_performance` →
  `property_baseline_performance`; Model renamed.
- UnitOfWork attribute `.baseline` → `.property_baseline`.
- Docs: ADR-0004 references + migration doc (renamed to
  property-baseline-performance-table.md) updated.

CONTEXT.md glossary term ("Baseline Performance") left as-is pending a
ubiquitous-language call (raised on the PR). 123 tests pass; pyright strict
clean (only the unrelated pre-existing moto import errors remain).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-01 14:54:59 +00:00
parent 8685f8ba3a
commit c3691d9af2
25 changed files with 148 additions and 148 deletions

View file

@ -10,10 +10,10 @@ from sqlmodel import Session
from applications.ara_first_run.ara_first_run_trigger_body import (
AraFirstRunTriggerBody,
)
from domain.baseline.rebaseliner import StubRebaseliner
from domain.property_baseline.rebaseliner import StubRebaseliner
from infrastructure.postgres.config import PostgresConfig
from infrastructure.postgres.engine import make_engine
from orchestration.baseline_orchestrator import BaselineOrchestrator
from orchestration.property_baseline_orchestrator import PropertyBaselineOrchestrator
from orchestration.first_run_pipeline import FirstRunPipeline
from orchestration.ingestion_orchestrator import (
EpcFetcher,
@ -78,7 +78,7 @@ def build_first_run_pipeline(
geospatial_repo=geospatial_repo,
solar_fetcher=solar_fetcher,
),
baseline=BaselineOrchestrator(
baseline=PropertyBaselineOrchestrator(
unit_of_work=unit_of_work,
rebaseliner=StubRebaseliner(),
),

View file

@ -1,27 +1,27 @@
# `BaselinePerformance` stores both lodged and effective values
# `PropertyBaselinePerformance` stores both lodged and effective values
A Property's current performance has two states we care about: the rating that was lodged on the government register (the "lodged" SAP / band / carbon / heat) and the rating produced by the modelling pipeline against the current Effective EPC (the "effective" values, which may have been rebaselined by ML when the EPC was pre-SAP10 or when Landlord Overrides / Site Notes changed physical state). We considered storing a single set of values — the rebaselined-if-needed-otherwise-lodged figures — and rejected that. Both are stored as a pair on every `BaselinePerformance`, equal when no rebaselining trigger fires.
A Property's current performance has two states we care about: the rating that was lodged on the government register (the "lodged" SAP / band / carbon / heat) and the rating produced by the modelling pipeline against the current Effective EPC (the "effective" values, which may have been rebaselined by ML when the EPC was pre-SAP10 or when Landlord Overrides / Site Notes changed physical state). We considered storing a single set of values — the rebaselined-if-needed-otherwise-lodged figures — and rejected that. Both are stored as a pair on every `PropertyBaselinePerformance`, equal when no rebaselining trigger fires.
The pair lets the FE show "this is what the gov register says vs this is the SAP10-equivalent we modelled against" side by side without a second query, and keeps the audit trail clean: a user looking at a property's plan can see exactly which figure drove the recommendation pipeline. Storing only one set forces a downstream consumer to recompute the missing one from raw EPC fields when it needs both, which is the kind of derivation creep we want to keep out of the FE.
The cost is a wider row + the discipline that **every** `BaselinePerformance` populates both halves, even when they're equal. Annual kWh, fuel split and bills are not paired — they are always derived deterministically by `EpcEnergyDerivationService` against the Effective state, because the EPC's recorded cost fields use fuel rates pinned to the inspection date and the UCL correction depends on the modelled band.
The cost is a wider row + the discipline that **every** `PropertyBaselinePerformance` populates both halves, even when they're equal. Annual kWh, fuel split and bills are not paired — they are always derived deterministically by `EpcEnergyDerivationService` against the Effective state, because the EPC's recorded cost fields use fuel rates pinned to the inspection date and the UCL correction depends on the modelled band.
## Consequences
- Reversing this means rewriting every consumer that has learned to read both values. Hard to roll back once the FE depends on the pair.
- The rebaseline trigger has two reasons (`pre_sap10`, `physical_state_changed`, or `both`) — store the reason alongside so we know *why* a property was rebaselined when debugging.
### Amendment (2026-05-30, #1135): standalone `baseline_performance` table
### Amendment (2026-05-30, #1135): standalone `property_baseline_performance` table
The original consequence read *"`property_details_epc` (or its successor) carries 8 fields
instead of 4 for the SAP-equivalent block"* — i.e. the pair as columns on the EPC-details table.
That is superseded. `property_details_epc` is being **retired**: it is too tightly coupled to the
schema of the legacy EPC API, which the Ara rebuild is moving off. So the pair has no home there.
`BaselinePerformance` instead persists as its **own standalone `baseline_performance` table, one
row per Property**, behind a dedicated `BaselineRepository` port (`save` / `get_for_property`),
`PropertyBaselinePerformance` instead persists as its **own standalone `property_baseline_performance` table, one
row per Property**, behind a dedicated `PropertyBaselineRepository` port (`save` / `get_for_property`),
mirroring the EPC slice's repo shape. This is the cleaner model regardless of the retirement:
`BaselinePerformance` is its own aggregate (a Property's current performance), not a detail of any
`PropertyBaselinePerformance` is its own aggregate (a Property's current performance), not a detail of any
single EPC.
The row is **flat typed columns**, not a JSONB blob, because the FE both surfaces the block and

View file

@ -1,8 +1,8 @@
# `baseline_performance` table — FE-owned migration
# `property_baseline_performance` table — FE-owned migration
**Context:** Slice 6 (Hestia-Homes/Model#1135) of the `ara_first_run` rebuild. The
`BaselineOrchestrator` establishes a Property's **Baseline Performance** (ADR-0004) and persists it
via a new `BaselineRepository` port. This is a brand-new table — no predecessor.
`PropertyBaselineOrchestrator` establishes a Property's **Baseline Performance** (ADR-0004) and persists it
via a new `PropertyBaselineRepository` port. This is a brand-new table — no predecessor.
Per ADR-0004's amendment, the lodged/effective pair does **not** land on `property_details_epc`
(which is being retired as too coupled to the legacy EPC-API schema). It lands here, as its own
@ -12,7 +12,7 @@ The SQLModel row is defined in `infrastructure/postgres/` so the ephemeral-Postg
via `SQLModel.metadata.create_all`. The **production migration is FE-owned (Drizzle ORM)** — a
straight lift-and-shift of the columns below.
## `baseline_performance` — one row per Property
## `property_baseline_performance` — one row per Property
| Column | Type | Notes |
|---|---|---|

View file

@ -2,12 +2,12 @@ from __future__ import annotations
from dataclasses import dataclass
from domain.baseline.performance import Performance
from domain.baseline.rebaseliner import RebaselineReason
from domain.property_baseline.performance import Performance
from domain.property_baseline.rebaseliner import RebaselineReason
@dataclass(frozen=True)
class BaselinePerformance:
class PropertyBaselinePerformance:
"""A Property's current performance aggregate (CONTEXT.md, ADR-0004).
Holds both halves ``lodged`` (what the gov register says) and

View file

@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
from typing import Literal
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from domain.baseline.performance import Performance
from domain.property_baseline.performance import Performance
RebaselineReason = Literal["none", "pre_sap10", "physical_state_changed", "both"]
@ -29,7 +29,7 @@ class Rebaseliner(ABC):
Rebaselining (CONTEXT.md) re-predicts the rated quantities via ML when the
EPC was lodged pre-SAP10 or its physical state diverged from the lodged EPC;
otherwise Effective Performance equals Lodged. Injected into the
BaselineOrchestrator (ADR-0011) so the ML adapter can swap in without
PropertyBaselineOrchestrator (ADR-0011) so the ML adapter can swap in without
touching the orchestrator, and so the single-property re-score-on-override
flow reuses the same port.
"""

View file

@ -5,20 +5,20 @@ from typing import ClassVar, Optional, cast
from sqlmodel import Field, SQLModel
from datatypes.epc.domain.epc import Epc
from domain.baseline.baseline_performance import BaselinePerformance
from domain.baseline.performance import Performance
from domain.baseline.rebaseliner import RebaselineReason
from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance
from domain.property_baseline.performance import Performance
from domain.property_baseline.rebaseliner import RebaselineReason
class BaselinePerformanceModel(SQLModel, table=True):
"""The ``baseline_performance`` row — one per Property (ADR-0004).
class PropertyBaselinePerformanceModel(SQLModel, table=True):
"""The ``property_baseline_performance`` row — one per Property (ADR-0004).
Flat typed columns (not a JSONB blob) so the FE can both surface the block
and query the lodged-vs-effective pair. The production migration is FE-owned
(Drizzle); see docs/migrations/baseline-performance-table.md.
(Drizzle); see docs/migrations/property-baseline-performance-table.md.
"""
__tablename__: ClassVar[str] = "baseline_performance" # pyright: ignore[reportIncompatibleVariableOverride]
__tablename__: ClassVar[str] = "property_baseline_performance" # pyright: ignore[reportIncompatibleVariableOverride]
id: Optional[int] = Field(default=None, primary_key=True)
property_id: int = Field(unique=True, index=True)
@ -40,8 +40,8 @@ class BaselinePerformanceModel(SQLModel, table=True):
@classmethod
def from_domain(
cls, baseline: BaselinePerformance, property_id: int
) -> "BaselinePerformanceModel":
cls, baseline: PropertyBaselinePerformance, property_id: int
) -> "PropertyBaselinePerformanceModel":
return cls(
property_id=property_id,
lodged_sap_score=baseline.lodged.sap_score,
@ -57,8 +57,8 @@ class BaselinePerformanceModel(SQLModel, table=True):
water_heating_kwh=baseline.water_heating_kwh,
)
def to_domain(self) -> BaselinePerformance:
return BaselinePerformance(
def to_domain(self) -> PropertyBaselinePerformance:
return PropertyBaselinePerformance(
lodged=Performance(
sap_score=self.lodged_sap_score,
epc_band=Epc(self.lodged_epc_band),

View file

@ -29,7 +29,7 @@ class IngestionStage(Protocol):
def run(self, property_ids: list[int]) -> None: ...
class BaselineStage(Protocol):
class PropertyBaselineStage(Protocol):
"""Stage 2 — establishes each Property's Baseline Performance."""
def run(self, property_ids: list[int]) -> None: ...
@ -57,7 +57,7 @@ class FirstRunPipeline:
self,
*,
ingestion: IngestionStage,
baseline: BaselineStage,
baseline: PropertyBaselineStage,
modelling: ModellingStage,
) -> None:
self._ingestion = ingestion

View file

@ -6,13 +6,13 @@ from datatypes.epc.domain.epc_property_data import (
EpcPropertyData,
RenewableHeatIncentive,
)
from domain.baseline.baseline_performance import BaselinePerformance
from domain.baseline.performance import lodged_performance
from domain.baseline.rebaseliner import Rebaseliner
from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance
from domain.property_baseline.performance import lodged_performance
from domain.property_baseline.rebaseliner import Rebaseliner
from repositories.unit_of_work import UnitOfWork
class BaselineOrchestrator:
class PropertyBaselineOrchestrator:
"""Stage 2: establish each Property's Baseline Performance and persist it.
Runs the whole batch in **one** Unit of Work and commits once (ADR-0012):
@ -46,14 +46,14 @@ class BaselineOrchestrator:
effective_epc, lodged
)
rhi = _require_rhi(effective_epc)
baseline = BaselinePerformance(
baseline = PropertyBaselinePerformance(
lodged=lodged,
effective=effective,
rebaseline_reason=reason,
space_heating_kwh=rhi.space_heating_kwh,
water_heating_kwh=rhi.water_heating_kwh,
)
uow.baseline.save(baseline, property_id)
uow.property_baseline.save(baseline, property_id)
uow.commit()

View file

@ -1,43 +0,0 @@
from __future__ import annotations
from typing import Optional
from sqlmodel import Session, col, delete, select
from domain.baseline.baseline_performance import BaselinePerformance
from infrastructure.postgres.baseline_performance_table import (
BaselinePerformanceModel,
)
from repositories.baseline.baseline_repository import BaselineRepository
class BaselinePostgresRepository(BaselineRepository):
"""Maps BaselinePerformance to/from the ``baseline_performance`` table."""
def __init__(self, session: Session) -> None:
self._session = session
def save(self, baseline: BaselinePerformance, property_id: int) -> int:
# Idempotent on property_id: a re-run (or re-score) replaces the row
# rather than hitting the unique constraint (ADR-0012).
self._session.exec( # type: ignore[call-overload]
delete(BaselinePerformanceModel).where(
col(BaselinePerformanceModel.property_id) == property_id
)
)
row = BaselinePerformanceModel.from_domain(baseline, property_id)
self._session.add(row)
self._session.flush()
if row.id is None:
raise ValueError("baseline_performance row did not receive an id")
return row.id
def get_for_property(
self, property_id: int
) -> Optional[BaselinePerformance]:
row = self._session.exec(
select(BaselinePerformanceModel).where(
BaselinePerformanceModel.property_id == property_id
)
).first()
return row.to_domain() if row is not None else None

View file

@ -6,8 +6,8 @@ from typing import Optional
from sqlmodel import Session
from repositories.baseline.baseline_postgres_repository import (
BaselinePostgresRepository,
from repositories.property_baseline.property_baseline_postgres_repository import (
PropertyBaselinePostgresRepository,
)
from repositories.epc.epc_postgres_repository import EpcPostgresRepository
from repositories.property.property_postgres_repository import (
@ -35,7 +35,7 @@ class PostgresUnitOfWork(UnitOfWork):
self.property = PropertyPostgresRepository(self._session, epc_repo)
self.epc = epc_repo
self.solar = SolarPostgresRepository(self._session)
self.baseline = BaselinePostgresRepository(self._session)
self.property_baseline = PropertyBaselinePostgresRepository(self._session)
return self
def __exit__(

View file

@ -0,0 +1,43 @@
from __future__ import annotations
from typing import Optional
from sqlmodel import Session, col, delete, select
from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance
from infrastructure.postgres.property_baseline_performance_table import (
PropertyBaselinePerformanceModel,
)
from repositories.property_baseline.property_baseline_repository import PropertyBaselineRepository
class PropertyBaselinePostgresRepository(PropertyBaselineRepository):
"""Maps PropertyBaselinePerformance to/from the ``property_baseline_performance`` table."""
def __init__(self, session: Session) -> None:
self._session = session
def save(self, baseline: PropertyBaselinePerformance, property_id: int) -> int:
# Idempotent on property_id: a re-run (or re-score) replaces the row
# rather than hitting the unique constraint (ADR-0012).
self._session.exec( # type: ignore[call-overload]
delete(PropertyBaselinePerformanceModel).where(
col(PropertyBaselinePerformanceModel.property_id) == property_id
)
)
row = PropertyBaselinePerformanceModel.from_domain(baseline, property_id)
self._session.add(row)
self._session.flush()
if row.id is None:
raise ValueError("property_baseline_performance row did not receive an id")
return row.id
def get_for_property(
self, property_id: int
) -> Optional[PropertyBaselinePerformance]:
row = self._session.exec(
select(PropertyBaselinePerformanceModel).where(
PropertyBaselinePerformanceModel.property_id == property_id
)
).first()
return row.to_domain() if row is not None else None

View file

@ -3,21 +3,21 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Optional
from domain.baseline.baseline_performance import BaselinePerformance
from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance
class BaselineRepository(ABC):
class PropertyBaselineRepository(ABC):
"""Persists and loads a Property's Baseline Performance.
One Baseline Performance per Property (ADR-0004: persisted as one row). The
Postgres adapter writes the standalone ``baseline_performance`` table not
Postgres adapter writes the standalone ``property_baseline_performance`` table not
columns on the retiring ``property_details_epc``.
"""
@abstractmethod
def save(self, baseline: BaselinePerformance, property_id: int) -> int: ...
def save(self, baseline: PropertyBaselinePerformance, property_id: int) -> int: ...
@abstractmethod
def get_for_property(
self, property_id: int
) -> Optional[BaselinePerformance]: ...
) -> Optional[PropertyBaselinePerformance]: ...

View file

@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
from types import TracebackType
from typing import Optional
from repositories.baseline.baseline_repository import BaselineRepository
from repositories.property_baseline.property_baseline_repository import PropertyBaselineRepository
from repositories.epc.epc_repository import EpcRepository
from repositories.property.property_repository import PropertyRepository
from repositories.solar.solar_repository import SolarRepository
@ -25,7 +25,7 @@ class UnitOfWork(ABC):
property: PropertyRepository
epc: EpcRepository
solar: SolarRepository
baseline: BaselineRepository
property_baseline: PropertyBaselineRepository
@abstractmethod
def commit(self) -> None: ...

View file

@ -2,7 +2,7 @@ from __future__ import annotations
from datatypes.epc.domain.epc import Epc
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from domain.baseline.performance import Performance, lodged_performance
from domain.property_baseline.performance import Performance, lodged_performance
def _epc_with_recorded_performance(

View file

@ -6,8 +6,8 @@ import pytest
from datatypes.epc.domain.epc import Epc
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from domain.baseline.performance import Performance
from domain.baseline.rebaseliner import RebaselineNotImplemented, StubRebaseliner
from domain.property_baseline.performance import Performance
from domain.property_baseline.rebaseliner import RebaselineNotImplemented, StubRebaseliner
def _epc(*, sap_version: Optional[float]) -> EpcPropertyData:

View file

@ -10,10 +10,10 @@ from types import TracebackType
from typing import Any, Optional
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from domain.baseline.baseline_performance import BaselinePerformance
from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance
from domain.property.properties import Properties
from domain.property.property import Property
from repositories.baseline.baseline_repository import BaselineRepository
from repositories.property_baseline.property_baseline_repository import PropertyBaselineRepository
from repositories.epc.epc_repository import EpcRepository
from repositories.property.property_repository import PropertyRepository
from repositories.solar.solar_repository import SolarRepository
@ -74,17 +74,17 @@ class FakeSolarRepo(SolarRepository):
raise NotImplementedError
class FakeBaselineRepo(BaselineRepository):
class FakePropertyBaselineRepo(PropertyBaselineRepository):
def __init__(self) -> None:
self.saved: list[tuple[BaselinePerformance, int]] = []
self.saved: list[tuple[PropertyBaselinePerformance, int]] = []
def save(self, baseline: BaselinePerformance, property_id: int) -> int:
def save(self, baseline: PropertyBaselinePerformance, property_id: int) -> int:
self.saved.append((baseline, property_id))
return len(self.saved)
def get_for_property(
self, property_id: int
) -> Optional[BaselinePerformance]: # pragma: no cover
) -> Optional[PropertyBaselinePerformance]: # pragma: no cover
raise NotImplementedError
@ -97,12 +97,12 @@ class FakeUnitOfWork(UnitOfWork):
property: FakePropertyRepo,
epc: Optional[FakeEpcRepo] = None,
solar: Optional[FakeSolarRepo] = None,
baseline: Optional[FakeBaselineRepo] = None,
property_baseline: Optional[FakePropertyBaselineRepo] = None,
) -> None:
self.property = property
self.epc = epc or FakeEpcRepo()
self.solar = solar or FakeSolarRepo()
self.baseline = baseline or FakeBaselineRepo()
self.property_baseline = property_baseline or FakePropertyBaselineRepo()
self.commits = 0
def __enter__(self) -> "FakeUnitOfWork":

View file

@ -18,19 +18,19 @@ from sqlmodel import Session, select
from datatypes.epc.domain.epc import Epc
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
from domain.baseline.rebaseliner import StubRebaseliner
from domain.property_baseline.rebaseliner import StubRebaseliner
from domain.geospatial.coordinates import Coordinates
from infrastructure.postgres.baseline_performance_table import (
BaselinePerformanceModel,
from infrastructure.postgres.property_baseline_performance_table import (
PropertyBaselinePerformanceModel,
)
from infrastructure.postgres.epc_property_table import EpcPropertyModel
from infrastructure.postgres.property_table import PropertyRow
from orchestration.baseline_orchestrator import BaselineOrchestrator
from orchestration.property_baseline_orchestrator import PropertyBaselineOrchestrator
from orchestration.first_run_pipeline import FirstRunPipeline
from orchestration.ingestion_orchestrator import IngestionOrchestrator
from orchestration.modelling_orchestrator import ModellingOrchestrator
from repositories.baseline.baseline_postgres_repository import (
BaselinePostgresRepository,
from repositories.property_baseline.property_baseline_postgres_repository import (
PropertyBaselinePostgresRepository,
)
from repositories.geospatial.geospatial_repository import GeospatialRepository
from repositories.materials.materials_repository import MaterialsRepository
@ -110,7 +110,7 @@ def test_first_run_baselines_through_repos_and_is_idempotent_on_rerun(
geospatial_repo=_NoCoordinates(),
solar_fetcher=_UnusedSolarFetcher(),
),
baseline=BaselineOrchestrator(
baseline=PropertyBaselineOrchestrator(
unit_of_work=unit_of_work, rebaseliner=StubRebaseliner()
),
modelling=ModellingOrchestrator(
@ -128,13 +128,13 @@ def test_first_run_baselines_through_repos_and_is_idempotent_on_rerun(
# property_ids crossed the stage boundary), and the re-run replaced rather
# than duplicated either row.
with Session(db_engine) as session:
baseline = BaselinePostgresRepository(session).get_for_property(10)
baseline = PropertyBaselinePostgresRepository(session).get_for_property(10)
epc_rows = session.exec(
select(EpcPropertyModel).where(EpcPropertyModel.property_id == 10)
).all()
baseline_rows = session.exec(
select(BaselinePerformanceModel).where(
BaselinePerformanceModel.property_id == 10
select(PropertyBaselinePerformanceModel).where(
PropertyBaselinePerformanceModel.property_id == 10
)
).all()

View file

@ -7,13 +7,13 @@ from datatypes.epc.domain.epc_property_data import (
EpcPropertyData,
RenewableHeatIncentive,
)
from domain.baseline.baseline_performance import BaselinePerformance
from domain.baseline.performance import Performance
from domain.baseline.rebaseliner import RebaselineNotImplemented, StubRebaseliner
from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance
from domain.property_baseline.performance import Performance
from domain.property_baseline.rebaseliner import RebaselineNotImplemented, StubRebaseliner
from domain.property.property import Property, PropertyIdentity
from orchestration.baseline_orchestrator import BaselineOrchestrator
from orchestration.property_baseline_orchestrator import PropertyBaselineOrchestrator
from tests.orchestration.fakes import (
FakeBaselineRepo,
FakePropertyBaselineRepo,
FakePropertyRepo,
FakeUnitOfWork,
)
@ -39,12 +39,12 @@ def _property(*, sap_version: float) -> Property:
def test_run_establishes_persists_and_commits_the_batch_once() -> None:
# Arrange
baseline_repo = FakeBaselineRepo()
property_baseline_repo = FakePropertyBaselineRepo()
uow = FakeUnitOfWork(
property=FakePropertyRepo({10: _property(sap_version=10.2)}),
baseline=baseline_repo,
property_baseline=property_baseline_repo,
)
orchestrator = BaselineOrchestrator(
orchestrator = PropertyBaselineOrchestrator(
unit_of_work=lambda: uow, rebaseliner=StubRebaseliner()
)
@ -56,9 +56,9 @@ def test_run_establishes_persists_and_commits_the_batch_once() -> None:
lodged = Performance(
sap_score=72, epc_band=Epc.C, co2_emissions=1.8, primary_energy_intensity=180
)
assert baseline_repo.saved == [
assert property_baseline_repo.saved == [
(
BaselinePerformance(
PropertyBaselinePerformance(
lodged=lodged,
effective=lodged,
rebaseline_reason="none",
@ -73,12 +73,12 @@ def test_run_establishes_persists_and_commits_the_batch_once() -> None:
def test_run_raises_on_a_pre_sap10_property_and_does_not_commit() -> None:
# Arrange — a pre-SAP10 cert needs ML rebaselining, which is not wired yet.
baseline_repo = FakeBaselineRepo()
property_baseline_repo = FakePropertyBaselineRepo()
uow = FakeUnitOfWork(
property=FakePropertyRepo({10: _property(sap_version=9.94)}),
baseline=baseline_repo,
property_baseline=property_baseline_repo,
)
orchestrator = BaselineOrchestrator(
orchestrator = PropertyBaselineOrchestrator(
unit_of_work=lambda: uow, rebaseliner=StubRebaseliner()
)
@ -86,5 +86,5 @@ def test_run_raises_on_a_pre_sap10_property_and_does_not_commit() -> None:
# committed (all-or-nothing).
with pytest.raises(RebaselineNotImplemented):
orchestrator.run([10])
assert baseline_repo.saved == []
assert property_baseline_repo.saved == []
assert uow.commits == 0

View file

@ -4,14 +4,14 @@ from sqlalchemy import Engine
from sqlmodel import Session
from datatypes.epc.domain.epc import Epc
from domain.baseline.baseline_performance import BaselinePerformance
from domain.baseline.performance import Performance
from repositories.baseline.baseline_postgres_repository import (
BaselinePostgresRepository,
from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance
from domain.property_baseline.performance import Performance
from repositories.property_baseline.property_baseline_postgres_repository import (
PropertyBaselinePostgresRepository,
)
def _baseline() -> BaselinePerformance:
def _baseline() -> PropertyBaselinePerformance:
lodged = Performance(
sap_score=72, epc_band=Epc.C, co2_emissions=1.8, primary_energy_intensity=180
)
@ -20,7 +20,7 @@ def _baseline() -> BaselinePerformance:
effective = Performance(
sap_score=64, epc_band=Epc.D, co2_emissions=2.4, primary_energy_intensity=210
)
return BaselinePerformance(
return PropertyBaselinePerformance(
lodged=lodged,
effective=effective,
rebaseline_reason="pre_sap10",
@ -33,12 +33,12 @@ def test_baseline_performance_round_trips(db_engine: Engine) -> None:
# Arrange
baseline = _baseline()
with Session(db_engine) as session:
BaselinePostgresRepository(session).save(baseline, property_id=10)
PropertyBaselinePostgresRepository(session).save(baseline, property_id=10)
session.commit()
# Act
with Session(db_engine) as session:
loaded = BaselinePostgresRepository(session).get_for_property(10)
loaded = PropertyBaselinePostgresRepository(session).get_for_property(10)
# Assert — the full aggregate reconstructs, both halves intact.
assert loaded == baseline
@ -50,7 +50,7 @@ def test_resaving_baseline_for_a_property_replaces_rather_than_duplicating(
# Arrange — a re-run re-establishes the same property's baseline with a
# different rating.
first = _baseline()
rerun = BaselinePerformance(
rerun = PropertyBaselinePerformance(
lodged=Performance(
sap_score=80,
epc_band=Epc.B,
@ -71,21 +71,21 @@ def test_resaving_baseline_for_a_property_replaces_rather_than_duplicating(
# Act — save twice for the same property_id (must not hit the unique
# constraint, must overwrite).
with Session(db_engine) as session:
repo = BaselinePostgresRepository(session)
repo = PropertyBaselinePostgresRepository(session)
repo.save(first, property_id=10)
repo.save(rerun, property_id=10)
session.commit()
# Assert
with Session(db_engine) as session:
loaded = BaselinePostgresRepository(session).get_for_property(10)
loaded = PropertyBaselinePostgresRepository(session).get_for_property(10)
assert loaded == rerun
def test_get_for_property_returns_none_when_absent(db_engine: Engine) -> None:
# Arrange / Act
with Session(db_engine) as session:
loaded = BaselinePostgresRepository(session).get_for_property(999)
loaded = PropertyBaselinePostgresRepository(session).get_for_property(999)
# Assert
assert loaded is None

View file

@ -7,8 +7,8 @@ from sqlalchemy import Engine
from sqlmodel import Session
from datatypes.epc.domain.epc import Epc
from domain.baseline.baseline_performance import BaselinePerformance
from domain.baseline.performance import Performance
from domain.property_baseline.property_baseline_performance import PropertyBaselinePerformance
from domain.property_baseline.performance import Performance
from repositories.postgres_unit_of_work import PostgresUnitOfWork
@ -16,11 +16,11 @@ def _session_factory(db_engine: Engine) -> Callable[[], Session]:
return lambda: Session(db_engine)
def _baseline() -> BaselinePerformance:
def _baseline() -> PropertyBaselinePerformance:
perf = Performance(
sap_score=72, epc_band=Epc.C, co2_emissions=1.8, primary_energy_intensity=180
)
return BaselinePerformance(
return PropertyBaselinePerformance(
lodged=perf,
effective=perf,
rebaseline_reason="none",
@ -36,12 +36,12 @@ def test_committed_work_is_visible_to_a_later_unit(db_engine: Engine) -> None:
# Act
with new_unit() as uow:
uow.baseline.save(baseline, property_id=10)
uow.property_baseline.save(baseline, property_id=10)
uow.commit()
# Assert — a fresh unit reads back what the first one committed.
with new_unit() as uow:
loaded = uow.baseline.get_for_property(10)
loaded = uow.property_baseline.get_for_property(10)
assert loaded == baseline
@ -52,12 +52,12 @@ def test_an_exception_in_the_block_rolls_the_batch_back(db_engine: Engine) -> No
# Act — a property mid-batch raises after a write but before commit.
with pytest.raises(RuntimeError, match="boom"):
with new_unit() as uow:
uow.baseline.save(_baseline(), property_id=10)
uow.property_baseline.save(_baseline(), property_id=10)
raise RuntimeError("boom")
# Assert — nothing from the aborted batch is persisted.
with new_unit() as uow:
assert uow.baseline.get_for_property(10) is None
assert uow.property_baseline.get_for_property(10) is None
def test_leaving_the_block_without_commit_persists_nothing(db_engine: Engine) -> None:
@ -66,8 +66,8 @@ def test_leaving_the_block_without_commit_persists_nothing(db_engine: Engine) ->
# Act — write but never commit.
with new_unit() as uow:
uow.baseline.save(_baseline(), property_id=10)
uow.property_baseline.save(_baseline(), property_id=10)
# Assert
with new_unit() as uow:
assert uow.baseline.get_for_property(10) is None
assert uow.property_baseline.get_for_property(10) is None