"""SQLAlchemy ORM models for tracking documents, reports, and sections.""" import enum import uuid from datetime import UTC, datetime from sqlalchemy import DateTime, Enum, ForeignKey, Index, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db.database import Base def _utcnow() -> datetime: return datetime.now(UTC) class IngestStatus(enum.StrEnum): pending = "pending" processing = "processing" complete = "complete" failed = "failed" class ReportStatus(enum.StrEnum): pending = "pending" generating = "generating" complete = "complete" partial = "partial" failed = "failed" class Document(Base): """Represents an uploaded file and its ingestion lifecycle.""" __tablename__ = "documents" id: Mapped[str] = mapped_column( String(36), primary_key=True, default=lambda: str(uuid.uuid4()) ) tenant_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True) filename: Mapped[str] = mapped_column(String(512), nullable=False) file_path: Mapped[str] = mapped_column(String(1024), nullable=False) status: Mapped[IngestStatus] = mapped_column( Enum(IngestStatus), default=IngestStatus.pending ) error_message: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=_utcnow ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=_utcnow, onupdate=_utcnow ) # RICS Home Survey product tier for this upload: 1 = Condition Report, 2 = HomeBuyer, 3 = Building Survey. survey_level: Mapped[int | None] = mapped_column(Integer, nullable=True) reports: Mapped[list["Report"]] = relationship( "Report", back_populates="document" ) class Report(Base): """A generation job tied to a document.""" __tablename__ = "reports" id: Mapped[str] = mapped_column( String(36), primary_key=True, default=lambda: str(uuid.uuid4()) ) tenant_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True) document_id: Mapped[str] = mapped_column( String(36), ForeignKey("documents.id"), nullable=False ) status: Mapped[ReportStatus] = mapped_column( Enum(ReportStatus), default=ReportStatus.pending ) error_message: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=_utcnow ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=_utcnow, onupdate=_utcnow ) generation_started_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, default=None, doc="Set when status flips to generating; used by stale-job sweeper.", ) # Survey product tier this report job uses for library RAG filtering (may override the primary document). survey_level: Mapped[int | None] = mapped_column(Integer, nullable=True) document: Mapped[Document] = relationship("Document", back_populates="reports") sections: Mapped[list["ReportSection"]] = relationship( "ReportSection", back_populates="report", cascade="all, delete-orphan" ) class ReportSection(Base): """A single adapted section within a generated report.""" __tablename__ = "report_sections" __table_args__ = ( # Speeds up the upsert lookup in _persist_section and the GET /sections query. Index("ix_report_sections_report_section", "report_id", "section_code", unique=True), ) id: Mapped[str] = mapped_column( String(36), primary_key=True, default=lambda: str(uuid.uuid4()) ) report_id: Mapped[str] = mapped_column( String(36), ForeignKey("reports.id"), nullable=False ) section_code: Mapped[str] = mapped_column(String(64), nullable=False) text: Mapped[str] = mapped_column(Text, nullable=False) confidence: Mapped[float] = mapped_column(default=0.0) provenance: Mapped[str] = mapped_column(Text, default="[]") cached: Mapped[bool] = mapped_column(default=False) report: Mapped[Report] = relationship("Report", back_populates="sections") class ReportSectionPhoto(Base): """A photo uploaded for a specific report section (evidence for generation + export).""" __tablename__ = "report_section_photos" __table_args__ = ( Index("ix_rsp_report_section", "report_id", "section_code"), Index("ix_rsp_tenant_report", "tenant_id", "report_id"), ) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) tenant_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True) report_id: Mapped[str] = mapped_column(String(36), ForeignKey("reports.id"), nullable=False, index=True) section_code: Mapped[str] = mapped_column(String(64), nullable=False, index=True) file_path: Mapped[str] = mapped_column(String(1024), nullable=False) original_filename: Mapped[str] = mapped_column(String(512), nullable=False) content_type: Mapped[str] = mapped_column(String(128), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)