- data-model: v5.16 → v5.17 (legal_entity(_history).kpp, миграция 0005), схема — настройка DB_SCHEMA (по умолчанию dkb), цепочка миграций до 0005, DBML_VERSION = "5.17", 28 enum-типов; сценарии «КПП добавляется и откатывается», «Нестандартная схема». - directories-api: v5.14 → v5.17; kpp в создании/PATCH (null очищает), поиск q по КПП, INVALID_KPP, ИП без КПП. - directory-import: необязательная колонка «КПП», код INVALID_KPP, пустая ячейка не стирает КПП при update. Код не менялся: спеки догоняют слитые PR #52 и #53. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
9.4 KiB
data-model Specification
Purpose
Схема базы данных ядра совпадает с текущей моделью данных DBML (v5.17) один в один: таблицы, колонки, типы, enum'ы, ключи и индексы; миграции применяются к пустой базе и полностью откатываются; расхождение между ORM и DBML невозможно пропустить.
Requirements
Requirement: Схема БД равна DBML v5.17
ORM-метаданные SHALL содержать ровно 43 таблицы и 28 enum'ов из ../docs/model/data-model-v5.17.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), откат — удалять. Поля, отсутствующие в DBML, MUST NOT существовать.
Scenario: Сверка ORM с DBML
- WHEN выполняется
python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.17.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
Requirement: Миграции применяются с нуля и откатываются
Цепочка ревизий 0001_dbml_v5_13 → 0002_dbml_v5_14 → 0003_dbml_v5_15 → 0004_dbml_v5_16 → 0005_dbml_v5_17 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.17"); скрипт сверки SHALL сравнивать её с версией в шапке файла
DBML и падать при расхождении — смена модели без осознанного обновления ORM невозможна.
Scenario: Новая версия DBML без обновления ORM
- WHEN в
docs/model/появляетсяdata-model-v5.18.dbml, а константа в ORM —5.17 - 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")