refactor(modelling): consolidate plan/recommendation models into infrastructure

Move the live plan, recommendation, recommendation_materials and (retiring)
plan_recommendations tables into a new infrastructure/postgres/modelling/
subpackage as single SQLModel definitions (the epc_property pattern), absorbing
the rebuild's partial PlanRow/RecommendationRow mirrors and carrying full
legacy column parity plus recommendation.plan_id. Out-of-cluster references are
plain indexed ints (mirror convention); the live FKs are owned by the Drizzle
schema. backend/app/db/models/recommendations.py becomes a re-export shim
(ScenarioModel/InstalledMeasure stay for a later slice).

Fix the export conftest to create SQLModel-first (so Base funding_package's FK
to the now-SQLModel plan resolves) and skip the redundant drop_all on its
function-scoped throwaway DB (the epc enum type is now shared across both
metadatas). Resolves the pre-existing dual-definition collision: the rebuild
and legacy export suites are now co-runnable. No behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-03 21:00:14 +00:00
parent b76d0f814b
commit c1c7b06f09
9 changed files with 312 additions and 287 deletions

View file

@ -1,13 +1,23 @@
"""Re-export shim + remaining legacy models (ADR-0017 amendment).
`plan`, `recommendation`, `recommendation_materials` and the retiring
`plan_recommendations` moved to `infrastructure/postgres/modelling/` as single
SQLModel definitions (the `epc_property` pattern). This module re-exports them
under their legacy names so the dying `backend/` callers keep working; new code
imports from `infrastructure.postgres.modelling` directly. `ScenarioModel` and
`InstalledMeasure` are not yet migrated and stay here for now.
"""
import enum
from typing import Iterable, List, NamedTuple, Optional, Type
from sqlalchemy import (
Column,
BigInteger,
String,
Float,
Boolean,
TIMESTAMP,
ForeignKey,
Column,
Enum,
)
from sqlalchemy.orm import Mapped, mapped_column
@ -16,158 +26,28 @@ from datetime import datetime
from backend.app.db.base import Base
from backend.app.db.models.portfolio import Portfolio, PortfolioGoal, PropertyModel
from backend.app.db.models.materials import Material
from datatypes.enums import QuantityUnits
from datatypes.epc.domain.epc import Epc
from infrastructure.postgres.modelling import (
PlanRow,
PlanType,
PlanRecommendationRow,
RecommendationMaterialRow,
RecommendationRow,
)
# Legacy names → the single SQLModel definitions now in
# `infrastructure/postgres/modelling/`.
Recommendation = RecommendationRow
RecommendationMaterials = RecommendationMaterialRow
PlanModel = PlanRow
PlanRecommendations = PlanRecommendationRow
PlanTypeEnum = PlanType
def portfolio_goal_values(enum_cls: Type[PortfolioGoal]) -> List[str]:
return [e.value for e in enum_cls]
class Recommendation(Base):
__tablename__ = "recommendation"
id = Column(BigInteger, primary_key=True, autoincrement=True)
property_id = Column(BigInteger, ForeignKey(PropertyModel.id), nullable=False)
created_at = Column(TIMESTAMP, nullable=False, server_default=func.now())
type = Column(String, nullable=False)
measure_type = Column(String)
description = Column(String, nullable=False)
estimated_cost = Column(Float)
default = Column(Boolean, nullable=False)
starting_u_value = Column(Float)
new_u_value = Column(Float)
sap_points = Column(Float)
heat_demand = Column(Float)
kwh_savings = Column(Float)
co2_equivalent_savings = Column(Float)
energy_savings = Column(Float)
energy_cost_savings = Column(Float)
property_valuation_increase = Column(Float)
rental_yield_increase = Column(Float)
total_work_hours = Column(Float)
labour_days = Column(Float)
already_installed = Column(Boolean, nullable=False, default=False)
class RecommendationMaterials(Base):
__tablename__ = "recommendation_materials"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
recommendation_id: Mapped[int] = mapped_column(
BigInteger,
ForeignKey("recommendation.id"),
nullable=False,
)
material_id: Mapped[int] = mapped_column(
BigInteger,
ForeignKey(Material.id),
nullable=False,
)
created_at: Mapped[datetime] = mapped_column(
TIMESTAMP,
nullable=False,
server_default=func.now(),
)
depth: Mapped[float] = mapped_column(
Float,
nullable=False,
)
quantity: Mapped[float] = mapped_column(
Float,
nullable=False,
)
quantity_unit: Mapped[QuantityUnits] = mapped_column(
Enum(QuantityUnits, values_callable=lambda x: [e.value for e in x]),
nullable=False,
)
estimated_cost: Mapped[float] = mapped_column(
Float,
nullable=False,
)
class PlanTypeEnum(enum.Enum): # TODO: move this to domain?
SOLAR_ECO4 = "solar_eco4"
SOLAR_HHRSH_ECO4 = "solar_hhrsh_eco4"
EMPTY_CAVITY_ECO = "empty_cavity_eco"
PARTIAL_CAVITY_ECO = "partial_cavity_eco"
EXTRACTION_ECO = "extraction_eco"
class PlanModel(Base):
__tablename__ = "plan"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
name: Mapped[Optional[str]] = mapped_column(String, nullable=True, default="")
portfolio_id: Mapped[int] = mapped_column(
BigInteger, ForeignKey(Portfolio.id), nullable=False
)
property_id: Mapped[int] = mapped_column(
BigInteger, ForeignKey(PropertyModel.id), nullable=False
)
scenario_id: Mapped[Optional[int]] = mapped_column(
BigInteger, ForeignKey("scenario.id")
)
created_at: Mapped[datetime] = mapped_column( # type: ignore
TIMESTAMP, nullable=False, server_default=func.now()
)
is_default: Mapped[bool] = mapped_column(Boolean, nullable=False)
valuation_increase_lower_bound: Mapped[Optional[float]] = mapped_column(Float)
valuation_increase_upper_bound: Mapped[Optional[float]] = mapped_column(Float)
valuation_increase_average: Mapped[Optional[float]] = mapped_column(Float)
plan_type: Mapped[Optional[PlanTypeEnum]] = mapped_column(
Enum(
PlanTypeEnum,
name="plan_type",
values_callable=lambda e: [m.value for m in e],
create_type=False,
),
nullable=True,
)
post_sap_points: Mapped[Optional[float]] = mapped_column(Float)
post_epc_rating: Mapped[Optional[Epc]] = mapped_column(Enum(Epc))
post_co2_emissions: Mapped[Optional[float]] = mapped_column(Float)
co2_savings: Mapped[Optional[float]] = mapped_column(Float)
post_energy_bill: Mapped[Optional[float]] = mapped_column(Float)
energy_bill_savings: Mapped[Optional[float]] = mapped_column(Float)
post_energy_consumption: Mapped[Optional[float]] = mapped_column(Float)
energy_consumption_savings: Mapped[Optional[float]] = mapped_column(Float)
valuation_post_retrofit: Mapped[Optional[float]] = mapped_column(Float)
valuation_increase: Mapped[Optional[float]] = mapped_column(Float)
# Financial metrics, excluding funding
cost_of_works: Mapped[Optional[float]] = mapped_column(Float)
contingency_cost: Mapped[Optional[float]] = mapped_column(Float)
class PlanRecommendations(Base):
__tablename__ = "plan_recommendations"
id = Column(BigInteger, primary_key=True, autoincrement=True)
plan_id = Column(BigInteger, ForeignKey("plan.id"), nullable=False)
recommendation_id = Column(
BigInteger, ForeignKey("recommendation.id"), nullable=False
)
class ScenarioModel(Base):
__tablename__ = "scenario"
@ -282,10 +162,10 @@ class InstalledMeasure(Base):
is_active = Column(Boolean, nullable=False, default=True)
def enum_values(e: Iterable[PlanTypeEnum]) -> list[str]:
def enum_values(e: Iterable[PlanType]) -> list[str]:
return [m.value for m in e]
class PlanPersistence(NamedTuple):
plan: PlanModel
plan: PlanRow
scenario: ScenarioModel

View file

@ -25,17 +25,23 @@ def engine(postgresql):
engine = create_engine(connection_string)
# Create tables once per test session
Base.metadata.create_all(engine)
# Create tables once per test session. SQLModel first: the Modelling tables
# (`plan` / `recommendation` / …) are SQLModel definitions, and Base tables
# FK them (`funding_package` → `plan`), so they must exist before Base's
# create_all runs (ADR-0017 amendment — single model per table).
SQLModel.metadata.create_all(engine)
Base.metadata.create_all(engine)
# Yeild will split this function into two phase. 1) setup and 2) teardown, the latter of which will run after all
# tests have completed
yield engine
# Clean-up after entire test session
SQLModel.metadata.drop_all(engine)
Base.metadata.drop_all(engine)
# The `postgresql` fixture is function-scoped — a fresh, throwaway database
# per test — so an explicit drop_all is redundant. We skip it: the `epc`
# Postgres enum type is now shared across both metadatas (Base `portfolio`
# tables and the SQLModel `plan`), and a two-phase metadata drop cannot drop
# a cross-metadata type cleanly (ADR-0017 amendment). Disposing the engine
# and letting the fixture discard the database is correct and conflict-free.
engine.dispose()

View file

@ -0,0 +1,24 @@
"""SQLModel definitions of the Modelling stage's live persistence tables
(ADR-0017 amendment).
One canonical SQLModel per physical table `plan`, `recommendation`,
`recommendation_materials` replacing the legacy SQLAlchemy `Base` models in
`backend/app/db/models/recommendations.py` (now a re-export shim, the
`epc_property` pattern). `recommendation` carries `plan_id`; the
`plan_recommendations` m2m is retired.
"""
from infrastructure.postgres.modelling.plan_table import PlanRow, PlanType
from infrastructure.postgres.modelling.recommendation_table import (
PlanRecommendationRow,
RecommendationMaterialRow,
RecommendationRow,
)
__all__ = [
"PlanRow",
"PlanType",
"RecommendationRow",
"RecommendationMaterialRow",
"PlanRecommendationRow",
]

View file

@ -0,0 +1,106 @@
from __future__ import annotations
import enum
from datetime import datetime
from typing import ClassVar, Optional
from sqlalchemy import Column, TIMESTAMP
from sqlalchemy import Enum as SAEnum
from sqlalchemy.sql import func
from sqlmodel import Field, SQLModel
from datatypes.epc.domain.epc import Epc
from domain.modelling.plan import Plan
# Calculator metrics are in kg CO₂/yr; the live ``plan`` columns are tonnes
# (legacy ``emissions_kg / 1000``). Convert on the way in.
_KG_PER_TONNE = 1000.0
class PlanType(enum.Enum):
SOLAR_ECO4 = "solar_eco4"
SOLAR_HHRSH_ECO4 = "solar_hhrsh_eco4"
EMPTY_CAVITY_ECO = "empty_cavity_eco"
PARTIAL_CAVITY_ECO = "partial_cavity_eco"
EXTRACTION_ECO = "extraction_eco"
class PlanRow(SQLModel, table=True):
"""The single SQLModel definition of the live ``plan`` table (ADR-0017
amendment). Full legacy column parity; out-of-cluster references
(``portfolio_id`` / ``property_id`` / ``scenario_id``) are plain indexed
ints, not FK constraints (mirror convention the live FKs are owned by the
Drizzle schema)."""
__tablename__: ClassVar[str] = "plan" # pyright: ignore[reportIncompatibleVariableOverride]
id: Optional[int] = Field(default=None, primary_key=True)
name: Optional[str] = Field(default="")
portfolio_id: int
property_id: int = Field(index=True)
scenario_id: Optional[int] = Field(default=None)
created_at: Optional[datetime] = Field(
default=None,
sa_column=Column(TIMESTAMP, nullable=False, server_default=func.now()),
)
is_default: bool = False
valuation_increase_lower_bound: Optional[float] = Field(default=None)
valuation_increase_upper_bound: Optional[float] = Field(default=None)
valuation_increase_average: Optional[float] = Field(default=None)
plan_type: Optional[PlanType] = Field(
default=None,
sa_column=Column(
SAEnum(
PlanType,
name="plan_type",
values_callable=lambda cls: [m.value for m in cls], # pyright: ignore[reportUnknownLambdaType, reportUnknownMemberType, reportUnknownVariableType]
create_type=False,
),
nullable=True,
),
)
post_sap_points: Optional[float] = Field(default=None)
post_epc_rating: Optional[Epc] = Field(
default=None,
sa_column=Column(SAEnum(Epc, name="epc"), nullable=True),
)
post_co2_emissions: Optional[float] = Field(default=None) # tonnes/yr
co2_savings: Optional[float] = Field(default=None) # tonnes/yr
post_energy_bill: Optional[float] = Field(default=None) # £/yr
energy_bill_savings: Optional[float] = Field(default=None) # £/yr
post_energy_consumption: Optional[float] = Field(default=None) # kWh/yr
energy_consumption_savings: Optional[float] = Field(default=None) # kWh/yr
valuation_post_retrofit: Optional[float] = Field(default=None)
valuation_increase: Optional[float] = Field(default=None)
cost_of_works: Optional[float] = Field(default=None)
contingency_cost: Optional[float] = Field(default=None)
@classmethod
def from_domain(
cls,
plan: Plan,
*,
property_id: int,
scenario_id: int,
portfolio_id: int,
is_default: bool,
) -> "PlanRow":
return cls(
portfolio_id=portfolio_id,
property_id=property_id,
scenario_id=scenario_id,
is_default=is_default,
post_sap_points=plan.post_sap_continuous,
post_epc_rating=plan.post_epc_rating,
post_co2_emissions=plan.post_retrofit.co2_kg_per_yr / _KG_PER_TONNE,
co2_savings=plan.co2_savings_kg_per_yr / _KG_PER_TONNE,
cost_of_works=plan.cost_of_works,
contingency_cost=plan.contingency_cost,
post_energy_bill=plan.post_energy_bill,
energy_bill_savings=plan.energy_bill_savings,
post_energy_consumption=plan.post_energy_consumption,
energy_consumption_savings=plan.energy_consumption_savings,
)

View file

@ -0,0 +1,139 @@
from __future__ import annotations
from datetime import datetime
from typing import ClassVar, Optional
from sqlalchemy import BigInteger, Column, ForeignKey, TIMESTAMP
from sqlalchemy import Enum as SAEnum
from sqlalchemy.sql import func
from sqlmodel import Field, SQLModel
from datatypes.enums import QuantityUnits
from domain.modelling.plan import PlanMeasure
# Calculator metrics are in kg CO₂/yr; the live ``recommendation`` column is
# tonnes (legacy ``emissions_kg / 1000``). Convert on the way in.
_KG_PER_TONNE = 1000.0
class RecommendationRow(SQLModel, table=True):
"""The single SQLModel definition of the live ``recommendation`` table
(ADR-0017 amendment) one row per persisted Plan Measure.
Carries full legacy column parity (the readers iterate the columns / sum
them) **plus** ``plan_id``, the FK that links a measure to its Plan and
replaces the retired ``plan_recommendations`` m2m. Out-of-cluster columns
(``property_id``) are plain indexed ints, not FK constraints, matching the
mirror convention so ``SQLModel.metadata.create_all`` needs no foreign
table to exist (the live FKs are owned by the Drizzle schema).
"""
__tablename__: ClassVar[str] = "recommendation" # pyright: ignore[reportIncompatibleVariableOverride]
id: Optional[int] = Field(default=None, primary_key=True)
property_id: int = Field(index=True)
plan_id: Optional[int] = Field(
default=None,
sa_column=Column(
BigInteger,
ForeignKey("plan.id", ondelete="CASCADE"),
nullable=True,
index=True,
),
)
created_at: Optional[datetime] = Field(
default=None,
sa_column=Column(TIMESTAMP, nullable=False, server_default=func.now()),
)
type: str
measure_type: Optional[str] = Field(default=None)
description: str
estimated_cost: Optional[float] = Field(default=None)
starting_u_value: Optional[float] = Field(default=None)
new_u_value: Optional[float] = Field(default=None)
sap_points: Optional[float] = Field(default=None)
heat_demand: Optional[float] = Field(default=None)
kwh_savings: Optional[float] = Field(default=None) # delivered kWh/yr
co2_equivalent_savings: Optional[float] = Field(default=None) # tonnes/yr
energy_savings: Optional[float] = Field(default=None)
energy_cost_savings: Optional[float] = Field(default=None) # £/yr
property_valuation_increase: Optional[float] = Field(default=None)
rental_yield_increase: Optional[float] = Field(default=None)
total_work_hours: Optional[float] = Field(default=None)
labour_days: Optional[float] = Field(default=None)
default: bool = True
already_installed: bool = False
@classmethod
def from_domain(
cls, measure: PlanMeasure, *, property_id: int, plan_id: int
) -> "RecommendationRow":
return cls(
property_id=property_id,
plan_id=plan_id,
type=measure.measure_type,
measure_type=measure.measure_type,
description=measure.description,
estimated_cost=measure.cost.total,
sap_points=measure.impact.sap_points,
co2_equivalent_savings=(
measure.impact.co2_savings_kg_per_yr / _KG_PER_TONNE
),
kwh_savings=measure.kwh_savings,
energy_cost_savings=measure.energy_cost_savings,
default=True,
already_installed=False,
)
class RecommendationMaterialRow(SQLModel, table=True):
"""The live ``recommendation_materials`` table — one row per material used
by a Recommendation. ``recommendation_id`` is an intra-cluster FK;
``material_id`` is a plain int (out-of-cluster, mirror convention)."""
__tablename__: ClassVar[str] = "recommendation_materials" # pyright: ignore[reportIncompatibleVariableOverride]
id: Optional[int] = Field(default=None, primary_key=True)
recommendation_id: int = Field(
sa_column=Column(
BigInteger, ForeignKey("recommendation.id"), nullable=False
)
)
material_id: int = Field(index=True)
created_at: Optional[datetime] = Field(
default=None,
sa_column=Column(TIMESTAMP, nullable=False, server_default=func.now()),
)
depth: Optional[float] = Field(default=None)
quantity: Optional[float] = Field(default=None)
quantity_unit: Optional[QuantityUnits] = Field(
default=None,
sa_column=Column(
SAEnum(
QuantityUnits,
values_callable=lambda cls: [m.value for m in cls], # pyright: ignore[reportUnknownLambdaType, reportUnknownMemberType, reportUnknownVariableType]
),
nullable=True,
),
)
estimated_cost: Optional[float] = Field(default=None)
class PlanRecommendationRow(SQLModel, table=True):
"""The legacy ``plan_recommendations`` m2m — **being retired** (ADR-0017
amendment). Kept as an intra-cluster SQLModel row only for the transition
window while readers/writers move onto ``recommendation.plan_id``; dropped
once no caller remains. Both FKs are intra-cluster."""
__tablename__: ClassVar[str] = "plan_recommendations" # pyright: ignore[reportIncompatibleVariableOverride]
id: Optional[int] = Field(default=None, primary_key=True)
plan_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("plan.id"), nullable=False)
)
recommendation_id: int = Field(
sa_column=Column(
BigInteger, ForeignKey("recommendation.id"), nullable=False
)
)

View file

@ -1,130 +0,0 @@
from __future__ import annotations
from typing import ClassVar, Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlalchemy import Enum as SAEnum
from sqlmodel import Field, SQLModel
from datatypes.epc.domain.epc import Epc
from domain.modelling.plan import Plan, PlanMeasure
# Calculator metrics are in kg CO₂/yr; the live `plan` / `recommendation`
# columns are tonnes (legacy `emissions_kg / 1000`). Convert on the way in.
_KG_PER_TONNE = 1000.0
class PlanRow(SQLModel, table=True):
"""SQLModel mirror of the live ``plan`` table (ADR-0017).
Declares only the columns the rebuild writes identity, the flat
post-retrofit headline figures, and the cost aggregates. The legacy
SQLAlchemy model owns the live reads and the columns left for later
slices (valuation, plan_type, the energy/bill cluster). The physical
table is the shared contract.
"""
__tablename__: ClassVar[str] = "plan" # pyright: ignore[reportIncompatibleVariableOverride]
id: Optional[int] = Field(default=None, primary_key=True)
portfolio_id: int
property_id: int = Field(index=True)
scenario_id: Optional[int] = Field(default=None)
is_default: bool = False
post_sap_points: Optional[float] = Field(default=None)
post_epc_rating: Optional[Epc] = Field(
default=None,
sa_column=Column(SAEnum(Epc, name="epc"), nullable=True),
)
post_co2_emissions: Optional[float] = Field(default=None) # tonnes/yr
co2_savings: Optional[float] = Field(default=None) # tonnes/yr
cost_of_works: Optional[float] = Field(default=None)
contingency_cost: Optional[float] = Field(default=None)
post_energy_bill: Optional[float] = Field(default=None) # £/yr
energy_bill_savings: Optional[float] = Field(default=None) # £/yr
post_energy_consumption: Optional[float] = Field(default=None) # delivered kWh/yr
energy_consumption_savings: Optional[float] = Field(default=None) # kWh/yr
@classmethod
def from_domain(
cls,
plan: Plan,
*,
property_id: int,
scenario_id: int,
portfolio_id: int,
is_default: bool,
) -> "PlanRow":
return cls(
portfolio_id=portfolio_id,
property_id=property_id,
scenario_id=scenario_id,
is_default=is_default,
post_sap_points=plan.post_sap_continuous,
post_epc_rating=plan.post_epc_rating,
post_co2_emissions=plan.post_retrofit.co2_kg_per_yr / _KG_PER_TONNE,
co2_savings=plan.co2_savings_kg_per_yr / _KG_PER_TONNE,
cost_of_works=plan.cost_of_works,
contingency_cost=plan.contingency_cost,
post_energy_bill=plan.post_energy_bill,
energy_bill_savings=plan.energy_bill_savings,
post_energy_consumption=plan.post_energy_consumption,
energy_consumption_savings=plan.energy_consumption_savings,
)
class RecommendationRow(SQLModel, table=True):
"""SQLModel mirror of the live ``recommendation`` table — one row per
persisted Plan Measure (ADR-0017). Adds the new ``plan_id`` FK linking the
measure to its Plan (ON DELETE CASCADE), replacing the ``plan_recommendations``
m2m for new writes. Only the impact + cost columns the tracer fills are
declared; the energy/bill, U-value, valuation and labour columns are left
to later slices.
"""
__tablename__: ClassVar[str] = "recommendation" # pyright: ignore[reportIncompatibleVariableOverride]
id: Optional[int] = Field(default=None, primary_key=True)
property_id: int = Field(index=True)
plan_id: Optional[int] = Field(
default=None,
sa_column=Column(
BigInteger,
ForeignKey("plan.id", ondelete="CASCADE"),
nullable=True,
index=True,
),
)
type: str
measure_type: Optional[str] = Field(default=None)
description: str
estimated_cost: Optional[float] = Field(default=None)
sap_points: Optional[float] = Field(default=None)
co2_equivalent_savings: Optional[float] = Field(default=None) # tonnes/yr
kwh_savings: Optional[float] = Field(default=None) # delivered kWh/yr
energy_cost_savings: Optional[float] = Field(default=None) # £/yr
default: bool = True
already_installed: bool = False
@classmethod
def from_domain(
cls, measure: PlanMeasure, *, property_id: int, plan_id: int
) -> "RecommendationRow":
return cls(
property_id=property_id,
plan_id=plan_id,
type=measure.measure_type,
measure_type=measure.measure_type,
description=measure.description,
estimated_cost=measure.cost.total,
sap_points=measure.impact.sap_points,
co2_equivalent_savings=(
measure.impact.co2_savings_kg_per_yr / _KG_PER_TONNE
),
kwh_savings=measure.kwh_savings,
energy_cost_savings=measure.energy_cost_savings,
default=True,
already_installed=False,
)

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from sqlmodel import Session, col, delete
from domain.modelling.plan import Plan
from infrastructure.postgres.plan_table import PlanRow, RecommendationRow
from infrastructure.postgres.modelling import PlanRow, RecommendationRow
from repositories.plan.plan_repository import PlanRepository

View file

@ -26,7 +26,7 @@ from infrastructure.postgres.property_baseline_performance_table import (
PropertyBaselinePerformanceModel,
)
from infrastructure.postgres.epc_property_table import EpcPropertyModel
from infrastructure.postgres.plan_table import PlanRow, RecommendationRow
from infrastructure.postgres.modelling import PlanRow, RecommendationRow
from infrastructure.postgres.product_table import MaterialRow
from infrastructure.postgres.property_table import PropertyRow
from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import (

View file

@ -18,7 +18,7 @@ from domain.modelling.scoring.package_scorer import Score
from domain.modelling.plan import Plan, PlanMeasure
from domain.modelling.recommendation import Cost
from domain.modelling.scoring.scoring import MeasureImpact
from infrastructure.postgres.plan_table import PlanRow, RecommendationRow
from infrastructure.postgres.modelling import PlanRow, RecommendationRow
from repositories.plan.plan_postgres_repository import PlanPostgresRepository