from __future__ import annotations from datetime import date, datetime from enum import Enum from typing import TYPE_CHECKING from sqlalchemy import ( Date, DateTime, Enum as SQLEnum, ForeignKey, Integer, Numeric, String, Text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship, validates from sqlalchemy import ( Date, DateTime, Enum as SQLEnum, ForeignKey, Integer, Numeric, String, Text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.sql import func from config.database import Base from .metadata import CostBucket if TYPE_CHECKING: # pragma: no cover from .scenario import Scenario class FinancialCategory(str, Enum): """Enumeration of cost and revenue classifications.""" CAPITAL_EXPENDITURE = "capex" OPERATING_EXPENDITURE = "opex" REVENUE = "revenue" CONTINGENCY = "contingency" OTHER = "other" class FinancialInput(Base): """Line-item financial assumption attached to a scenario.""" __tablename__ = "financial_inputs" id: Mapped[int] = mapped_column(Integer, primary_key=True) scenario_id: Mapped[int] = mapped_column( ForeignKey("scenarios.id", ondelete="CASCADE"), nullable=False, index=True ) name: Mapped[str] = mapped_column(String(255), nullable=False) category: Mapped[FinancialCategory] = mapped_column( SQLEnum(FinancialCategory), nullable=False ) cost_bucket: Mapped[CostBucket | None] = mapped_column( SQLEnum(CostBucket), nullable=True ) amount: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False) currency: Mapped[str | None] = mapped_column(String(3), nullable=True) effective_date: Mapped[date | None] = mapped_column(Date, nullable=True) 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() ) scenario: Mapped["Scenario"] = relationship("Scenario", back_populates="financial_inputs") @validates("currency") def _validate_currency(self, key: str, value: str | None) -> str | None: if value is None: return value value = value.upper() if len(value) != 3: raise ValueError("Currency code must be a 3-letter ISO 4217 value") return value def __repr__(self) -> str: # pragma: no cover return f"FinancialInput(id={self.id!r}, scenario_id={self.scenario_id!r}, name={self.name!r})"