# directory-import Specification ## Purpose Импорт справочников ГК / ЮЛ / ГСК / ГСЗ / филиалов из выгрузки ЕЦП CRM (xlsx или csv) по правилам §13 `05-domain-rules.md`: построчные решения с кодами, одна транзакция, `dry_run`, стратегия дублей, отчёт в ответе, в `audit_event(DIRECTORY_IMPORT)` и файл в S3. ## Requirements ### Requirement: Приём файла и формат `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. #### Scenario: Реальная выгрузка принята - **WHEN** загружается `fixtures/directories/ecp-export-russkiy-biskvit.xlsx` с `dry_run = true` - **THEN** ответ 200 с отчётом: `rows = 21`, ошибок файла нет #### Scenario: Нет колонки «Тип» - **WHEN** в csv отсутствует колонка `Тип` - **THEN** ответ 422 `MISSING_COLUMNS`, в отчёте перечислена недостающая колонка, в БД ничего не изменено ### Requirement: Построчные правила и коды отчёта Для каждой строки импорт SHALL применять §13.2 и присваивать ровно один итоговый код из перечня: `UNSUPPORTED_TYPE`, `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` - **THEN** создана 1 ГК (`crm_id = 1-AZFL6BQ`, имя «Русский Бисквит»), 1 филиал «Санкт-Петербургский РФ» (`RF_CREATED`), 20 ЮЛ/ИП (`LE_CREATED`: 18 ЮЛ и 2 ИП), 1 ГСК с именем ГК и двумя членами (АО «Русский бисквит», ООО «Пекарня Счастливочная»), 0 ГСЗ; строка типа `ФЛ` (идёт после ИП с тем же ИНН) → `UNSUPPORTED_TYPE` (тип проверяется первым, `DUPLICATE_IN_FILE` в этом файле не возникает) #### Scenario: Повторный импорт идемпотентен - **WHEN** тот же файл импортируется второй раз с `on_existing = skip` - **THEN** все применённые строки получают `LE_EXISTS`/`GK_EXISTS`, число строк в таблицах не меняется #### Scenario: Обновление имени - **WHEN** в файле изменено имя существующего ЮЛ и импорт идёт с `on_existing = update` - **THEN** имя обновлено, в `legal_entity_history` снимок `UPDATE`, код строки `LE_UPDATED` #### Scenario: КПП у ИП в файле - **WHEN** у строки с типом `ИП` заполнена колонка `КПП` - **THEN** строка пропущена с `INVALID_KPP`, импорт продолжается #### Scenario: ЮЛ в другой ГК - **WHEN** ИНН уже состоит в другой ГК - **THEN** строка пропущена с `LE_IN_OTHER_GROUP`, ЮЛ не переведено, импорт продолжается ### Requirement: Транзакция, dry_run, след Импорт SHALL выполняться в одной транзакции: ошибки строк не прерывают его, при `dry_run = true` транзакция откатывается, а отчёт идентичен боевому. При `dry_run = false` SHALL быть записан `audit_event(action = DIRECTORY_IMPORT, section = DIRECTORIES, object_type = 'directory_import', after_value = отчёт, description = имя файла)` и файл сохранён в S3 `uploads/imports/{audit_event_id}/{original_name}` с регистрацией в `s3_object` (`bucket_role = uploads`, `created_by = user:{id}`); при `dry_run` ни аудита, ни файла. Отчёт SHALL содержать счётчики (`rows`, `groups_created`, `groups_updated`, `groups_skipped`, `entities_created`, `entities_updated`, `entities_skipped`, `memberships_created`, `offices_created`, `errors`) и построчный список `{row, inn, code, message}`. #### Scenario: dry_run не оставляет следов - **WHEN** импорт выполнен с `dry_run = true` - **THEN** число строк в `company_group`, `legal_entity`, `regional_office`, `audit_event`, `s3_object` не изменилось, отчёт возвращён #### Scenario: Полная выгрузка - **WHEN** `docs/source/biq/ecp-export-lagal-entity.xlsx` импортируется с `dry_run = true` (маркер `fixtures`, `ARM_KA_DOCS`) - **THEN** ответ 200 за ≤ 60 с, `rows = 8400`, в отчёте есть коды `UNSUPPORTED_TYPE` (≥ 500), `INVALID_INN`, `DUPLICATE_IN_FILE`, `RF_CREATED` = 53, `groups_created` = 344 (из 345 ГК файла одна целиком состоит из строк-дублей и не создаётся)