# Design: схема среза ЕХ ex-payload-v1 Change `ex-payload-schema`, issue RSHB/arm-ka-docs#208, ветка `feat/208-ex-payload` (от `feat/206-kfo-contract`). Читатель — Потапова (AURORA-1560, запись среза), следом — исполнитель AURORA-2001 (чтение среза) и тестировщик 1562. Читают как постановку, без созвона. ## 1. Зачем Срез ЕХ — самодостаточная заморозка верифицированной версии: его читают и выгружают, когда рабочих таблиц консолидации уже нет. Всё производное (сводная ОСВ, агрегаты ВГО, шахматка, КФО, гудвилл, корректировки, сравнение с клиентом, карточка версии) лежит в `ex_snapshot_item.payload` (jsonb). Без схемы payload каждый, кто пишет и читает срез, решает формат сам — и просмотр среза разойдётся с экраном рабочей версии. Успех: Потапова пишет срез, проверяя каждый элемент схемой; исполнитель 2001 рендерит и выгружает срез по той же схеме, не заглядывая в таблицы консолидации. ## 2. Цели, не цели, анти-цели **Цели** — схема конверта и данных по каждому `item_type`, примеры, проверка в `make check`; правила среза в §15; правки DBML, без которых схему не записать (`version_card`, действие журнала). **Не цели** — `proposal.md`. **Анти-цели** - Payload, который без таблиц консолидации не прочитать: ссылки допустимы только на то, что живёт не меньше среза — `document`, `legal_entity`, `related_group`, `company_group`, `app_user` (DOC-274687 правило 4); остальные id (строк ОСВ, корректировок, правил) — справочно, для рендера не нужны. - Схема, которая заставляет фронт ЕХ пересчитывать: в payload лежат готовые значения так, как их видел аналитик. - Повторный перевод единиц: суммы в payload — как в источнике (DOC-274687 правило 5), единица — рядом. ## 3. Ограничения - JSON Schema 2020-12, как остальные контракты; проверка примеров — `scripts/validate_contracts.py` (`make check`). - Деньги — строки с двумя знаками (`^-?\d{1,16}\.\d{2}$`), как в API; `null` — «не рассчитано». - Схема версионируется: `schema_version` в конверте и `ex_snapshot_item.payload_schema_version` = `"1.0"`; несовместимое изменение — `ex-payload-v2.schema.json` рядом, старые срезы не переписываются. - Модель — DBML v5.26 из #207: эта ветка дописывает в неё пункты changelog (версия ещё не слита, номер не тратим). ## 4. Что решено ### 4.1 Конверт элемента Каждый `payload` (у всех `item_type`, кроме `primary_document`, у которого `payload = null` и есть `document_id`): ``` {schema_version: "1.0", item_type, units: "rub" | "thousand_rub" | "none", generated_at (date-time), source_version_no (int), data: {...}} ``` `item_type` в конверте совпадает с колонкой; `units` — единица всех денег в `data`; `generated_at` — момент расчёта перед записью. Конверт — `additionalProperties: false`. ### 4.2 Данные по типам Общий принцип (**главнее любой таблицы ниже**): `data` — **ответ API экрана рабочей версии в момент верификации, теми же DTO, именами и вложенностью**, без полей редактирования (`editable`, `read_only_reason`, `lock`) и плюс то, что нужно **выгрузке xlsx рабочей версии** (срез выгружается на лету из payload и должен совпадать с выгрузкой рабочей версии — proposal, AURORA-1562). Обязательные поля задаёт схема; лишние поля допускаются (`additionalProperties: true`), рендер опирается только на обязательные. Ссылки на компании — как в DTO (`Ref {id, name}` или `legal_entity_id` + `legal_entity_name`/`_inn`): имена в DTO уже есть, отдельный `CompanyRef` не вводится; список компаний периметра с ИНН — в `version_card.members`. Id строк ОСВ, корректировок и правил остаются в данных справочно. | `item_type` | `units` | `data` (DTO) | |---|---|---| | `summary_osv` | `rub` | `{rows: ConsolidatedRowOut[], summary: OsvSummaryOut}` (`dev/api-contract-slice-4.md` §3) — строки свода, включая `match_kind` (колонка «Способ сопоставления» выгрузки), и `summary.gaps[]` (лист «Пробелы») | | `vgo_pairs` | `rub` | `{balance: CorrectionTableOut, income: CorrectionTableOut \| null}` (`dev/api-contract-corrections.*`) — агрегаты таблицы корректировок со сторонами и строками ОСВ (`entries`, нужны выгрузке), нераспределённые строки, итоги; без `editable`/`read_only_reason`; `income` — `null`, пока агрегатов ОФР нет (AURORA-1999). Это вход КФО, не прежний экран «Пары ВГО» | | `chess` | `rub` | `{matrix_all: IntercompanyMatrixOut, presets: [{rule_no, matrix: IntercompanyMatrixOut}], result_table}` (`dev/frontend-api-guide.md` §5.7.1) — «все счета», пресет каждого правила ББ версии правил, результирующая таблица пяти статей | | `kfo_balance` / `kfo_income` | `thousand_rub` | `KfoResultOut` (`dev/api-contract-kfo.md`) без `editable`/`read_only_reason`, с `balance` **или** `income` своего отчёта (второй отчёт — в своём элементе); `requisites` (в т. ч. `corrections_balance`/`_income` для формы 2.03.01.01с), `summary`, `goodwill`, обоснование, `warnings` — в обоих элементах одинаковые | | `goodwill` | `rub` | только при `goodwill_enabled`: `GoodwillTableOut` (`dev/api-contract-goodwill.*`) без `editable`/`read_only_reason` — строки, группы, итоги | | `corrections` | `rub` | применённые корректировки (`APPLIED`; `REVOKED` нет, `DRAFT` не пишется): `{items: [{statement_type, rule_no, company_1: Ref, company_2: Ref, correction: CorrectionOut}]}` — `CorrectionOut` как в контракте корректировок (`updated_by`/`updated_at` — кто и когда сохранил) | | `other_report` | `thousand_rub` | только при `scenario = CONFIRM`: `{report_kind: "client_kfo_comparison", lines[] {statement_type, line_code, line_name, client_amount, client_vgo_amount, bank_amount, bank_vgo_amount, deviation_amount, deviation_percent, deviation_reason, comment}}` по `client_kfo_line` (контракта API ещё нет — AURORA-1549; имена — как в DBML) | | `version_card` | `none` | один на срез, **новый тип**: `company_group {id, name}`, `related_group {id, name}`, `reporting_date`, `year`, `quarter`, `version_no`, `scenario`, `approach`, `goodwill_enabled`, `initiator` (UserRef); `quarter` — `"Q1"…"Q4"`, `initiator_office {id, name}`, `members[]` `{legal_entity_id, name, inn, is_head_entity}`, `history[]` как `VersionCard.history` (`{from_status, to_status, changed_by (UserRef), changed_at, comment}`), `verified_by` (UserRef), `verified_at`, `uob_decision_no`, `uob_decision_date`, `methodology_deviations` | `UserRef {user_id, full_name}` — ФИО на дату верификации (пользователь может быть отключён, но не удаляется). ### 4.3 Правила среза — §15 `05-domain-rules.md` Выжимка DOC-274687 под нашу модель: срез пишется один раз при «Верифицирован» (после фиксации ГСК, AURORA-2002), один на версию (`source_version_id` уникален); состав — таблица 4.2 + `primary_document` по каждому документу `consolidation_document` + `ex_snapshot_member` по каждому ЮЛ версии; строки форм без кода, непривязанные документы и черновики корректировок не входят; `goodwill` — только при `goodwill_enabled`, `other_report` — только при `CONFIRM`; номер среза — следующий по (ГСК, год, квартал) (уникальный индекс DBML; новая ГСК после смены периметра начинает с 1); прежний срез запись в архив не переводит (архив ручной, решение 27.10.2026): переход в «Верифицирован» уже проверил, что у той же **ГК** за ту же дату актуального среза нет (AURORA-2002), поэтому после записи актуальный срез по ГК и дате ровно один — новый; архивным срезам той же ГК и даты без ссылки ставится `superseded_by_snapshot_id` нового (ГК, а не ГСК — чтобы ссылку получил и срез прежней ГСК); разархивирование ссылку снимает (AURORA-2001). Если заказчик выберет ключ «ГСК × дата» (вопрос AURORA-1397) — меняются проверка 2002 и это правило связи, схема payload — нет; производные считаются в последний раз и замораживаются, `kfo_result.is_frozen = true`; сбой записи не отменяет верификацию (повтор — вопрос заказчику, OQ); аудит записи — `EX_SNAPSHOT_WRITE`. Вопросы заказчику (кто архивирует, ключ ГК или ГСК, повтор при сбое, PDF) — OQ со ссылкой на AURORA-1397. ### 4.4 DBML v5.26 (дописать в changelog #207) - `ex_snapshot_item.item_type` — + `version_card`; `payload` — «конверт `contracts/ex-payload-v1.schema.json`, у `primary_document` — null»; `payload_schema_version` — `"1.0"`. - `audit_action` + `EX_SNAPSHOT_WRITE` (раздел `KFO`, `object_type = ex_snapshot`, `after` — номер среза, ГСК, дата, число элементов). - `ex_snapshot.status` — «ACTUAL — не архивирован; ARCHIVE — архивирован пользователем в ЕХ» (сейчас — «предыдущие → ARCHIVE», противоречит ручному архиву). Миграция колонок не нужна; значение enum — в миграции AURORA-1560. ### 4.5 Вложения в Jira После открытия PR: AURORA-1560 — схема, §15 (`05-domain-rules.md`), DBML; комментарий со ссылкой на PR. AURORA-2001 — ссылка в комментарии (чтение — по той же схеме). ## 5. Что остаётся исполнителю плана Точная разметка схемы (`$defs`, имена), тексты §15, числа примеров (из `scripts/kfo_contract/kfo_example.py`), какие элементы показать примерами (минимум: `kfo_balance`, `version_card`, `corrections`, `summary_osv`). ## 6. Decisions made without discussion | # | Дата | Решение | Обоснование | Цена ошибки | |---|---|---|---|---| | 1 | 06.11.2026 | `data` = замороженный ответ экрана теми же именами полей, обязательные поля — схемой, лишние допускаются | Рендер ЕХ переиспользует экраны рабочей версии; исполнителю не надо вырезать поля | Лишние поля раздувают срез; рендер, опирающийся на необязательное поле, сломается на старом срезе | | 2 | 06.11.2026 | `vgo_pairs` — агрегаты таблицы корректировок, не прежний экран «Пары ВГО» | Это вход КФО и то, что аналитик видит в UI | Если ЕХ нужна детализация упаковки по строкам — новый тип элемента | | 3 | 06.11.2026 | `chess` — «все счета» + пресет каждого правила ББ + результирующая таблица; ручной выбор счетов не замораживается | Ручной режим — исследование, его состояние не хранится | В ЕХ нельзя посмотреть шахматку по произвольным счетам | | 4 | 06.11.2026 | Карточка версии — новый `item_type version_card` с ФИО, а не колонки `ex_snapshot` | DOC-274687 оставляет выбор команде; элемент не трогает схему таблицы | Фильтры ЕХ по участникам маршрута пойдут по jsonb | | 5 | 06.11.2026 | В срез — только `APPLIED` корректировки (DOC-274687 предполагал и `REVOKED`) | `REVOKED` в системе нет: корректировку меняют пересохранением (ответ Лысенко п. 9) | История правок корректировки — только в аудите | | 6 | 06.11.2026 | DBML-правки — в ту же неслитую v5.26, что в #207 | Одна версия на пакет КФО + ЕХ; номер не тратим | Если #207 сольют раньше и v5.26 «закроется» — перенести правки в v5.27 | | 7 | 06.11.2026 | `EX_SNAPSHOT_WRITE` — раздел журнала `KFO` | Раздела `EX` в `audit_section` нет; запись среза — часть верификации КФО | Фильтр журнала по ЕХ не найдёт событие | | 9 | 06.11.2026 | Payload покрывает и выгрузку xlsx рабочей версии (пробелы, способ сопоставления, строки ОСВ агрегатов, сводка корректировок), не только экран | Срез выгружается на лету из payload, выгрузка должна совпасть с рабочей (1562); ответ на вопрос проверки исполнителем 1 | Срез заметно больше (строки ОСВ агрегатов) | | 10 | 06.11.2026 | `data` — DTO API без изменений имён и вложенности; таблица §4.2 подчинена принципу | Рендер и выгрузка ЕХ переиспользуют виджеты и выгрузки рабочей версии без маппинга; вопрос проверки 2 | Изменение DTO API после 1.0 требует новой версии схемы или совместимого расширения | | 11 | 06.11.2026 | Нумерация среза — по ГСК; «один актуальный» и `superseded_by` — по ГК и дате | Как в DBML и DOC-274686 (решения Р-35/Р-36 по ГК); вопрос проверки 3 | При ключе «ГСК» у заказчика — правка правила связи и проверки 2002 | | 12 | 06.11.2026 | Схема обязывает то, что читают выгрузки xlsx рабочей версии: итоги шахматки `total_debit`/`total_credit`, правило пресета и `rules` таблицы корректировок, `pairs`, `balance` корректировки, `entry_ids` строк | Ревью C/I: без них выгрузка среза не совпадёт с рабочей (решение #9) | Больше обязательных полей — писателю нельзя «урезать» DTO | | 13 | 06.11.2026 | Проценты к меньшей стороне (шахматка, агрегаты, корректировки) — без ограничения разрядов; квартал — `"Q1"…"Q4"`; `updated_by` — `Ref`, как в DTO | Ревью C-1, C-2, I-2: настоящие ответы API не проходили схему | — | | 14 | 06.11.2026 | КФО в срезе — только замороженная и `VERIFIED`; карточка — непустые состав и история | Ревью M-1: пустой или незамороженный payload не должен проходить | Срез версии без истории маршрута (миграция старых данных) схему не пройдёт | | 15 | 06.11.2026 | Ошибка пресета правила (422 `INVALID_RULE`/`ACCOUNTS_REQUIRED`) — пресет пропускается; другая ошибка сборки — сбой записи среза | Ревью M-5 | Срез без пресета правила, которое на экране тоже не строится | | 8 | 06.11.2026 | Без отдельного согласования дизайна с владельцем: схема описана в чате 06.11 («все так»), владелец сказал «делай схему» | Срок 12.11, владелец просил не останавливаться | Расхождение с ожиданием владельца видно только в PR |