From 3b4f329f1639e677ea9aead60b26454b0f95b146 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 7 Oct 2026 18:23:09 +0400 Subject: [PATCH] =?UTF-8?q?openspec:=20=D0=B0=D1=80=D1=85=D0=B8=D0=B2=20ch?= =?UTF-8?q?ange=20front-api-gaps=20(2026-10-07)=20=E2=80=94=20=D1=81=D0=BF?= =?UTF-8?q?=D0=B5=D0=BA=D0=B0=20consolidations-api=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- .../2026-10-07-front-api-gaps}/.openspec.yaml | 0 .../2026-10-07-front-api-gaps}/design.md | 4 +- .../2026-10-07-front-api-gaps}/proposal.md | 0 .../specs/consolidations-api/spec.md | 0 .../2026-10-07-front-api-gaps}/tasks.md | 0 openspec/specs/consolidations-api/spec.md | 123 +++++++++++++----- 6 files changed, 89 insertions(+), 38 deletions(-) rename openspec/changes/{front-api-gaps => archive/2026-10-07-front-api-gaps}/.openspec.yaml (100%) rename openspec/changes/{front-api-gaps => archive/2026-10-07-front-api-gaps}/design.md (97%) rename openspec/changes/{front-api-gaps => archive/2026-10-07-front-api-gaps}/proposal.md (100%) rename openspec/changes/{front-api-gaps => archive/2026-10-07-front-api-gaps}/specs/consolidations-api/spec.md (100%) rename openspec/changes/{front-api-gaps => archive/2026-10-07-front-api-gaps}/tasks.md (100%) diff --git a/openspec/changes/front-api-gaps/.openspec.yaml b/openspec/changes/archive/2026-10-07-front-api-gaps/.openspec.yaml similarity index 100% rename from openspec/changes/front-api-gaps/.openspec.yaml rename to openspec/changes/archive/2026-10-07-front-api-gaps/.openspec.yaml diff --git a/openspec/changes/front-api-gaps/design.md b/openspec/changes/archive/2026-10-07-front-api-gaps/design.md similarity index 97% rename from openspec/changes/front-api-gaps/design.md rename to openspec/changes/archive/2026-10-07-front-api-gaps/design.md index 2d75c5a..c2086d9 100644 --- a/openspec/changes/front-api-gaps/design.md +++ b/openspec/changes/archive/2026-10-07-front-api-gaps/design.md @@ -59,9 +59,9 @@ ### D-3. Участник периметра: `entity_type` и `osv_accounts` (вариант B) `MemberOut.entity_type: EntityType` — из `legal_entity`. `MemberOut.osv_accounts: list[str]` — новый запрос репозитория -`osv_accounts(session, entity_ids, year, quarter) -> dict[uuid, list[str]]` одним `SELECT DISTINCT legal_entity_id, +`osv_accounts(session, entity_ids, year, quarters) -> dict[uuid, list[str]]` (`quarters` — `quarters_through(quarter версии)`) одним `SELECT DISTINCT legal_entity_id, osv_account_code` по `document ⋈ stored_file`: `document_type = OSV`, `period_year = year`, `period_quarter IN -quarters_through(quarter)`, `document.status != ERROR`, `stored_file.status NOT IN (SUPERSEDED, ERROR)`, +quarters`, `document.status != ERROR`, `stored_file.status NOT IN (SUPERSEDED, ERROR)`, `osv_account_code IS NOT NULL`. Сортировка — в Python `account_sort_key` (та же натуральная, что у столбцов чек-листа); ЮЛ без документов — пустой список. diff --git a/openspec/changes/front-api-gaps/proposal.md b/openspec/changes/archive/2026-10-07-front-api-gaps/proposal.md similarity index 100% rename from openspec/changes/front-api-gaps/proposal.md rename to openspec/changes/archive/2026-10-07-front-api-gaps/proposal.md diff --git a/openspec/changes/front-api-gaps/specs/consolidations-api/spec.md b/openspec/changes/archive/2026-10-07-front-api-gaps/specs/consolidations-api/spec.md similarity index 100% rename from openspec/changes/front-api-gaps/specs/consolidations-api/spec.md rename to openspec/changes/archive/2026-10-07-front-api-gaps/specs/consolidations-api/spec.md diff --git a/openspec/changes/front-api-gaps/tasks.md b/openspec/changes/archive/2026-10-07-front-api-gaps/tasks.md similarity index 100% rename from openspec/changes/front-api-gaps/tasks.md rename to openspec/changes/archive/2026-10-07-front-api-gaps/tasks.md diff --git a/openspec/specs/consolidations-api/spec.md b/openspec/specs/consolidations-api/spec.md index af897b3..64fcd7b 100644 --- a/openspec/specs/consolidations-api/spec.md +++ b/openspec/specs/consolidations-api/spec.md @@ -8,27 +8,51 @@ ## Requirements ### Requirement: Создание консолидации -`POST /consolidations` (`CONSOLIDATION_EDIT`, policy по ГК) с телом `{company_group_id, related_group_id, year, quarter}` -SHALL в одной транзакции создать `consolidation` (`reporting_date`: Q1 → 01.04.year, Q2 → 01.07.year, Q3 → 01.10.year, -Q4 → 01.01.year+1; ASSUMPTION(OQ-40)), `consolidation_version` №1 (`INITIATED`, `scenario = CREATE`, `approach = BASIC`, -`goodwill_enabled = false`, `initiator_id` и `initiator_office_id` из пользователя, `is_in_work = true`), -`consolidation_member` из текущих членов ГСК (`is_head_entity = false`), первую запись `consolidation_status_history` -(`from_status` NULL → `INITIATED`), подхват подтверждённых документов (см. `documents-api` «Подхват документов») и -`audit_event(CONSOLIDATION_CREATE)`; ответ 201 с карточкой версии. ГСК SHALL принадлежать ГК и иметь `kind = gsk`, -иначе 422 `RELATED_GROUP_MISMATCH`; членов меньше двух → 422 `PERIMETER_TOO_SMALL`; пара (ГК, год, квартал) уже есть -→ 409 `CONSOLIDATION_EXISTS` (в том числе при гонке двух запросов — UNIQUE `(company_group_id, year, quarter)`); -`year` вне 2000..2100 → 422. Блокировка на создателя MUST NOT ставиться. `related_group_id` в `consolidation` SHALL -оставаться NULL (фиксируется при верификации, Срез 7). +`POST /consolidations` (`CONSOLIDATION_EDIT`, policy по ГК) с телом `{company_group_id, related_group_id, year, quarter, +legal_entity_ids?}` SHALL в одной транзакции создать `consolidation` (`reporting_date`: Q1 → 01.04.year, Q2 → 01.07.year, +Q3 → 01.10.year, Q4 → 01.01.year+1; ASSUMPTION(OQ-40)), `consolidation_version` №1 (`INITIATED`, `scenario = CREATE`, +`approach = BASIC`, `goodwill_enabled = false`, `initiator_id` и `initiator_office_id` из пользователя, +`is_in_work = true`), `consolidation_member` периметра (`is_head_entity = false`), первую запись +`consolidation_status_history` (`from_status` NULL → `INITIATED`), подхват подтверждённых документов ЮЛ периметра (см. +`documents-api` «Подхват документов») и одно событие `audit_event(CONSOLIDATION_CREATE)`, у которого +`after_value.members` — отсортированный список ЮЛ периметра; ответ 201 с карточкой версии. Периметр SHALL быть: без +`legal_entity_ids` (поле отсутствует или `null`) — текущие члены ГСК; с `legal_entity_ids` — ровно эти ЮЛ после +схлопывания дублей (порядок первого вхождения), каждое SHALL принадлежать ГК консолидации +(`legal_entity.company_group_id`), иначе 422 `ENTITY_NOT_IN_GROUP` с перечнем `legal_entity_ids` чужих в порядке +запроса; состав ГСК при явном списке не проверяется и не меняется. ГСК SHALL быть обязательной, принадлежать ГК и иметь +`kind = gsk`, иначе 422 `RELATED_GROUP_MISMATCH`. Периметр меньше двух ЮЛ (членов ГСК без списка либо выбранных ЮЛ после +схлопывания) → 422 `PERIMETER_TOO_SMALL`; пара (ГК, год, квартал) уже есть → 409 `CONSOLIDATION_EXISTS` (в том числе при +гонке двух запросов — UNIQUE `(company_group_id, year, quarter)`); `year` вне 2000..2100 → 422. Порядок проверок: ГК +видима (404) → ГСК видима (404) и соответствует ГК (422) → периметр (422) → период свободен (409). Блокировка на +создателя MUST NOT ставиться; `MEMBERSHIP_CHANGE` при создании MUST NOT писаться. `related_group_id` в `consolidation` +SHALL оставаться NULL (фиксируется при верификации, Срез 7); `related_group_member` MUST NOT изменяться. #### Scenario: Консолидация РБ за Q3 2024 -- **WHEN** `ka_rf` Санкт-Петербургского РФ создаёт консолидацию ГК «Русский бисквит», ГСК из двух ЮЛ, 2024, Q3 +- **WHEN** `ka_rf` Санкт-Петербургского РФ создаёт консолидацию ГК «Русский бисквит», ГСК из двух ЮЛ, 2024, Q3, без + `legal_entity_ids` - **THEN** 201; версия №1 `INITIATED`, `reporting_date = 2024-10-01`, два `consolidation_member`, история с одной записью, `audit_event(CONSOLIDATION_CREATE)` с `consolidation_version_id`; активной блокировки нет +#### Scenario: Выбранный периметр шире ГСК +- **WHEN** в ГК три ЮЛ, в ГСК два из них, создаётся консолидация с `legal_entity_ids` из всех трёх ЮЛ ГК +- **THEN** 201; в `members` три ЮЛ; `after_value.members` события `CONSOLIDATION_CREATE` — три ЮЛ; состав ГСК не изменился + +#### Scenario: ГСК из одного ЮЛ, второе отмечено вручную +- **WHEN** в ГСК один текущий член, в `legal_entity_ids` он и ещё одно ЮЛ той же ГК +- **THEN** 201; в `members` два ЮЛ + #### Scenario: ГСК из одного ЮЛ -- **WHEN** в ГСК один текущий член +- **WHEN** в ГСК один текущий член, `legal_entity_ids` не передан - **THEN** 422 `PERIMETER_TOO_SMALL`; `consolidation` и версия не созданы +#### Scenario: ЮЛ другой ГК в списке +- **WHEN** в `legal_entity_ids` попало ЮЛ, `company_group_id` которого не равен ГК консолидации +- **THEN** 422 `ENTITY_NOT_IN_GROUP` с этим ЮЛ в `legal_entity_ids`; `consolidation` не создана + +#### Scenario: Дубли схлопываются до одного ЮЛ +- **WHEN** `legal_entity_ids` — один и тот же идентификатор дважды +- **THEN** 422 `PERIMETER_TOO_SMALL` + #### Scenario: Повтор периода - **WHEN** консолидация ГК за 2024 Q3 уже есть, создаётся ещё одна - **THEN** 409 `CONSOLIDATION_EXISTS` @@ -39,11 +63,16 @@ Q4 → 01.01.year+1; ASSUMPTION(OQ-40)), `consolidation_version` №1 (`INITIATE (`id`, `version_no`, `status`, `scenario`, `approach`, `lock` — активная блокировка или null, `created_at`, `members_count`); параметр `in_work_only=true` SHALL оставлять только версии со статусом не из {`VERIFIED`, `CANCELLED`, `ARCHIVED`, `REJECTED`}. `GET /consolidations/versions/{id}` SHALL отдавать карточку: поля -версии и консолидации, `company_group {id, name}`, инициатора и его офис, `lock`, `members[]` (ЮЛ: `id`, `name`, `inn`, -`is_head_entity`, `checklist[]` — ячейки по правилам подхода, см. «Чек-лист комплекта»), `history[]` (`from_status`, -`to_status`, `changed_by`, `changed_at`, `comment`). Консолидация ГК вне видимости и несуществующий идентификатор — -404 `NOT_FOUND` с сообщением `CONSOLIDATION_NOT_FOUND` / `VERSION_NOT_FOUND` и одинаковым телом (так же во всех -спецификациях среза: идентификаторы `…_NOT_FOUND` — текст сообщения при фиксированном коде `NOT_FOUND`). +версии и консолидации, `company_group {id, name}`, инициатора и его офис, `lock`, `members[]`, `history[]` +(`from_status`, `to_status`, `changed_by`, `changed_at`, `comment`). Член периметра SHALL содержать `id`, `name`, `inn`, +`entity_type` (`ЮЛ`/`ИП`, из `legal_entity`), `is_head_entity`, `checklist[]` (ячейки по правилам подхода, см. +«Чек-лист комплекта») и `osv_accounts[]` — уникальные `osv_account_code` документов ЮЛ, у которых `document_type = OSV`, +`period_year` равен году версии, `period_quarter` не позже квартала версии, статус документа не `ERROR`, файл не в +`SUPERSEDED` и не в `ERROR`; документы без квартала или без кода счёта не учитываются; статус подтверждения и привязка к +версии не важны; порядок — натуральная сортировка кода счёта (как столбцы чек-листа: `01 < 51 < 58.03 < 60`). Консолидация +ГК вне видимости и несуществующий идентификатор — 404 `NOT_FOUND` с сообщением `CONSOLIDATION_NOT_FOUND` / +`VERSION_NOT_FOUND` и одинаковым телом (так же во всех спецификациях среза: идентификаторы `…_NOT_FOUND` — текст +сообщения при фиксированном коде `NOT_FOUND`). #### Scenario: Дерево версий - **WHEN** у ГК консолидации 2024 Q1 (версии 1 и 2) и 2024 Q3 (версия 1), запрошен список @@ -53,6 +82,16 @@ Q4 → 01.01.year+1; ASSUMPTION(OQ-40)), `consolidation_version` №1 (`INITIATE - **WHEN** `ka_rf` другого филиала запрашивает карточку версии - **THEN** 404 `NOT_FOUND` (сообщение `VERSION_NOT_FOUND`), тело совпадает с ответом на случайный `uuid` +#### Scenario: Счета ОСВ участника +- **WHEN** версия за 2024 Q3; у ЮЛ есть ОСВ 60 за Q1 2024 (`RECOGNIZED`), ОСВ 51 за Q3 2024 (`CONFIRMED`), ОСВ 58.03 за + Q3 2024 (`NEEDS_CHECK`), ОСВ 62 за Q4 2024, ОСВ 01 за 2023, ОСВ 76 в заменённом (`SUPERSEDED`) файле и ОСВ 90 в + документе `ERROR` +- **THEN** `osv_accounts = ["51", "58.03", "60"]` + +#### Scenario: Тип ЮЛ +- **WHEN** в периметре индивидуальный предприниматель +- **THEN** у него `entity_type = "ИП"`, у остальных — `"ЮЛ"` + ### Requirement: Периметр версии `PUT /consolidations/versions/{id}/members` (`CONSOLIDATION_EDIT`, policy, статус «в работе», своя активная блокировка) с телом `{legal_entity_ids: [...]}` SHALL заменить периметр версии: все ЮЛ SHALL принадлежать ГК консолидации @@ -129,22 +168,25 @@ DOCUMENTS_PROCESSING`; маппинг документа `NEEDS_CHECK`, подх ### Requirement: Чек-лист комплекта `GET /consolidations/versions/{id}/checklist` (`CONSOLIDATION_VIEW`, policy) SHALL вернуть `rules[]` (правила `required_document_rule` подхода версии: `document_type`, `account`, `is_mandatory`; порядок — `BALANCE`, `INCOME`, -затем ОСВ по коду счёта) и `rows[]` по ЮЛ периметра (по имени) с `cells[]` в порядке `rules`. Ячейка `{status, -missing_quarters, document_ids}`: `CONFIRMED` — пара (ЮЛ, правило) полностью покрыта привязанными документами (ОСВ: по -правилу выбора источников `consolidated-osv`; ББ/ОФР: документ за квартал версии); `NEEDS_CHECK` / `RECOGNIZED` — -есть неподтверждённый документ ЮЛ того же типа, счёта и года, подходящий версии (приоритет `NEEDS_CHECK`); -`PROCESSING` — у ЮЛ есть файл `UPLOADED` или `PROCESSING`; `ERROR` — последний по `uploaded_at` файл ЮЛ в `ERROR`; -иначе `MISSING`. `missing_quarters` SHALL перечислять кварталы без квартальной ОСВ при частичном покрытии. Правило ОСВ -SHALL закрываться только документом с `osv_account_code`, равным `account` правила (OQ-04). Те же ячейки SHALL -отдаваться в карточке версии у каждого члена. С `?format=csv` SHALL отдаваться тот же чек-лист файлом `text/csv; -charset=utf-8` с BOM и разделителем `;`: заголовок «Организация;ИНН;<правило>…» (правила — «Бух. баланс», «ОФР», -«ОСВ <счёт>»), ячейки — подписи макета: `CONFIRMED` «Загружен», `PROCESSING` «Обработка», `NEEDS_CHECK`/`RECOGNIZED` -«Требуется проверка», `ERROR` «Ошибка загрузки», `MISSING` «Отсутствует» (с « (нет: Q1, Q2)» при `missing_quarters`); -`Content-Disposition` — `attachment` с именем `checklist-<ГК>-<год>-<квартал>.csv` в RFC 5987. +затем ОСВ по коду счёта) и `rows[]` по ЮЛ периметра (по имени) с `cells[]` в порядке `rules`. Ячейка `{document_type, +account, status, missing_quarters, document_ids}`: `document_type` и `account` — правила ячейки (ячейка +самоописывающая, фронт не опирается на позицию); `CONFIRMED` — пара (ЮЛ, правило) полностью покрыта привязанными +документами (ОСВ: по правилу выбора источников `consolidated-osv`; ББ/ОФР: документ за квартал версии); +`NEEDS_CHECK` / `RECOGNIZED` — есть неподтверждённый документ ЮЛ того же типа, счёта и года, подходящий версии +(приоритет `NEEDS_CHECK`); `PROCESSING` — у ЮЛ есть файл `UPLOADED` или `PROCESSING`; `ERROR` — последний по +`uploaded_at` файл ЮЛ в `ERROR`; иначе `MISSING`. `missing_quarters` SHALL перечислять кварталы без квартальной ОСВ при +частичном покрытии. Правило ОСВ SHALL закрываться только документом с `osv_account_code`, равным `account` правила +(OQ-04). Те же ячейки (с `document_type` и `account`, в том же порядке) SHALL отдаваться в карточке версии у каждого +члена. С `?format=csv` SHALL отдаваться тот же чек-лист файлом `text/csv; charset=utf-8` с BOM и разделителем `;`: +заголовок «Организация;ИНН;<правило>…» (правила — «Бух. баланс», «ОФР», «ОСВ <счёт>»), ячейки — подписи макета: +`CONFIRMED` «Загружен», `PROCESSING` «Обработка», `NEEDS_CHECK`/`RECOGNIZED` «Требуется проверка», `ERROR` «Ошибка +загрузки», `MISSING` «Отсутствует» (с « (нет: Q1, Q2)» при `missing_quarters`); `Content-Disposition` — `attachment` с +именем `checklist-<ГК>-<год>-<квартал>.csv` в RFC 5987. #### Scenario: Пробел Q2 - **WHEN** у ЮЛ привязаны квартальные ОСВ 60 за Q1 и Q3 2024, версия за 2024 Q3 -- **THEN** ячейка ОСВ 60: `status = MISSING`, `missing_quarters = ["Q2"]`, `document_ids` — два документа +- **THEN** ячейка ОСВ 60: `document_type = OSV`, `account = "60"`, `status = MISSING`, `missing_quarters = ["Q2"]`, + `document_ids` — два документа #### Scenario: Субсчёт не закрывает правило - **WHEN** у ЮЛ подтверждена только ОСВ 58.03 за квартал версии @@ -154,21 +196,30 @@ charset=utf-8` с BOM и разделителем `;`: заголовок «Ор - **WHEN** у ЮЛ загружен файл, задание ещё не завершено, других документов нет - **THEN** все ячейки ЮЛ — `PROCESSING` +#### Scenario: Ячейки карточки подписаны +- **WHEN** запрошена карточка версии подхода `BASIC` +- **THEN** у каждого члена `checklist[i].document_type` и `checklist[i].account` равны `rules[i]` чек-листа той же + версии; ячейка ББ — `document_type = BALANCE`, `account = null` + #### Scenario: Чек-лист в CSV - **WHEN** запрошен чек-лист версии с `format=csv`, у ЮЛ подтверждён ББ, ОСВ 60 отсутствует за Q2 - **THEN** файл начинается с BOM, первая строка — «Организация;ИНН;Бух. баланс;ОФР;ОСВ 60…», в строке ЮЛ — «Загружен» и «Отсутствует (нет: Q2)» ### Requirement: Границы входных данных -Входы консолидаций SHALL иметь верхние границы: `legal_entity_ids` тела `PUT …/members` — не больше 500 идентификаторов, -`comment` тела `POST …/status` — не больше 1000 символов, `q` фильтров `GET …/osv` и экспорта — не больше 500 символов. -Превышение SHALL давать 422 со списком ошибок валидации (`detail[].loc` указывает поле) до обращения к БД. Версия -консолидации SHALL проверяться на видимость до блокировки `FOR UPDATE`: невидимая версия отвечает 404 сразу, не ожидая -транзакцию, которая держит её строку. +Входы консолидаций SHALL иметь верхние границы: `legal_entity_ids` тела `PUT …/members` и тела `POST /consolidations` — +не больше 500 идентификаторов, `comment` тела `POST …/status` — не больше 1000 символов, `q` фильтров `GET …/osv` и +экспорта — не больше 500 символов. Превышение SHALL давать 422 со списком ошибок валидации (`detail[].loc` указывает +поле) до обращения к БД. Версия консолидации SHALL проверяться на видимость до блокировки `FOR UPDATE`: невидимая +версия отвечает 404 сразу, не ожидая транзакцию, которая держит её строку. #### Scenario: Периметр длиннее предела - **WHEN** в `PUT …/members` передано 501 ЮЛ - **THEN** 422, `detail[0].loc = ["body", "legal_entity_ids"]`, периметр не изменился +#### Scenario: Периметр создания длиннее предела +- **WHEN** в `POST /consolidations` передано 501 ЮЛ в `legal_entity_ids` +- **THEN** 422, `detail[0].loc = ["body", "legal_entity_ids"]`, консолидация не создана + #### Scenario: Невидимая версия при занятой строке - **WHEN** версию чужого филиала держит `FOR UPDATE` другая транзакция, пользователь без видимости вызывает `POST …/lock` - **THEN** 404 `VERSION_NOT_FOUND` без ожидания