from sqlalchemy import ForeignKey, Integer, String, Text, text from sqlalchemy.orm import Mapped, mapped_column from api.v1.common.base_models import TenantScopedMixin, TimestampMixin from core.database import Base class Case(Base, TenantScopedMixin, TimestampMixin): """Expediente: hilo maestro de un trámite (Oportunidad → Solicitud → Cotización → Operación → Factura). Una sola referencia (``EXP…``) que agrupa toda la historia. Nace al crear la Oportunidad y se hereda a las entidades siguientes vía ``case_id``. """ __tablename__ = "cases" __table_args__ = {"schema": "crm"} id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True) reference: Mapped[str | None] = mapped_column(String(40), nullable=True, index=True) # folio EXP... account_id: Mapped[int | None] = mapped_column( Integer, ForeignKey("crm.accounts.id"), nullable=True, index=True ) title: Mapped[str | None] = mapped_column(String(255), nullable=True) # Etapa más avanzada alcanzada: oportunidad|solicitud|cotizacion|operacion|facturacion|cerrado stage: Mapped[str] = mapped_column(String(20), nullable=False, server_default=text("'oportunidad'")) # abierto | cerrado status: Mapped[str] = mapped_column(String(20), nullable=False, server_default=text("'abierto'"), index=True) created_by: Mapped[str | None] = mapped_column(String(64), nullable=True) updated_by: Mapped[str | None] = mapped_column(String(64), nullable=True) # ── Espejo del carril hacia EFC (T2026-08-046) ────────────────────────────────────────── # EFC es la fuente única de los documentos del expediente: cada expediente se refleja allá # como un *pedimento provisional* y los archivos viven en su MinIO, no en el del CRM. # # Estas columnas son un ESPEJO, nunca el handle. El handle con el que el CRM habla de este # expediente es su ``id`` y su ``reference``: ``efc_pedimento_id`` es un caché de la # resolución, y del lado de EFC el ``pedimento_app`` es mutable —se reescribe al completar # el provisional con la data aduanera real—, así que apoyarse en él rompería justo cuando # llegue esa data. La liga vive en EFC, en la tabla desechable ``pedimento_expediente``. efc_organizacion_id: Mapped[str | None] = mapped_column(String(36), nullable=True) efc_pedimento_id: Mapped[str | None] = mapped_column(String(36), nullable=True) # INMUTABLE una vez asignado: es la carpeta de MinIO donde EFC guarda los objetos de este # expediente. Que no cambie nunca es lo que permite completar el pedimento sin mover ni un # archivo. efc_storage_token: Mapped[str | None] = mapped_column(String(25), nullable=True) # PENDING | LINKED | FAILED efc_link_state: Mapped[str] = mapped_column( String(20), nullable=False, server_default=text("'PENDING'"), index=True ) # El diagnóstico se guarda en la fila para que se vea en la ficha del expediente, sin # obligar a nadie a ir a los logs del worker. efc_error_code: Mapped[str | None] = mapped_column(String(60), nullable=True) efc_error_detail: Mapped[str | None] = mapped_column(Text, nullable=True)