140 lines
9.5 KiB
Python
140 lines
9.5 KiB
Python
"""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)
|