import uuid from datetime import datetime from typing import Any from sqlalchemy import JSON, DateTime, ForeignKey, Integer, String, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.sql import func from app.core.database import Base from app.core.sql_types import GUID as Uuid class ApprovalRequest(Base): __tablename__ = "approval_requests" __mapper_args__ = {"eager_defaults": True} id: Mapped[uuid.UUID] = mapped_column( Uuid, primary_key=True, default=uuid.uuid4, ) external_id: Mapped[str] = mapped_column(String(128), nullable=False, unique=True) approval_type: Mapped[str] = mapped_column(String(64), nullable=False) applicant_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("identity_persons.id"), ) status: Mapped[str] = mapped_column(String(32), nullable=False) source_payload: Mapped[Any | None] = mapped_column(JSON) 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(), ) steps: Mapped[list["ApprovalStep"]] = relationship(back_populates="approval") class ApprovalSubmissionIntent(Base): """Durable intent for one Feishu approval-instance submission. The provider UUID is persisted before the outbound POST. It is Feishu's native idempotency key and is therefore the recovery anchor when a timeout leaves the create result unknown. """ __tablename__ = "approval_submission_intents" __table_args__ = ( UniqueConstraint( "scope", "applicant_id", "idempotency_key", name="uq_approval_submission_intent_key", ), UniqueConstraint( "provider_request_uuid", name="uq_approval_submission_intent_provider_uuid", ), ) __mapper_args__ = {"eager_defaults": True} id: Mapped[uuid.UUID] = mapped_column( Uuid, primary_key=True, default=uuid.uuid4, ) scope: Mapped[str] = mapped_column(String(64), nullable=False) applicant_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("identity_persons.id"), nullable=False, ) idempotency_key: Mapped[str] = mapped_column(String(128), nullable=False) payload_fingerprint: Mapped[str] = mapped_column(String(64), nullable=False) provider_request_uuid: Mapped[str] = mapped_column(String(64), nullable=False) state: Mapped[str] = mapped_column( String(32), nullable=False, default="prepared", ) payload: Mapped[Any] = mapped_column(JSON, nullable=False) external_id: Mapped[str | None] = mapped_column( String(128), nullable=True, unique=True, ) employee_request_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("employee_requests.id"), nullable=True, ) approval_request_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("approval_requests.id"), nullable=True, ) attempt_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) last_error: Mapped[str | None] = mapped_column(String(512), nullable=True) next_attempt_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, index=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(), ) completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) class ApprovalStep(Base): __tablename__ = "approval_steps" id: Mapped[uuid.UUID] = mapped_column( Uuid, primary_key=True, default=uuid.uuid4, ) approval_id: Mapped[uuid.UUID] = mapped_column( Uuid, ForeignKey("approval_requests.id"), nullable=False, ) step_name: Mapped[str] = mapped_column(String(128), nullable=False) approver_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("identity_persons.id"), ) status: Mapped[str] = mapped_column(String(32), nullable=False) acted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) detail: Mapped[Any | None] = mapped_column(JSON) approval: Mapped["ApprovalRequest"] = relationship(back_populates="steps")