import enum from datetime import datetime from sqlalchemy import String, Text, Integer, Numeric, Boolean, DateTime, Enum, ForeignKey, Index from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database import Base from app.models.gmal import ModelType class ProjectStatus(str, enum.Enum): DRAFT = "draft" PARSING = "parsing" MATCHING = "matching" REVIEW = "review" BUILDING = "building" FINALIZED = "finalized" class MatchConfidence(str, enum.Enum): EXACT = "exact" CLOSE = "close" MULTIPLE = "multiple" NONE = "none" class Project(Base): __tablename__ = "projects" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(255), nullable=False) client_name: Mapped[str | None] = mapped_column(String(255)) description: Mapped[str | None] = mapped_column(Text) model_type: Mapped[ModelType] = mapped_column(Enum(ModelType), default=ModelType.CURRENT_OPLUS) status: Mapped[ProjectStatus] = mapped_column(Enum(ProjectStatus), default=ProjectStatus.DRAFT) source_filename: Mapped[str | None] = mapped_column(String(255)) parse_stage: Mapped[str | None] = mapped_column(String(255)) brief_analysis: Mapped[str | None] = mapped_column(Text) ai_input_tokens: Mapped[int] = mapped_column(Integer, default=0) ai_output_tokens: Mapped[int] = mapped_column(Integer, default=0) ai_cost_usd: Mapped[float] = mapped_column(Numeric(10, 6), default=0) ai_call_count: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) client_assets: Mapped[list["ClientAsset"]] = relationship(back_populates="project", cascade="all, delete-orphan") ratecard_lines: Mapped[list["RatecardLine"]] = relationship(back_populates="project", cascade="all, delete-orphan") class ClientAsset(Base): __tablename__ = "client_assets" id: Mapped[int] = mapped_column(primary_key=True) project_id: Mapped[int] = mapped_column(ForeignKey("projects.id", ondelete="CASCADE"), nullable=False) raw_name: Mapped[str | None] = mapped_column(String(500)) raw_description: Mapped[str | None] = mapped_column(Text) volume: Mapped[int] = mapped_column(Integer, default=1) sort_order: Mapped[int | None] = mapped_column(Integer) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) project: Mapped["Project"] = relationship(back_populates="client_assets") matches: Mapped[list["Match"]] = relationship(back_populates="client_asset", cascade="all, delete-orphan") class Match(Base): __tablename__ = "matches" id: Mapped[int] = mapped_column(primary_key=True) client_asset_id: Mapped[int] = mapped_column(ForeignKey("client_assets.id", ondelete="CASCADE"), nullable=False) gmal_asset_id: Mapped[int] = mapped_column(ForeignKey("gmal_assets.id"), nullable=False) confidence: Mapped[MatchConfidence] = mapped_column(Enum(MatchConfidence), nullable=False) confidence_score: Mapped[float | None] = mapped_column(Numeric(3, 2)) ai_reasoning: Mapped[str | None] = mapped_column(Text) caveat_text: Mapped[str | None] = mapped_column(Text) is_selected: Mapped[bool] = mapped_column(Boolean, default=False) rank: Mapped[int] = mapped_column(Integer, default=1) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) client_asset: Mapped["ClientAsset"] = relationship(back_populates="matches") gmal_asset: Mapped["GmalAsset"] = relationship() __table_args__ = ( Index("idx_matches_client_asset", "client_asset_id"), ) class RatecardLine(Base): __tablename__ = "ratecard_lines" id: Mapped[int] = mapped_column(primary_key=True) project_id: Mapped[int] = mapped_column(ForeignKey("projects.id", ondelete="CASCADE"), nullable=False) client_asset_id: Mapped[int] = mapped_column(ForeignKey("client_assets.id"), nullable=False) gmal_asset_id: Mapped[int] = mapped_column(ForeignKey("gmal_assets.id"), nullable=False) role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"), nullable=False) base_hours: Mapped[float | None] = mapped_column(Numeric(10, 2)) volume: Mapped[int] = mapped_column(Integer, default=1) total_hours: Mapped[float | None] = mapped_column(Numeric(10, 2)) manual_override: Mapped[float | None] = mapped_column(Numeric(10, 2)) notes: Mapped[str | None] = mapped_column(Text) project: Mapped["Project"] = relationship(back_populates="ratecard_lines") client_asset: Mapped["ClientAsset"] = relationship() gmal_asset: Mapped["GmalAsset"] = relationship() role: Mapped["Role"] = relationship() __table_args__ = ( Index("idx_ratecard_project", "project_id"), )