## RENAMED Requirements - FROM: `### Requirement: Схема БД равна DBML v5.17` - TO: `### Requirement: Схема БД равна DBML v5.18` ## MODIFIED Requirements ### Requirement: Схема БД равна DBML v5.18 ORM-метаданные SHALL содержать ровно 43 таблицы и 28 enum'ов из `../docs/model/data-model-v5.18.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 существовать. #### Scenario: Сверка ORM с DBML - **WHEN** выполняется `python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.18.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` их возвращает ### 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.18"`); скрипт сверки SHALL сравнивать её с версией в шапке файла DBML и падать при расхождении — смена модели без осознанного обновления ORM невозможна. #### Scenario: Новая версия DBML без обновления ORM - **WHEN** в `docs/model/` появляется `data-model-v5.19.dbml`, а константа в ORM — `5.18` - **THEN** сверка против нового файла падает с сообщением о версии до сравнения таблиц