from __future__ import annotations import uuid from datetime import datetime from typing import Any from sqlalchemy import CheckConstraint, DateTime, Index, String, UniqueConstraint, func from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.types import JSON from app.db.base_class import Base class AIApplicationPreviewDecision(Base): """费用申请落单前,由服务端签发并短期持有的不可变建议快照。""" __tablename__ = "ai_application_preview_decisions" __table_args__ = ( UniqueConstraint( "tenant_id", "id", name="uq_ai_application_preview_decisions_tenant_id", ), UniqueConstraint( "tenant_id", "actor_id", "auth_session_id", "issue_request_id", name="uq_ai_application_preview_decisions_issue_request", ), CheckConstraint( "status IN ('issued', 'consumed', 'expired', 'revoked')", name="ck_ai_application_preview_decisions_status", ), CheckConstraint( "decision_source IN ('heuristic', 'rule', 'hybrid', 'server_draft')", name="ck_ai_application_preview_decisions_source", ), CheckConstraint( "consumed_action IS NULL OR consumed_action IN ('save_draft', 'submit')", name="ck_ai_application_preview_decisions_consumed_action", ), CheckConstraint( "status != 'consumed' OR (" "consumed_action IS NOT NULL AND consumed_request_id IS NOT NULL AND " "consumed_claim_id IS NOT NULL AND consumed_business_event_id IS NOT NULL AND " "consumed_final_fingerprint IS NOT NULL AND consumed_at IS NOT NULL)", name="ck_ai_application_preview_decisions_consumed_fields", ), CheckConstraint( "expires_at > created_at", name="ck_ai_application_preview_decisions_expiry", ), Index( "ix_ai_application_preview_decisions_actor_status_expiry", "tenant_id", "actor_id", "status", "expires_at", ), Index( "ix_ai_application_preview_decisions_conversation", "tenant_id", "conversation_id", "created_at", ), ) id: Mapped[str] = mapped_column( String(36), primary_key=True, default=lambda: str(uuid.uuid4()), ) tenant_id: Mapped[str] = mapped_column(String(64), nullable=False) actor_id: Mapped[str] = mapped_column(String(120), nullable=False) auth_session_id: Mapped[str] = mapped_column(String(120), nullable=False) conversation_id: Mapped[str] = mapped_column(String(120), nullable=False, default="") decision_type: Mapped[str] = mapped_column(String(80), nullable=False) decision_source: Mapped[str] = mapped_column(String(20), nullable=False) field_keys_json: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list) field_fingerprints_json: Mapped[dict[str, str]] = mapped_column( JSON, nullable=False, default=dict, ) snapshot_fingerprint: Mapped[str] = mapped_column(String(80), nullable=False) fingerprint_key_version: Mapped[str] = mapped_column(String(40), nullable=False) source_version_json: Mapped[dict[str, Any]] = mapped_column( JSON, nullable=False, default=dict, ) status: Mapped[str] = mapped_column(String(20), nullable=False, default="issued") issue_request_id: Mapped[str] = mapped_column(String(120), nullable=False) consumed_action: Mapped[str | None] = mapped_column(String(30), nullable=True) consumed_request_id: Mapped[str | None] = mapped_column(String(120), nullable=True) consumed_claim_id: Mapped[str | None] = mapped_column(String(36), nullable=True) consumed_business_event_id: Mapped[str | None] = mapped_column(String(36), nullable=True) consumed_final_fingerprint: Mapped[str | None] = mapped_column(String(80), nullable=True) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) consumed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), )