from __future__ import annotations from sqlmodel import Session, col, select from domain.modelling.contingencies import contingency_rate from domain.modelling.product import Product from infrastructure.postgres.product_table import MaterialRow from repositories.product.product_repository import ProductNotFound, ProductRepository # The domain ``MeasureType`` vocabulary and the catalogue's ``material.type`` # pgEnum drifted apart: these five measures are spelled differently on the # catalogue side (and querying the domain spelling raises a pgEnum DataError # that poisons the session's transaction). Translate them to the catalogue's # own vocabulary at this boundary so the domain enum stays stable. Every other # MeasureType already matches its material.type and maps to itself. _MATERIAL_TYPE_BY_MEASURE: dict[str, str] = { "low_energy_lighting": "low_energy_lighting_installation", "gas_boiler_upgrade": "boiler_upgrade", "system_tune_up": "roomstat_programmer_trvs", "system_tune_up_zoned": "time_temperature_zone_control", "sloping_ceiling_insulation": "room_roof_insulation", } class ProductPostgresRepository(ProductRepository): """Reads the ``material`` catalogue table and maps an active row to a Product: `total_cost` becomes the fully-loaded `unit_cost_per_m2`, and the per-Measure-Type contingency is joined from config.""" def __init__(self, session: Session) -> None: self._session = session def get(self, measure_type: str) -> Product: # Resolve the domain MeasureType to the catalogue's ``material.type`` # spelling (identity for all but the five drifted types above). catalogue_type = _MATERIAL_TYPE_BY_MEASURE.get(measure_type, measure_type) # The live catalogue holds many active rows per type; order by id so the # pick is deterministic (a re-seed prices the same) rather than relying # on the database's physical row order. row: MaterialRow | None = self._session.exec( select(MaterialRow) .where( col(MaterialRow.type) == catalogue_type, col(MaterialRow.is_active).is_(True), ) .order_by(col(MaterialRow.id)) ).first() if row is None: raise ProductNotFound( f"no active product for measure type {measure_type!r}" ) if row.total_cost is None: raise ValueError(f"product {measure_type!r} has no total_cost") return Product( measure_type=measure_type, unit_cost_per_m2=row.total_cost, contingency_rate=contingency_rate(measure_type), id=row.id, )