214 lines
24 KiB
Markdown
214 lines
24 KiB
Markdown
# 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` у `A` `consolidation_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_SCHEMA` 43 таблицы модели (+ служебная `alembic_version`)
|
||
и в `pg_type` 28 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`
|