hungtran24 2268b6a77a feat: replace tenant auth with app-scoped scoring access
Add app credentials, account-scoped idempotency and per-app terminal notifications. Consolidate the unreleased migration baseline, preserve rule descriptions as AI context, and regenerate provider contracts with regression coverage.
2026-10-02 10:56:58 +07:00

132 lines
6.0 KiB
Python

from datetime import datetime
from typing import Any
from uuid import uuid4
from sqlalchemy import (
JSON,
Boolean,
DateTime,
Float,
ForeignKey,
Index,
Integer,
String,
UniqueConstraint,
false,
func,
)
from sqlalchemy.orm import Mapped, mapped_column
from ai_platform.infrastructure.persistence.database import Base
class AppCredential(Base):
__tablename__ = "app_credentials"
app_id: Mapped[str] = mapped_column(String(128), primary_key=True)
verifier: Mapped[str] = mapped_column(String(64), nullable=False, unique=True)
active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
lead_scoring: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
)
class AppCredentialAuditEvent(Base):
__tablename__ = "app_credential_audit_events"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
app_id: Mapped[str] = mapped_column(String(128), nullable=False)
actor: Mapped[str] = mapped_column(String(254), nullable=False)
action: Mapped[str] = mapped_column(String(16), nullable=False)
occurred_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
class AdminUser(Base):
__tablename__ = "admin_users"
email: Mapped[str] = mapped_column(String(254), primary_key=True)
password_hash: Mapped[str] = mapped_column(String(512), nullable=False)
class AdminSession(Base):
__tablename__ = "admin_sessions"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
admin_email: Mapped[str] = mapped_column(ForeignKey("admin_users.email"), nullable=False)
csrf_hash: Mapped[str] = mapped_column(String(64), nullable=False)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
class AIProfile(Base):
__tablename__ = "ai_profiles"
version: Mapped[str] = mapped_column(String(64), primary_key=True)
model: Mapped[str] = mapped_column(String(128), nullable=False)
provider: Mapped[str] = mapped_column(
String(32), default="aihub", server_default="aihub", nullable=False
)
base_url: Mapped[str | None] = mapped_column(String(2048))
system_prompt: Mapped[str] = mapped_column(String(8000), nullable=False)
timeout_seconds: Mapped[int] = mapped_column(Integer, nullable=False)
limits: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False)
active: Mapped[bool] = mapped_column(
Boolean, default=False, server_default=false(), nullable=False
)
class AIProfileAuditEvent(Base):
__tablename__ = "ai_profile_audit_events"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
actor: Mapped[str] = mapped_column(String(254), nullable=False)
version: Mapped[str] = mapped_column(String(64), nullable=False)
provider: Mapped[str] = mapped_column(String(32), nullable=False)
occurred_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
class ScoringRun(Base):
__tablename__ = "scoring_runs"
__table_args__ = (
UniqueConstraint("app_id", "account_id", "request_id", name="uq_run_app_account_request"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
app_id: Mapped[str] = mapped_column(ForeignKey("app_credentials.app_id"), index=True)
account_id: Mapped[str] = mapped_column(String(255), nullable=False)
request_id: Mapped[str] = mapped_column(String(36), nullable=False)
fingerprint: Mapped[str] = mapped_column(String(64), nullable=False)
request: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False)
profile_version: Mapped[str] = mapped_column(String(64), nullable=False)
status: Mapped[str] = mapped_column(String(24), default="pending", index=True)
rule_score: Mapped[int | None] = mapped_column(Integer)
ai_score: Mapped[int | None] = mapped_column(Integer)
ai_confidence: Mapped[float | None] = mapped_column(Float)
ai_reasoning: Mapped[str | None] = mapped_column(String(2_000))
error_code: Mapped[str | None] = mapped_column(String(64))
final_score: Mapped[int | None] = mapped_column(Integer)
degraded: Mapped[bool] = mapped_column(Boolean, default=False, server_default=false())
result_mode: Mapped[str | None] = mapped_column(String(32))
breakdown: Mapped[list[dict[str, Any]] | None] = mapped_column(JSON)
ai_evidence: Mapped[list[dict[str, Any]] | None] = mapped_column(JSON)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True
)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
class ScoringNotification(Base):
__tablename__ = "scoring_notifications"
__table_args__ = (Index("ix_scoring_notifications_due", "dead", "next_attempt_at"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
run_id: Mapped[str] = mapped_column(
ForeignKey("scoring_runs.id", ondelete="CASCADE"), unique=True, nullable=False
)
app_id: Mapped[str] = mapped_column(String(128), nullable=False)
account_id: Mapped[str] = mapped_column(String(255), nullable=False)
request_id: Mapped[str] = mapped_column(String(36), nullable=False)
status: Mapped[str] = mapped_column(String(24), nullable=False)
attempts: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
next_attempt_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
delivered_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
dead: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)