24 KiB
data-model Specification
Purpose
Схема базы данных ядра совпадает с текущей моделью данных DBML (v5.18) один в один: таблицы, колонки, типы, enum'ы, ключи и индексы; миграции применяются к пустой базе и полностью откатываются; расхождение между ORM и DBML невозможно пропустить.
Requirements
Requirement: Схема БД равна DBML v5.21
ORM-метаданные SHALL содержать ровно 44 таблицы и 33 enum'а из ../docs/model/data-model-v5.21.dbml в схеме PostgreSQL
из настройки DB_SCHEMA (по умолчанию dkb; в контуре — OPENBAO__APP__DB_SCHEMA; простой идентификатор
[a-z_][a-z0-9_]{0,62}, не начинающийся с pg_, иначе приложение не стартует); сверка с DBML идёт в эталонной схеме dkb;
имена таблиц, колонок, enum'ов и их значений SHALL совпадать с DBML буквально; nullability, PK, UNIQUE, FK, значения
по умолчанию и индексы SHALL соответствовать DBML, включая частичные уникальные индексы (related_group_member
(related_group_id, legal_entity_id) и (legal_entity_id, kind) WHERE is_current, consolidation_lock WHERE
released_at IS NULL) и NULLS NOT DISTINCT на document (stored_file_id, document_type, osv_account_code).
related_group_member.kind SHALL быть NOT NULL и равняться related_group.kind группы (проверяет ядро при вставке).
audit_action SHALL содержать LEGAL_ENTITY_CHANGE и USER_CHANGE; osv_data_type SHALL содержать NU (после BU).
Enum counterparty_match_kind SHALL содержать INN, NAME, ALIAS, MANUAL, PROPAGATED; колонка
osv_entry.counterparty_match_kind (nullable) SHALL быть NULL тогда и только тогда, когда counterparty_id NULL
(проверяет ядро при записи). app_user.email SHALL храниться в нижнем регистре. Миграция 0004_dbml_v5_16 SHALL
создавать enum и колонку и заполнять её по уже смапленным строкам (ИНН строки без пробелов = legal_entity.inn → INN;
trim(counterparty_name) = legal_entity.name → NAME; остальные с counterparty_id → ALIAS); откат SHALL удалять
колонку и enum. Колонка kpp (varchar, nullable, без индексов и уникальности; уникален только inn) SHALL быть
последней в legal_entity и legal_entity_history; формат и запрет КПП у ИП проверяет ядро. Миграция 0005_dbml_v5_17
SHALL добавлять обе колонки (существующие записи — kpp IS NULL), откат — удалять.
Колонка stored_file.consolidation_id (uuid, nullable, FK → consolidation.id; v5.18) SHALL быть последней в
stored_file; индекс ix_stored_file_legal_entity_id_consolidation_id (legal_entity_id, consolidation_id). Миграция
0006_dbml_v5_18 SHALL добавлять колонку, FK и индекс и заполнять колонку по consolidation_document: файлу с документом,
привязанным к версии, — консолидация этой версии; при привязках к версиям разных консолидаций — самый ранний период
(год, затем квартал); файлы без привязок остаются NULL; откат SHALL удалять индекс и колонку. Поля, отсутствующие в
DBML, MUST NOT существовать.
Схема v5.19 (RSHB/arm-ka-docs#116) SHALL добавлять: таблицу document_issue и enum'ы statement_line_status, statement_form_variant, document_issue_kind и прочие enum'ы DBML v5.19; колонки document.file_ordinal (уникальна в паре (stored_file_id, file_ordinal)), recognition_job.statements_version, related_group.crm_id (varchar, nullable, уникальна), в financial_statement_meta — form_variant, needs_review_fields, header_source, organization_name_original_value, organization_name_modified; в financial_statement_column — period_kind, label_confidence; в financial_statement_line — row_index, source_ref, line_status, line_code_text, parent_row_index, needs_review, is_confirmed; в financial_statement_value — source_text, needs_review, is_confirmed (с confirmed_by/at). Уникальность (meta_id, line_code) снимается, добавляется частичная уникальность (meta_id, source_ref) WHERE source_ref IS NOT NULL; уникальность документа (stored_file_id, document_type, osv_account_code) становится частичной (только для типов, кроме BALANCE / INCOME). Состав и порядок колонок — буквально по DBML v5.19.
Схема v5.20 SHALL добавлять в конец document колонки is_modified (boolean NOT NULL DEFAULT false), original_values
(jsonb, nullable), modified_by (uuid, nullable, FK → app_user.id) и modified_at (timestamptz, nullable); схема v5.21 SHALL
добавлять значение FILE_REPROCESS в enum audit_action. Состав и порядок колонок — буквально по DBML v5.21.
Scenario: Сверка ORM с DBML
- WHEN выполняется
python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.21.dbml(make dbml-check) - THEN вывод пуст и код возврата 0
Scenario: Лишняя колонка в ORM
- WHEN в ORM добавлена колонка, которой нет в DBML (или удалена существующая)
- THEN скрипт печатает
таблица.колонка: отсутствует в DBML(илиотсутствует в ORM) и возвращает код 1
Scenario: Расхождение типа или nullability
- WHEN колонка
osv_entry.opening_debitобъявлена в ORM какNumeric(18, 4)илиNOT NULL - THEN скрипт сообщает
ожидалось numeric(18,2) NULL / фактически numeric(18,4) NOT NULLи возвращает код 1
Scenario: ЮЛ одновременно в ГСК и ГСЗ
- WHEN для одного
legal_entity_idвставлены две текущие строкиrelated_group_memberсkind = gskиkind = gsz - THEN обе вставки проходят; третья текущая строка с
kind = gskнарушает уникальный индекс
Scenario: Строка налогового учёта
- WHEN в
osv_entryвставляется строка сdata_type = 'NU' - THEN вставка проходит
Scenario: Backfill при миграции
- WHEN до миграции 0004 есть строки с
counterparty_id, найденным по ИНН, по имени и по алиасу, и строка безcounterparty_id - THEN после
alembic upgrade headу нихcounterparty_match_kind=INN,NAME,ALIASи NULL соответственно;alembic downgrade 0003удаляет колонку и enum
Scenario: Backfill консолидации файла при миграции 0006
- WHEN до миграции 0006 есть файл
Aс документом, привязанным к версии консолидации 2025 Q2 и к версии консолидации 2025 Q1, и файлBбез привязок - THEN после
alembic upgrade headуAconsolidation_id= консолидация 2025 Q1, уB— NULL; колонка последняя вstored_file;alembic downgrade 0005_dbml_v5_17удаляет индекс и колонку, повторныйupgradeих возвращает
Scenario: Сверка с DBML v5.19
- WHEN выполняется
make dbml-checkпо../docs/model/data-model-v5.21.dbml - THEN таблицы, колонки, enum'ы, ключи и индексы ORM совпадают с DBML без расхождений
Requirement: Миграции применяются с нуля и откатываются
Цепочка ревизий 0001_dbml_v5_13 → 0002_dbml_v5_14 → 0003_dbml_v5_15 → 0004_dbml_v5_16 → 0005_dbml_v5_17 →
0006_dbml_v5_18 SHALL создавать схему из DB_SCHEMA (если нет), все
enum'ы, таблицы, ограничения, индексы и последовательности кодов (company_group_code_seq, related_group_code_seq)
на пустой базе PostgreSQL 16 (требуется ≥ 15); 0003 SHALL добавлять NU в osv_data_type, приводить
app_user.email к нижнему регистру и создавать индексы ix_osv_entry_parent_entry_id, ix_osv_entry_recognition_job_id,
ix_stored_file_current_recognition_job_id (DBML v5.15 п. 4); downgrade до base SHALL удалять всё созданное (новые значения enum — через
пересоздание типа), оставляя пустую схему; повторный upgrade после downgrade SHALL проходить без ошибок.
Scenario: Полный цикл
- WHEN на пустой БД выполняются
alembic upgrade head,alembic downgrade base,alembic upgrade head - THEN все три команды завершаются успешно, после последней в
information_schema.tablesсхемыDB_SCHEMA43 таблицы модели (+ служебнаяalembic_version) и вpg_type28 enum-типов
Scenario: Откат до v5.14
- WHEN после
upgrade headвыполняетсяalembic downgrade 0002_dbml_v5_14 - THEN
osv_data_typeне содержитNU, индексов v5.15 п. 4 нет; регистрapp_user.emailне возвращается (данные)
Scenario: КПП добавляется и откатывается
- WHEN на базе с ЮЛ выполняется
alembic upgrade head, затемalembic downgrade 0004_dbml_v5_16 - THEN после
upgradeуlegal_entityиlegal_entity_historyесть последняя колонкаkpp varchar NULL, у существующих ЮЛkpp IS NULL; послеdowngradeколонок нет, повторныйupgradeих возвращает
Scenario: Нестандартная схема
- WHEN
DB_SCHEMA = dkb_altи на пустой БД выполняетсяalembic upgrade head - THEN таблицы и enum'ы модели создаются в схеме
dkb_alt, приложение работает с ней; схемаdkbне создаётся
Scenario: Автогенерация ничего не находит
- WHEN после
upgrade headвыполняетсяalembic revision --autogenerateна той же БД - THEN сгенерированная ревизия не содержит операций (пустые
upgrade/downgrade)
Requirement: Версия DBML зафиксирована в коде
Пакет моделей SHALL объявлять константу версии DBML (DBML_VERSION = "5.23"); скрипт сверки SHALL сравнивать её с версией в шапке файла
DBML и падать при расхождении — смена модели без осознанного обновления ORM невозможна.
Scenario: Новая версия DBML без обновления ORM
- WHEN в
docs/model/появляетсяdata-model-v5.24.dbml, а константа в ORM —5.23 - THEN сверка против нового файла падает с сообщением о версии до сравнения таблиц
Requirement: Деньги и проценты — точные типы
Все денежные колонки SHALL быть numeric(18,2), проценты — numeric(7,4)/numeric(9,4), уверенность — numeric(5,2);
ORM SHALL отдавать их как Decimal, float в моделях MUST NOT встречаться.
Scenario: Чтение денежной колонки
- WHEN из
osv_entryчитается строка соclosing_debit = 106311116.46 - THEN значение имеет тип
Decimalи равноDecimal("106311116.46")
Requirement: Миграция DBML v5.19
Одна миграция Alembic SHALL приводить схему к DBML v5.19 и только добавлять колонки, таблицу, enum'ы и индексы и
заполнять значения по умолчанию (Р-24); §7 по существующим формам миграция MUST NOT считать, импорта копии
arm_statements в ревизии нет. Перед выкладкой база очищается от форм ББ/ОФР (требование «Очистка форм перед
выкладкой»), но миграция SHALL проходить и на непустой базе (стенды разработчиков, база, очищенная не полностью):
row_index — NULL у строк с line_code_original_value IS NULL (созданных правкой), у остальных — по возрастанию
line_code внутри формы с 0; line_status = ok, needs_review строк — false, needs_review значений — НЕ
values_ok (values_ok — правлено, или is_recognized не false и confidence равна 100 или пуста), source_text = string_value, is_confirmed = false, period_kind — absolute при period_date, иначе none, form_variant = null, header_source — из *_original_value и inn_confidence, organization_name_original_value = organization_name, document.file_ordinal — по (created_at, id) внутри файла с 0 (у всех документов),
related_group.crm_id — null; document_issue — пуст. Корректность нарушений у форм, оставшихся с до-миграционных
времён, не гарантируется: у них нет записей document_issue, поэтому до первой правки экран не показывает нарушений
§7, а POST …/confirm не отвечает CHECKS_NOT_PASSED; первый PATCH пересчитывает форму общим кодом. Это цена Р-24; на
контуре её снимает очистка. Сверка check_dbml_vs_orm.py — против data-model-v5.19.dbml.
Откат (downgrade) SHALL быть гарантирован на пустой БД и на данных, которые схема v5.18 может представить без потери
первичных данных и ручного ввода. downgrade SHALL останавливаться с ошибкой, называющей причину и число записей, и
ничего не менять, если есть: две строки с одним кодом в форме, строка с line_code = null, две формы одного типа в файле,
непустой related_group.crm_id, organization_name_modified = true (исходное наименование хранится только в новой
колонке). Остальные новые колонки и таблица document_issue — производные и сигнальные данные v5.19 (needs_review,
source_text, header_source, is_confirmed с confirmed_by/at, period_kind, label_confidence, form_variant,
needs_review_fields, file_ordinal, statements_version, document_issue): downgrade удаляет их и пишет в журнал
миграции, сколько записей с непустыми значениями удалено (подтверждения аналитика остаются в audit_event). Это не
«молчаливое удаление»: потеря названа в журнале и в этом требовании.
Scenario: Цикл миграций
- WHEN
alembic upgrade head→downgrade base→upgrade headна чистой БД - THEN все три шага проходят, сверка ORM с DBML v5.19 — без расхождений
Scenario: Миграция на непустой базе
- WHEN на стенде есть форма ББ, загруженная до v5.19, с несошедшимся
1600 = 1700, и выполнена миграция - THEN миграция проходит: у строк
row_indexпо коду,line_status = ok, у документаfile_ordinal, записейdocument_issueнет — нарушение до первой правки не показано (корректность не гарантируется); после любого PATCH форма пересчитана, и записьRELATION_FAILEDпоявляется
Scenario: Строка правки до миграции
- WHEN до миграции у формы есть строка 1250, созданная правкой (
line_code_original_value = null), с суммой - THEN после миграции у неё
row_index = null,origin = template; строки источника пронумерованы с 0
Scenario: Откат на данных v2.0
- WHEN в БД есть форма с двумя строками 2410, выполняется
downgradeниже v5.19 - THEN откат останавливается с ошибкой о повторе кода; данные не изменены
Scenario: Откат после подтверждений
- WHEN в БД нет несовместимых данных, но у ячеек есть
is_confirmed = trueи записиdocument_issue - THEN
downgradeпроходит, удаляет колонки и таблицу и пишет в журнал число удалённых подтверждений и записей
Requirement: Миграция DBML v5.20
Одна добавочная миграция после 0007_dbml_v5_19 SHALL добавить четыре колонки document по DBML v5.20; существующие записи получают is_modified = false и остальные поля NULL. Миграция MUST NOT менять документы, их строки, подтверждения и привязки. Downgrade SHALL до любых изменений проверить отсутствие document.is_modified = true и при наличии остановиться с ошибкой и числом записей.
Scenario: Обновление существующих ОСВ
- WHEN миграция применяется к базе с подтверждёнными ОСВ РБ
- THEN добавлены четыре поля, данные ОСВ, id, file_ordinal, подтверждения и привязки сохранены; сверка с DBML v5.20 проходит
Scenario: Защита от потери исправлений
- WHEN хотя бы один документ имеет is_modified = true и запрошен downgrade до v5.19
- THEN откат прекращается до DDL с причиной и числом записей, схема и данные не меняются
Scenario: Пустая база и нестандартная схема
- WHEN выполняется upgrade head, downgrade base, upgrade head на пустой базе с настроенным DB_SCHEMA
- THEN цикл успешен, ORM соответствует DBML v5.20, autogenerate пуст
Requirement: Миграция DBML v5.21 и действие аудита повторной обработки
Отдельная миграция v5.21 после миграции v5.20 SHALL добавить только FILE_REPROCESS в enum audit_action по DBML v5.21. Миграция SHALL сохранять все существующие значения и события. Downgrade MUST NOT удалять либо переписывать audit_event; при наличии FILE_REPROCESS он SHALL завершаться ошибкой до изменения схемы.
Scenario: Схема после дополнения модели
- WHEN миграция v5.21 применена к БД v5.20
- THEN событие нового типа записывается, прежние события читаются без изменений, сверка DBML успешна
Scenario: Откат после ручного повтора
- WHEN существует audit_event с FILE_REPROCESS и выполняется downgrade с v5.21 к v5.20
- THEN ошибка с причиной и количеством несовместимых событий до DDL, события и схема сохранены
Scenario: Независимый откат enum
- WHEN в БД v5.21 нет FILE_REPROCESS и выполняется downgrade к v5.20
- THEN удалено только новое значение enum; четыре поля document, исходные значения и привязки сохранены
Requirement: Миграция DBML v5.23 — версия правил элиминации обязательна
Миграция 0010_dbml_v5_23 после 0009_dbml_v5_21 SHALL заполнить пустой consolidation_version.elimination_rules_version
номером версии правил, действовавшей на московскую дату created_at версии (пустой created_at — текущая дата), а если
такой нет — последней открытой версии (valid_to IS NULL), затем сделать колонку NOT NULL. Если после заполнения
остаются пустые значения (правил нет вовсе), миграция SHALL завершиться ошибкой с числом таких версий до изменения схемы.
Откат SHALL снимать только NOT NULL, значения сохраняются. v5.22 структуру БД не меняла, отдельной миграции у неё нет.
Scenario: Заполнение по дате создания
- WHEN правила v1 действуют до 30.06.2026, v2 — с 01.07.2026, версии созданы 01.03.2026, 30.06.2026 21:30 UTC и без даты
- THEN после
upgrade headу них 1, 2 и 2; колонка NOT NULL;downgrade 0009_dbml_v5_21снимает NOT NULL, значения остаются
Scenario: Версии без правил
- WHEN в БД есть версия консолидации и нет ни одного правила элиминации
- THEN
upgrade headпадает с ошибкой «миграция v5.23 невозможна…», ревизия остаётся0009_dbml_v5_21