- Tier mapping on projects: configurable label→complexity mapping
- Presets: A/B/C, 1/2/3, Gold/Silver/Bronze
- Stored as JSON on project.tier_mapping
- ClientAsset.client_tier field for tracking which tier an asset belongs to
- GMAL family endpoint: GET /gmal/assets/{id}/family returns all complexity variants
- Looks up by asset_name (NOT by GMAL number increment)
- Verified: families share asset_name across non-sequential GMAL IDs
- Expand to Tiers: POST /projects/{id}/expand-tiers
- Splits each matched asset into N tier variants (one per tier)
- Finds correct GMAL variant by asset_name + complexity_level query
- Creates new ClientAsset + Match per tier with correct GMAL
- Removes original un-tiered asset after expansion
- Frontend: tier preset buttons + expand button on Match Review tab
- Tier tags shown with label → complexity mapping
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
110 lines
4.9 KiB
Python
110 lines
4.9 KiB
Python
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)
|
|
tier_mapping: 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)
|
|
client_tier: Mapped[str | None] = mapped_column(String(50))
|
|
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"),
|
|
)
|