arm-core/api/src/models/quality_facts.py
Andrey Epifancev 251c30d5ca feat(#190): DBML v5.25 — ORM и миграция фактов качества первички
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-05 15:49:15 +04:00

140 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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)