Merge pull request 'OpenSpec-спеки по коду: DBML v5.17 (КПП), схема из DB_SCHEMA (#55)' (#56) from feat/55-specs-v517 into main
Reviewed-on: #56
This commit is contained in:
commit
53ac9f19ed
@ -1,13 +1,15 @@
|
||||
# data-model Specification
|
||||
|
||||
## Purpose
|
||||
Схема базы данных ядра совпадает с моделью данных DBML v5.13 один в один: таблицы, колонки, типы, enum'ы, ключи и
|
||||
Схема базы данных ядра совпадает с текущей моделью данных DBML (v5.17) один в один: таблицы, колонки, типы, enum'ы, ключи и
|
||||
индексы; миграции применяются к пустой базе и полностью откатываются; расхождение между ORM и DBML невозможно пропустить.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Схема БД равна DBML v5.16
|
||||
ORM-метаданные SHALL содержать ровно 43 таблицы и 28 enum'ов из `../docs/model/data-model-v5.16.dbml` в схеме `dkb`;
|
||||
### 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
|
||||
@ -19,10 +21,12 @@ Enum `counterparty_match_kind` SHALL содержать `INN`, `NAME`, `ALIAS`,
|
||||
(проверяет ядро при записи). `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. Поля, отсутствующие в DBML, MUST NOT существовать.
|
||||
колонку и 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.16.dbml`
|
||||
- **WHEN** выполняется `python scripts/check_dbml_vs_orm.py ../../docs/model/data-model-v5.17.dbml` (`make dbml-check`)
|
||||
- **THEN** вывод пуст и код возврата 0
|
||||
|
||||
#### Scenario: Лишняя колонка в ORM
|
||||
@ -47,7 +51,8 @@ Enum `counterparty_match_kind` SHALL содержать `INN`, `NAME`, `ALIAS`,
|
||||
`alembic downgrade 0003` удаляет колонку и enum
|
||||
|
||||
### Requirement: Миграции применяются с нуля и откатываются
|
||||
Цепочка ревизий `0001_dbml_v5_13` → `0002_dbml_v5_14` → `0003_dbml_v5_15` SHALL создавать схему `dkb` (если нет), все
|
||||
Цепочка ревизий `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`,
|
||||
@ -56,23 +61,32 @@ enum'ы, таблицы, ограничения, индексы и послед
|
||||
|
||||
#### Scenario: Полный цикл
|
||||
- **WHEN** на пустой БД выполняются `alembic upgrade head`, `alembic downgrade base`, `alembic upgrade head`
|
||||
- **THEN** все три команды завершаются успешно, после последней в `information_schema.tables` схемы `dkb` 43 таблицы модели (+ служебная `alembic_version`)
|
||||
и в `pg_type` 27 enum-типов
|
||||
- **THEN** все три команды завершаются успешно, после последней в `information_schema.tables` схемы `DB_SCHEMA` 43 таблицы модели (+ служебная `alembic_version`)
|
||||
и в `pg_type` 28 enum-типов
|
||||
|
||||
#### Scenario: Откат на один шаг
|
||||
#### 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 (`5.15`); скрипт сверки SHALL сравнивать её с версией в шапке файла
|
||||
Пакет моделей SHALL объявлять константу версии DBML (`DBML_VERSION = "5.17"`); скрипт сверки SHALL сравнивать её с версией в шапке файла
|
||||
DBML и падать при расхождении — смена модели без осознанного обновления ORM невозможна.
|
||||
|
||||
#### Scenario: Новая версия DBML без обновления ORM
|
||||
- **WHEN** в `docs/model/` появляется `data-model-v5.16.dbml`, а константа в ORM — `5.15`
|
||||
- **WHEN** в `docs/model/` появляется `data-model-v5.18.dbml`, а константа в ORM — `5.17`
|
||||
- **THEN** сверка против нового файла падает с сообщением о версии до сравнения таблиц
|
||||
|
||||
### Requirement: Деньги и проценты — точные типы
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
## Purpose
|
||||
API справочников ядра: группы компаний (ГК), юридические лица и ИП с алиасами и историей, группы связанных компаний и
|
||||
заёмщиков (ГСК/ГСЗ) с составом, филиалы. Все изменения идут через сервисы с историей (SCD4/SCD2) и записью в `audit_event`;
|
||||
имена полей — как в DBML v5.14.
|
||||
имена полей — как в текущей DBML (v5.17).
|
||||
|
||||
## Requirements
|
||||
|
||||
@ -42,10 +42,13 @@ API справочников ядра: группы компаний (ГК), ю
|
||||
### Requirement: Юридическое лицо / ИП — создание, чтение, изменение
|
||||
`POST /api/v1/directories/legal-entities` (`DIRECTORY_EDIT`) SHALL создавать запись с `inn` (ЮЛ — ровно 10 цифр, ИП — 12;
|
||||
иначе 422 `INVALID_INN`), `entity_type` (`ЮЛ` | `ИП`), `name`, `company_group_id` (NULL = монозаёмщик), `regional_office_id`
|
||||
(по умолчанию — филиал ГК), `crm_id`; `inn` уникален (409 `INN_EXISTS`). `PATCH …/{id}` SHALL менять `name`, `entity_type`,
|
||||
`crm_id`, `regional_office_id` со снимком в `legal_entity_history(event_type = UPDATE)` и `audit_event(LEGAL_ENTITY_CHANGE)`.
|
||||
(по умолчанию — филиал ГК), `crm_id`, `kpp`; `inn` уникален (409 `INN_EXISTS`). `kpp` (DBML v5.17, `05-domain-rules.md` §13.5)
|
||||
необязателен у ЮЛ и запрещён у ИП; нормализуется (пробелы убираются, буквы — в верхний регистр) и SHALL быть 9 символов
|
||||
`[0-9]{4}[0-9A-Z]{2}[0-9]{3}`, иначе 422 `INVALID_KPP`; пустая строка — «КПП нет»; уникальности у КПП нет.
|
||||
`PATCH …/{id}` SHALL менять `name`, `entity_type`, `crm_id`, `regional_office_id`, `kpp` (`kpp: null` очищает КПП; смена типа
|
||||
на ИП при непустом итоговом КПП — 422 `INVALID_KPP`) со снимком в `legal_entity_history(event_type = UPDATE)` и `audit_event(LEGAL_ENTITY_CHANGE)`.
|
||||
`GET …` SHALL давать список с фильтрами `company_group_id`, `outside_group` (`company_group_id IS NULL`), `inn`
|
||||
(точно), `q` (подстрока имени/ИНН), `closed` (по `close_date`), пагинацией; `GET …/{id}` — карточку с текущими
|
||||
(точно), `q` (подстрока имени/ИНН/КПП), `closed` (по `close_date`), пагинацией; `GET …/{id}` — карточку с текущими
|
||||
членствами ГСК/ГСЗ и алиасами; `GET …/{id}/history` — снимки. В списке и карточке SHALL быть `documents_count` — число
|
||||
документов файлов ЮЛ, кроме файлов `SUPERSEDED`.
|
||||
|
||||
@ -53,6 +56,14 @@ API справочников ядра: группы компаний (ГК), ю
|
||||
- **WHEN** создаётся ИП с ИНН из 10 цифр
|
||||
- **THEN** ответ 422 с кодом `INVALID_INN`, запись не создана
|
||||
|
||||
#### Scenario: КПП у ИП
|
||||
- **WHEN** создаётся ИП с `kpp = "770101001"` или ЮЛ с КПП из 8 символов
|
||||
- **THEN** ответ 422 с кодом `INVALID_KPP`, запись не создана
|
||||
|
||||
#### Scenario: Очистка КПП
|
||||
- **WHEN** у ЮЛ с КПП выполняется `PATCH` с `{"kpp": null}`
|
||||
- **THEN** `kpp` = NULL, прежнее значение — в снимке `legal_entity_history(event_type = UPDATE)` и в `audit_event`
|
||||
|
||||
#### Scenario: Монозаёмщик
|
||||
- **WHEN** ЮЛ создано без `company_group_id`
|
||||
- **THEN** оно отдаётся в списке с `outside_group = true` и видно пользователям филиала `regional_office_id`
|
||||
|
||||
@ -11,7 +11,8 @@
|
||||
`POST /api/v1/directories/import` (multipart: `file`; поля `dry_run` (по умолчанию `true`), `on_existing` ∈ {`skip`, `update`},
|
||||
по умолчанию `skip`; право `DIRECTORY_ADMIN`) SHALL принимать xlsx (первый лист) или csv (UTF-8, разделитель `;` или `,`)
|
||||
размером до 20 МБ, сопоставлять заголовки по нормализованному тексту (`ИНН`, `iD ГК`, `Базовый РФ ГК в CRM`, `Группа
|
||||
компаний`, `Организация`, `ГСК`, `ГСЗ`, `Тип`; порядок и лишние колонки не важны). Отсутствие любой из обязательных колонок
|
||||
компаний`, `Организация`, `ГСК`, `ГСЗ`, `Тип`, `КПП`; порядок и лишние колонки не важны). Колонка `КПП` (v5.17) необязательна:
|
||||
её отсутствие и пустая ячейка — не ошибка. Отсутствие любой из обязательных колонок
|
||||
(`ИНН`, `iD ГК`, `Группа компаний`, `Организация`, `Тип`) → 422 `MISSING_COLUMNS` с перечнем; нечитаемый файл → 422
|
||||
`UNSUPPORTED_FILE`; превышение размера → 413.
|
||||
|
||||
@ -25,12 +26,14 @@
|
||||
|
||||
### Requirement: Построчные правила и коды отчёта
|
||||
Для каждой строки импорт SHALL применять §13.2 и присваивать ровно один итоговый код из перечня: `UNSUPPORTED_TYPE`,
|
||||
`INVALID_INN`, `DUPLICATE_IN_FILE`, `LE_IN_OTHER_GROUP`, `GK_NAME_CONFLICT` (строка пропущена) или `LE_CREATED`, `LE_UPDATED`,
|
||||
`INVALID_INN`, `INVALID_KPP`, `DUPLICATE_IN_FILE`, `LE_IN_OTHER_GROUP`, `GK_NAME_CONFLICT` (строка пропущена) или `LE_CREATED`, `LE_UPDATED`,
|
||||
`LE_EXISTS` (строка применена), плюс сопутствующие события `RF_CREATED`, `GK_CREATED`, `GK_UPDATED`, `GK_EXISTS`,
|
||||
`GSK_CREATED`, `GSZ_CREATED`, `MEMBER_ADDED`, `UNKNOWN_FLAG`. Тип вне {ЮЛ, ИП} (ASSUMPTION(OQ-87)), ИНН не 10/12 цифр
|
||||
(ASSUMPTION(OQ-88)) и ИНН, уже встречавшийся в файле, пропускаются. Филиал по имени создаётся при отсутствии
|
||||
(ASSUMPTION(OQ-89)). ЮЛ, существующее в другой ГК, не переносится (ASSUMPTION(OQ-90)). Флаги «входит в ГСК»/«входит в ГСЗ»
|
||||
создают не более одной ГСК и одной ГСЗ на ГК с именем ГК (ASSUMPTION(OQ-91)). Отсутствие флага членство NOT закрывает.
|
||||
КПП проверяется после ИНН и до дубля в файле по правилам `directories-api` (формат, запрет у ИП) → `INVALID_KPP`.
|
||||
С `on_existing = update` КПП существующего ЮЛ SHALL меняться только непустым значением из файла: пустая ячейка КПП не стирает.
|
||||
|
||||
#### Scenario: Срез «Русский Бисквит», пустая база
|
||||
- **WHEN** импорт `ecp-export-russkiy-biskvit.xlsx` с `dry_run = false`, `on_existing = skip` на базе после `seed`
|
||||
@ -46,6 +49,10 @@
|
||||
- **WHEN** в файле изменено имя существующего ЮЛ и импорт идёт с `on_existing = update`
|
||||
- **THEN** имя обновлено, в `legal_entity_history` снимок `UPDATE`, код строки `LE_UPDATED`
|
||||
|
||||
#### Scenario: КПП у ИП в файле
|
||||
- **WHEN** у строки с типом `ИП` заполнена колонка `КПП`
|
||||
- **THEN** строка пропущена с `INVALID_KPP`, импорт продолжается
|
||||
|
||||
#### Scenario: ЮЛ в другой ГК
|
||||
- **WHEN** ИНН уже состоит в другой ГК
|
||||
- **THEN** строка пропущена с `LE_IN_OTHER_GROUP`, ЮЛ не переведено, импорт продолжается
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user