from __future__ import annotations """Database models for persisted pricing configuration settings.""" from datetime import datetime from typing import TYPE_CHECKING from sqlalchemy import ( JSON, DateTime, ForeignKey, Integer, Numeric, String, Text, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column, relationship, validates from sqlalchemy.sql import func from config.database import Base from services.currency import normalise_currency if TYPE_CHECKING: # pragma: no cover from .project import Project class PricingSettings(Base): """Persisted pricing defaults applied to scenario evaluations.""" __tablename__ = "pricing_settings" id: Mapped[int] = mapped_column(Integer, primary_key=True) name: Mapped[str] = mapped_column(String(128), nullable=False, unique=True) slug: Mapped[str] = mapped_column(String(64), nullable=False, unique=True) description: Mapped[str | None] = mapped_column(Text, nullable=True) default_currency: Mapped[str | None] = mapped_column( String(3), nullable=True) default_payable_pct: Mapped[float] = mapped_column( Numeric(5, 2), nullable=False, default=100.0 ) moisture_threshold_pct: Mapped[float] = mapped_column( Numeric(5, 2), nullable=False, default=8.0 ) moisture_penalty_per_pct: Mapped[float] = mapped_column( Numeric(14, 4), nullable=False, default=0.0 ) metadata_payload: Mapped[dict | None] = mapped_column( "metadata", JSON, nullable=True ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) metal_overrides: Mapped[list["PricingMetalSettings"]] = relationship( "PricingMetalSettings", back_populates="pricing_settings", cascade="all, delete-orphan", passive_deletes=True, ) impurity_overrides: Mapped[list["PricingImpuritySettings"]] = relationship( "PricingImpuritySettings", back_populates="pricing_settings", cascade="all, delete-orphan", passive_deletes=True, ) projects: Mapped[list["Project"]] = relationship( "Project", back_populates="pricing_settings", cascade="all", ) @validates("slug") def _normalise_slug(self, key: str, value: str) -> str: return value.strip().lower() @validates("default_currency") def _validate_currency(self, key: str, value: str | None) -> str | None: return normalise_currency(value) def __repr__(self) -> str: # pragma: no cover return f"PricingSettings(id={self.id!r}, slug={self.slug!r})" class PricingMetalSettings(Base): """Contract-specific overrides for a particular metal.""" __tablename__ = "pricing_metal_settings" __table_args__ = ( UniqueConstraint( "pricing_settings_id", "metal_code", name="uq_pricing_metal_settings_code" ), ) id: Mapped[int] = mapped_column(Integer, primary_key=True) pricing_settings_id: Mapped[int] = mapped_column( ForeignKey("pricing_settings.id", ondelete="CASCADE"), nullable=False, index=True ) metal_code: Mapped[str] = mapped_column(String(32), nullable=False) payable_pct: Mapped[float | None] = mapped_column( Numeric(5, 2), nullable=True) moisture_threshold_pct: Mapped[float | None] = mapped_column( Numeric(5, 2), nullable=True) moisture_penalty_per_pct: Mapped[float | None] = mapped_column( Numeric(14, 4), nullable=True ) data: Mapped[dict | None] = mapped_column(JSON, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) pricing_settings: Mapped["PricingSettings"] = relationship( "PricingSettings", back_populates="metal_overrides" ) @validates("metal_code") def _normalise_metal_code(self, key: str, value: str) -> str: return value.strip().lower() def __repr__(self) -> str: # pragma: no cover return ( "PricingMetalSettings(" # noqa: ISC001 f"id={self.id!r}, pricing_settings_id={self.pricing_settings_id!r}, " f"metal_code={self.metal_code!r})" ) class PricingImpuritySettings(Base): """Impurity penalty thresholds associated with pricing settings.""" __tablename__ = "pricing_impurity_settings" __table_args__ = ( UniqueConstraint( "pricing_settings_id", "impurity_code", name="uq_pricing_impurity_settings_code", ), ) id: Mapped[int] = mapped_column(Integer, primary_key=True) pricing_settings_id: Mapped[int] = mapped_column( ForeignKey("pricing_settings.id", ondelete="CASCADE"), nullable=False, index=True ) impurity_code: Mapped[str] = mapped_column(String(32), nullable=False) threshold_ppm: Mapped[float] = mapped_column( Numeric(14, 4), nullable=False, default=0.0) penalty_per_ppm: Mapped[float] = mapped_column( Numeric(14, 4), nullable=False, default=0.0) notes: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) pricing_settings: Mapped["PricingSettings"] = relationship( "PricingSettings", back_populates="impurity_overrides" ) @validates("impurity_code") def _normalise_impurity_code(self, key: str, value: str) -> str: return value.strip().upper() def __repr__(self) -> str: # pragma: no cover return ( "PricingImpuritySettings(" # noqa: ISC001 f"id={self.id!r}, pricing_settings_id={self.pricing_settings_id!r}, " f"impurity_code={self.impurity_code!r})" )