"""DBML v5.25 «Метрики качества»: факты качества распознавания первички (AURORA-1759, openspec/changes/quality-metrics). Три append-only таблицы в отдельной схеме PostgreSQL `settings.QUALITY_METRICS_SCHEMA` (по умолчанию `dkb_metrics`, `.base.METRICS_SCHEMA`) — не в `SCHEMA` (`dkb`), где живёт остальная модель. Задаётся явно через `__table_args__ = {"schema": METRICS_SCHEMA}`: `Base`/`Base.metadata` общие для обеих схем (как и enum-типы `pg_enum`, которые остаются в `SCHEMA`) — второй `DeclarativeBase` не нужен. Без FK на рабочие таблицы (`document`, `stored_file`, `recognition_job`, `legal_entity`, `company_group`): факт обязан переживать удаление/перераспознавание документа и физическое удаление первички (design §4). Идентификаторы — простой `uuid`, имена ЮЛ/ГК — копия на момент факта. """ import datetime import uuid from typing import Any from sqlalchemy import Boolean, Date, DateTime, Index, Integer, SmallInteger, String, text from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import Mapped, mapped_column from .base import METRICS_SCHEMA, Base, UUIDPrimaryMixin from .enums import ( DocumentStatus, DocumentType, QualityCause, QualityFailureKind, QualityOutcome, RecognitionPipeline, pg_enum, ) class QualityIntakeFact(UUIDPrimaryMixin, Base): """Документ «как распознано» (design §4.1): один факт на созданный документ BALANCE/INCOME/OSV (DISCLOSURE/OTHER факт не пишут). Пишется в транзакции маппинга, после `map_file`, до `auto_confirm`.""" __tablename__ = "quality_intake_fact" __table_args__ = ( Index("uq_quality_intake_fact_document_id", "document_id", unique=True), Index("ix_quality_intake_fact_legal_entity_id_intake_at", "legal_entity_id", "intake_at"), Index("ix_quality_intake_fact_group_id_intake_at", "group_id", "intake_at"), {"schema": METRICS_SCHEMA}, ) document_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) file_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) job_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) attempt: Mapped[int] = mapped_column(SmallInteger, nullable=False) legal_entity_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) legal_entity_name: Mapped[str] = mapped_column(String, nullable=False) group_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True)) group_name: Mapped[str | None] = mapped_column(String) document_type: Mapped[DocumentType] = mapped_column(pg_enum("document_type"), nullable=False) pipeline: Mapped[RecognitionPipeline] = mapped_column(pg_enum("recognition_pipeline"), nullable=False) is_scan: Mapped[bool | None] = mapped_column(Boolean) service_name: Mapped[str | None] = mapped_column(String) service_version: Mapped[str | None] = mapped_column(String) statements_version: Mapped[str | None] = mapped_column(String) result_schema_version: Mapped[str | None] = mapped_column(String) report_date: Mapped[datetime.date | None] = mapped_column(Date) intake_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), nullable=False) intake_status: Mapped[DocumentStatus] = mapped_column(pg_enum("document_status"), nullable=False) line_count: Mapped[int | None] = mapped_column(Integer) value_count: Mapped[int | None] = mapped_column(Integer) value_needs_review_count: Mapped[int | None] = mapped_column(Integer) line_unreadable_count: Mapped[int | None] = mapped_column(Integer) line_missing_count: Mapped[int | None] = mapped_column(Integer) issue_relation_count: Mapped[int | None] = mapped_column(Integer) issue_soft_count: Mapped[int | None] = mapped_column(Integer) warning_count: Mapped[int | None] = mapped_column(Integer) raw_s3_key: Mapped[str | None] = mapped_column(String) result_s3_key: Mapped[str | None] = mapped_column(String) upload_s3_key: Mapped[str | None] = mapped_column(String) payload: Mapped[Any] = mapped_column(JSONB, nullable=False) payload_schema_version: Mapped[str | None] = mapped_column(String) class QualityOutcomeFact(UUIDPrimaryMixin, Base): """Исход документа (design §4.2): подтверждение (ручное/`auto_confirm`), удаление до подтверждения или автозамена. НЕ unique на `document_id` — повторное подтверждение ОСВ и `DELETED_AFTER_CONFIRM`/ `SUPERSEDED_AFTER_CONFIRM` пишут новый факт поверх прежнего; метрики берут последний по `outcome_at`.""" __tablename__ = "quality_outcome_fact" __table_args__ = ( Index("ix_quality_outcome_fact_document_id_outcome_at", "document_id", "outcome_at"), Index("ix_quality_outcome_fact_outcome_outcome_at", "outcome", "outcome_at"), {"schema": METRICS_SCHEMA}, ) document_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) outcome: Mapped[QualityOutcome] = mapped_column(pg_enum("quality_outcome"), nullable=False) outcome_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), nullable=False) # NULL для DELETED*/SUPERSEDED* — они не входят в матрицу причин (design §6) primary_cause: Mapped[QualityCause | None] = mapped_column(pg_enum("quality_cause")) fixes_value_service: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0")) fixes_value_ours: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0")) fixes_code_service: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0")) fixes_code_ours: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0")) fixes_added_row_service: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0")) fixes_added_row_ours: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0")) fixes_header_service: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0")) fixes_header_ours: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0")) # всегда «наш разбор» (Excel) — не делится по источнику (design §5, последняя строка таблицы) fixes_osv_period_account: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0")) # подтверждён с акцептом жёстких нарушений §7 — условие «документ клиента» (design §5, §14 05-domain-rules.md) checks_accepted: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default=text("false")) issue_count_at_outcome: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0")) report_date: Mapped[datetime.date | None] = mapped_column(Date) confirmed_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True)) # для агрегатов не используется payload: Mapped[Any] = mapped_column(JSONB, nullable=False) payload_schema_version: Mapped[str | None] = mapped_column(String) class QualityFailureFact(UUIDPrimaryMixin, Base): """Технический отказ задания распознавания (`recognition_job.status` error/timeout/dlq) или неповторяемая ошибка маппинга (`MappingFatal`, файл -> `ERROR`; design §4.3). Считается по файлам, не по документам: у отказавшего файла нет документов и типа. `FILE_REPROCESS` создаёт новое задание — отказ первой попытки остаётся фактом, документы удачной попытки получают свои `QualityIntakeFact`/`QualityOutcomeFact`.""" __tablename__ = "quality_failure_fact" __table_args__ = ( Index("uq_quality_failure_fact_job_id", "job_id", unique=True), Index("ix_quality_failure_fact_file_id_attempt", "file_id", "attempt"), {"schema": METRICS_SCHEMA}, ) job_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) file_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) attempt: Mapped[int] = mapped_column(SmallInteger, nullable=False) legal_entity_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False) legal_entity_name: Mapped[str] = mapped_column(String, nullable=False) group_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True)) group_name: Mapped[str | None] = mapped_column(String) pipeline: Mapped[RecognitionPipeline] = mapped_column(pg_enum("recognition_pipeline"), nullable=False) is_scan: Mapped[bool | None] = mapped_column(Boolean) service_name: Mapped[str | None] = mapped_column(String) failure_kind: Mapped[QualityFailureKind] = mapped_column(pg_enum("quality_failure_kind"), nullable=False) error_code: Mapped[str | None] = mapped_column(String) failed_at: Mapped[datetime.datetime] = mapped_column(DateTime(timezone=True), nullable=False)