Model/repositories/product/product_postgres_repository.py
Khalim Conn-Kowlessar 62e1d4b813 fix(product): deterministic catalogue pick by ordering get() by id
ProductPostgresRepository.get took .first() with no ORDER BY, so when a
measure type has several active material rows (the live catalogue holds 74
solar_pv, 5 high_heat_retention_storage_heaters) the chosen row — hence the
cost and material_id — depended on the database's physical row order. Order by
id so a re-seed prices the same product every time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 20:35:45 +00:00

40 lines
1.6 KiB
Python

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 ProductRepository
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:
# 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) == measure_type,
col(MaterialRow.is_active).is_(True),
)
.order_by(col(MaterialRow.id))
).first()
if row is None:
raise ValueError(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,
)