From 31b8e4167400bfccf25e36825e4aeb929425578a Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Fri, 30 Oct 2026 13:30:52 +0400 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=20API=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D1=8B=20?= =?UTF-8?q?=D0=B3=D1=83=D0=B4=D0=B2=D0=B8=D0=BB=D0=BB=D0=B0=20(AURORA-1543?= =?UTF-8?q?/1544)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Черновик для параллельной разработки бэкенда и фронта, по аналогии с api-contract-corrections: фронт строит экран на фейковых данных, не дожидаясь методологов (AURORA-1369, срок 02.11) и бэкенда. Co-Authored-By: Claude Sonnet 5 --- dev/api-contract-goodwill.md | 190 +++++++++++++++++ dev/api-contract-goodwill.openapi.yaml | 278 +++++++++++++++++++++++++ 2 files changed, 468 insertions(+) create mode 100644 dev/api-contract-goodwill.md create mode 100644 dev/api-contract-goodwill.openapi.yaml diff --git a/dev/api-contract-goodwill.md b/dev/api-contract-goodwill.md new file mode 100644 index 0000000..99a068b --- /dev/null +++ b/dev/api-contract-goodwill.md @@ -0,0 +1,190 @@ +# Контракт API таблицы гудвилла (AURORA-1543 / AURORA-1544) + +Черновик контракта для бэкенда (AURORA-1543, Цыганков) и фронта (AURORA-1544, Голов) — по аналогии с +`dev/api-contract-corrections.md`: фронт строит экран по контракту на фейковых данных, не дожидаясь бэкенда, подключение +к реальным ручкам — потом. Машиночитаемая копия — `dev/api-contract-goodwill.openapi.yaml`; при расхождении двух файлов +правится тот, что ошибся, и оба держатся одинаковыми. + +Источники, в порядке приоритета: +1. **AURORA-1369** (решение методологов, срок 02.11.2026) — формула ещё не подтверждена, см. §5 «Открытые вопросы». Пока + не закрыта, бэкенд не считает по-настоящему — но контракт (форма ответа, поля, эндпоинты) этим не затронут: методологи + могут поменять *формулу* (входит ли 1350, источник УК), а не *набор полей* (поля `charter_capital_amount` / + `additional_capital_amount` под обе ветки уже есть в DBML). +2. `05-domain-rules.md` §8 «Гудвилл» — текущий рабочий алгоритм (допущения OQ-10, OQ-37). +3. DBML v5.23, `Table goodwill`, `Table kfo_result_line` (`line_code = 'GOODWILL'`), `consolidation_version.goodwill_enabled`. +4. Макет: `[31-03]` «Гудвилл» (таблица, режим «Редактировать»), `[31-04]` «КФО» (строка «В т.ч. Гудвилл» под 1170). +5. ТЗ 6.7, Прил. 3 п. 2, Прил. 8 — только там, где остальное молчит. + +## 1. Общее + +Всё как в `dev/api-contract-slice-4.md` §1 и `dev/api-contract-corrections.md` §1: +- префикс `/api/v1`, `Authorization: Bearer `; +- чтение — право `CONSOLIDATION_VIEW`, правки — `CONSOLIDATION_EDIT`; +- версия вне видимости — 404 `NOT_FOUND`, как несуществующая; +- доменные ошибки — `{"detail": {"code", "message", ...}}`, 422 валидации тела — массив pydantic; +- **деньги — строки с двумя знаками** (`"1000000.00"`), всегда в **рублях**; перевод в тыс. руб. — на фронте; +- порядок проверок правок: право (403) → видимость (404) → версия редактируема (409) → операция; коды — те же, что везде: + 409 `VERSION_NOT_EDITABLE`, `LOCK_REQUIRED`, `LOCKED_BY_OTHER`. Фронт читает `editable` / `read_only_reason` из ответа + таблицы, правило не повторяет. + +## 2. Понятия + +- **Головная организация периметра (ГО)** — член версии консолидации с `is_head_entity = true` (как в `VersionCard.members[]`, + `dev/api-contract-slice-4.md` §3). **Дочерняя организация (ДО)** — любой другой член периметра. Таблица гудвилла — по + одной строке на каждую пару (ГО, ДО); строки не создаются и не удаляются пользователем, их состав определяется + составом периметра (`PUT …/members`) на момент запроса. **Если в периметре несколько `is_head_entity`** — строки + считаются для каждой ГО отдельно (как для независимых голов); на реальных данных такого пока не встречалось — + ASSUMPTION, уточнить при первом расхождении. +- **Фондовые вложения (`financial_investment_amount`, FV)** — `Σ closing_debit` счёта `58.01` ГО по контрагенту ДО + (сводная ОСВ версии). Может быть `0.00` — строка всё равно показывается (полный список ДО периметра под каждой ГО, + как в макете `[31-03]`). +- **Уставный капитал ДО (`charter_capital_amount`, УК)** — стр. `1310` ББ ДО на отчётную дату; при наличии ОСВ ДО — + `Кт 80 + Кт 83`, иначе `1310` из формы (§8 domain-rules, уточняет первую часть OQ-10). **Добавочный капитал + (`additional_capital_amount`)** — стр. `1350` ББ ДО, считается и хранится всегда, независимо от того, участвует ли + в формуле (см. OQ-10 ниже). +- **Доля владения (`ownership_share`)** — ручной ввод аналитика, % (`numeric(7,4)`); единственное обязательное поле + правки наряду с `comment`. +- **Доля участия (`participation_amount`)** = `(charter_capital_amount [+ additional_capital_amount, если решение + методологов это подтвердит]) × ownership_share / 100`. +- **Гудвилл (`goodwill_amount`)** = `financial_investment_amount − participation_amount`; может быть отрицательным. +- **Учёт в КФО (`include_in_kfo`)** — чекбокс «Учёт в отчёте» строки (макет `[31-03]`, с «Выделить все» по ГО на + фронте — пакетное действие фронта, не отдельная ручка). Комментарий обязателен **только** когда `include_in_kfo = true` + (ТЗ 6.7). Положительная сумма отмеченных строк уходит в `kfo_result_line` с `line_code = 'GOODWILL'` + (раздел I КФО, допущение OQ-37) — расчёт и запись при построении результата КФО, не в этом контракте. +- **Признак расчёта гудвилла (`goodwill_enabled`)** — переключатель версии (`consolidation_version.goodwill_enabled`, + решено OQ-27: живёт на вкладке «Гудвилл», не в модалке создания). Сейчас в контракте нет ручки, которая бы его + выставляла (только чтение через `VersionCard`, `dev/api-contract-slice-4.md`) — добавляется здесь (§3.2). + Выключен — таблица показывается как есть (доли и комментарии не теряются), но ничего не уходит в КФО. + +## 3. Эндпоинты + +| Метод и путь | Право | Запрос | Ответ | Ошибки | +|---|---|---|---|---| +| `GET /consolidations/versions/{version_id}/goodwill` | VIEW | — | 200 `GoodwillTableOut` | 404 `NOT_FOUND` | +| `PUT /consolidations/versions/{version_id}/goodwill-settings` | EDIT | `GoodwillSettingsIn` | 200 `GoodwillTableOut` | 404, 409 правок версии | +| `PUT /consolidations/versions/{version_id}/goodwill/{head_legal_entity_id}/{subsidiary_legal_entity_id}` | EDIT | `GoodwillRowIn` | 200 `GoodwillRowOut` | 404 `NOT_FOUND` (версия или пара не из периметра), 409 правок версии, 422 `COMMENT_REQUIRED`, 422 `INVALID_SHARE` | +| `GET /consolidations/versions/{version_id}/goodwill/export` | VIEW | — | файл xlsx | 404 `NOT_FOUND` | + +### 3.1 `GET …/goodwill` — таблица + +Все строки одним ответом (пар — единицы, не десятки; без страниц). Порядок: по имени ГО, внутри — по имени ДО. +Группировку по ГО и подсчёт «Выделить все» делает фронт. Пустой периметр (только ГО, без ДО) — `rows = []`, это не ошибка. + +### 3.2 `PUT …/goodwill-settings` — переключатель версии + +Меняет `consolidation_version.goodwill_enabled`. Аудит — новое действие в `audit_action` (например, +`GOODWILL_ENABLED_CHANGE`), результат КФО помечается устаревшим при любом изменении значения (как в AURORA-1417). +Выключение не трогает сохранённые строки (`ownership_share`, `comment`, `include_in_kfo` остаются). + +### 3.3 `PUT …/goodwill/{head_legal_entity_id}/{subsidiary_legal_entity_id}` — сохранить строку + +Сохраняет **все поля строки целиком** (как в `PUT …/corrections/aggregates/…` — текущие `ownership_share`, +`include_in_kfo` и `comment` приходят каждый раз, частичного `PATCH` нет). + +- `head_legal_entity_id` должен быть членом периметра с `is_head_entity = true`, `subsidiary_legal_entity_id` — любым + другим членом периметра; иначе 404 `NOT_FOUND` (пары не существует). +- `ownership_share` — строка `0`…`100`, до 4 знаков после запятой (`numeric(7,4)`); вне диапазона или больше 4 знаков — + 422 `INVALID_SHARE`. +- `comment` обязателен, если `include_in_kfo = true`; пустой после обрезки в этом случае — 422 `COMMENT_REQUIRED` + (как `COMMENT_REQUIRED` в контракте корректировок); не длиннее 1000 символов. При `include_in_kfo = false` комментарий + необязателен и может быть `null`. +- `financial_investment_amount`, `charter_capital_amount`, `additional_capital_amount`, `participation_amount`, + `goodwill_amount` в тело **не входят** — это вычисляемые поля, пересчитываются сервером из текущей ОСВ версии при + каждом ответе, в запросе их нет. +- Аудит `GOODWILL_ROW_EDIT` (поле нужно добавить в `audit_action`, как `CORRECTION_*` для корректировок) с `before`/`after`; + результат КФО помечается устаревшим. + +Ответ — пересчитанная строка (а не вся таблица — в отличие от корректировок, строки гудвилла друг на друга не влияют). + +### 3.4 `GET …/export` — xlsx + +Таблица гудвилла одним листом, группировка по ГО, итоговая строка «Всего» (макет `[31-03]`). Имя файла — +`Гудвилл_<название ГК>_<год>_<квартал>.xlsx` в `Content-Disposition` (как в экспорте корректировок). Аудит `EXPORT`. + +## 4. Схемы + +Поля без `?` есть всегда; `null` указан явно. + +**`GoodwillTableOut`** +``` +version_id uuid +goodwill_enabled bool +editable bool +read_only_reason null | "VERSION_NOT_EDITABLE" | "LOCK_REQUIRED" | "LOCKED_BY_OTHER" +rows[] GoodwillRowOut +totals GoodwillTotalsOut +``` + +**`GoodwillRowOut`** +``` +head_legal_entity Ref {id, name} +subsidiary_legal_entity Ref +ownership_share share | null % 4 знака; null — ещё не введена +financial_investment_amount money +charter_capital_amount money +additional_capital_amount money +participation_amount money +goodwill_amount money может быть отрицательным +include_in_kfo bool +comment string | null +updated_by Ref | null +updated_at datetime | null +``` + +**`GoodwillTotalsOut`** — сумма по всем строкам ответа: `{financial_investment_amount, participation_amount, +goodwill_amount}`, плюс `goodwill_in_kfo_amount` — сумма `goodwill_amount` только по строкам с `include_in_kfo = true` +и положительным `goodwill_amount` (то, что реально уйдёт в `kfo_result_line.line_code = 'GOODWILL'`). + +**`GoodwillRowIn`** `{ownership_share (share-строка 0…100), include_in_kfo (bool), comment (string | null, обязателен +при include_in_kfo = true, ≤ 1000 символов)}`. + +**`GoodwillSettingsIn`** `{goodwill_enabled (bool)}`. + +## 5. Открытые вопросы (AURORA-1369, срок 02.11.2026) + +- **OQ-10** — входит ли стр. 1350 (добавочный капитал) в базу участия. Допущение сейчас: **нет**, `participation_amount` + считается только от `charter_capital_amount`; `additional_capital_amount` хранится и показывается в таблице (колонка + есть и в DBML, и на фронте — макет её не показывает явно, но поле в `goodwill` есть с пометкой ADD), но не входит в + формулу. Если методологи подтвердят, что 1350 нужен — меняется только формула на бэкенде, контракт не меняется. +- **OQ-37** — код строки «Гудвилл» в результате КФО: служебный `GOODWILL` (как в DBML, принято этим контрактом) или + `1105` как в распознанной форме ББ (ФСБУ 4/2023). Контракта это не касается напрямую (он про таблицу `goodwill`, не + про `kfo_result_line`), но влияет на отдельную задачу построения КФО — не блокирует 1543/1544. +- Несколько `is_head_entity` в одном периметре — не встречалось на фикстурах, поведение не проверено отдельно. + +**Фронт (AURORA-1544) ничего из этого не ждёт** — строит экран по схемам §4 на фейковых данных; поля не меняются от +исхода открытых вопросов, меняются только числа внутри них. + +## 6. Пример + +`GET /api/v1/consolidations/versions/7b…/goodwill` (сокращено): + +```json +{ + "version_id": "7b1c…", + "goodwill_enabled": true, + "editable": true, + "read_only_reason": null, + "rows": [ + { + "head_legal_entity": {"id": "a…", "name": "АО «Русский бисквит»"}, + "subsidiary_legal_entity": {"id": "b…", "name": "ООО «Пекарня Счастливочная»"}, + "ownership_share": "100.0000", + "financial_investment_amount": "5000000.00", + "charter_capital_amount": "4500000.00", + "additional_capital_amount": "0.00", + "participation_amount": "4500000.00", + "goodwill_amount": "500000.00", + "include_in_kfo": true, + "comment": "Доля подтверждена уставом от 01.01.2024", + "updated_by": {"id": "u…", "name": "Иванов И. И."}, + "updated_at": "2026-10-30T12:00:00+03:00" + } + ], + "totals": { + "financial_investment_amount": "5000000.00", + "participation_amount": "4500000.00", + "goodwill_amount": "500000.00", + "goodwill_in_kfo_amount": "500000.00" + } +} +``` diff --git a/dev/api-contract-goodwill.openapi.yaml b/dev/api-contract-goodwill.openapi.yaml new file mode 100644 index 0000000..892176f --- /dev/null +++ b/dev/api-contract-goodwill.openapi.yaml @@ -0,0 +1,278 @@ +openapi: 3.1.0 +info: + title: АРМ КА — таблица гудвилла + version: 0.1.0-draft + description: > + Черновик контракта AURORA-1543 (бэкенд) / AURORA-1544 (фронт). Текстовое описание, источники и открытые вопросы — + dev/api-contract-goodwill.md; оба файла держатся одинаковыми. Деньги — строки с двумя знаками в рублях, доля + владения — строка с четырьмя знаками. Чтение — CONSOLIDATION_VIEW, правки — CONSOLIDATION_EDIT. +servers: + - url: /api/v1 +security: + - bearerAuth: [] + +paths: + /consolidations/versions/{version_id}/goodwill: + get: + operationId: getGoodwillTable + summary: Таблица гудвилла версии + x-permission: CONSOLIDATION_VIEW + parameters: + - $ref: '#/components/parameters/VersionId' + responses: + '200': + description: Строки пар (ГО, ДО) периметра, итоги + content: + application/json: + schema: { $ref: '#/components/schemas/GoodwillTableOut' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/DomainError' } + + /consolidations/versions/{version_id}/goodwill-settings: + put: + operationId: saveGoodwillSettings + summary: Переключатель расчёта гудвилла версии (consolidation_version.goodwill_enabled) + description: > + Аудит GOODWILL_ENABLED_CHANGE; результат КФО устаревает при любом изменении значения. Выключение не стирает + сохранённые строки (ownership_share, comment, include_in_kfo). + x-permission: CONSOLIDATION_EDIT + parameters: + - $ref: '#/components/parameters/VersionId' + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/GoodwillSettingsIn' } + responses: + '200': + description: Таблица с обновлённым флагом + content: + application/json: + schema: { $ref: '#/components/schemas/GoodwillTableOut' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/DomainError' } + '409': { $ref: '#/components/responses/DomainError' } + + /consolidations/versions/{version_id}/goodwill/{head_legal_entity_id}/{subsidiary_legal_entity_id}: + put: + operationId: saveGoodwillRow + summary: Сохранить строку гудвилла (создать или заменить целиком) + description: > + Аудит GOODWILL_ROW_EDIT; результат КФО устаревает. Вычисляемые поля (financial_investment_amount, + charter_capital_amount, additional_capital_amount, participation_amount, goodwill_amount) в тело запроса не + входят — пересчитываются сервером. 404 — версия или пара (head/subsidiary) не из периметра; 409 — правок + версии; 422 — COMMENT_REQUIRED (include_in_kfo = true без комментария), INVALID_SHARE (вне 0…100 или больше + 4 знаков после запятой). + x-permission: CONSOLIDATION_EDIT + parameters: + - $ref: '#/components/parameters/VersionId' + - $ref: '#/components/parameters/HeadLegalEntityId' + - $ref: '#/components/parameters/SubsidiaryLegalEntityId' + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/GoodwillRowIn' } + responses: + '200': + description: Пересчитанная строка + content: + application/json: + schema: { $ref: '#/components/schemas/GoodwillRowOut' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/DomainError' } + '409': { $ref: '#/components/responses/DomainError' } + '422': { $ref: '#/components/responses/DomainOrValidationError' } + + /consolidations/versions/{version_id}/goodwill/export: + get: + operationId: exportGoodwillTable + summary: Выгрузка таблицы гудвилла в xlsx + description: Аудит EXPORT. + x-permission: CONSOLIDATION_VIEW + parameters: + - $ref: '#/components/parameters/VersionId' + responses: + '200': + description: Файл xlsx + content: + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: + schema: { type: string, format: binary } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/DomainError' } + +components: + securitySchemes: + bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT } + + parameters: + VersionId: + name: version_id + in: path + required: true + schema: { type: string, format: uuid } + HeadLegalEntityId: + name: head_legal_entity_id + in: path + required: true + description: Член периметра с is_head_entity = true + schema: { type: string, format: uuid } + SubsidiaryLegalEntityId: + name: subsidiary_legal_entity_id + in: path + required: true + description: Любой другой член периметра + schema: { type: string, format: uuid } + + responses: + Unauthorized: + description: Нет или плохой токен + Forbidden: + description: Нет права + content: + application/json: + schema: + type: object + properties: { detail: { type: string } } + DomainError: + description: Доменная ошибка + content: + application/json: + schema: { $ref: '#/components/schemas/DomainErrorOut' } + DomainOrValidationError: + description: Доменная ошибка или массив ошибок валидации pydantic + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/DomainErrorOut' + - type: object + properties: + detail: + type: array + items: + type: object + properties: + loc: { type: array, items: {} } + msg: { type: string } + type: { type: string } + + schemas: + Money: + type: string + pattern: '^-?\d+\.\d{2}$' + description: Сумма в рублях, два знака; может быть отрицательной (goodwill_amount) + examples: ['500000.00'] + Share: + type: [string, 'null'] + pattern: '^\d{1,3}\.\d{4}$' + description: Доля владения, % (numeric(7,4)); null — ещё не введена + ShareIn: + type: string + pattern: '^\d{1,3}(\.\d{1,4})?$' + description: 0…100, до 4 знаков после запятой; вне диапазона — 422 INVALID_SHARE + Ref: + type: object + required: [id, name] + properties: + id: { type: string, format: uuid } + name: { type: string } + + DomainErrorOut: + type: object + required: [detail] + properties: + detail: + type: object + required: [code, message] + properties: + code: + type: string + enum: + - NOT_FOUND + - VERSION_NOT_EDITABLE + - LOCK_REQUIRED + - LOCKED_BY_OTHER + - COMMENT_REQUIRED + - INVALID_SHARE + message: { type: string, description: Текст по-русски } + additionalProperties: true + + GoodwillTableOut: + type: object + required: [version_id, goodwill_enabled, editable, read_only_reason, rows, totals] + properties: + version_id: { type: string, format: uuid } + goodwill_enabled: { type: boolean } + editable: { type: boolean } + read_only_reason: + type: [string, 'null'] + enum: [null, VERSION_NOT_EDITABLE, LOCK_REQUIRED, LOCKED_BY_OTHER] + rows: + type: array + items: { $ref: '#/components/schemas/GoodwillRowOut' } + totals: { $ref: '#/components/schemas/GoodwillTotalsOut' } + + GoodwillRowOut: + type: object + required: + - head_legal_entity + - subsidiary_legal_entity + - ownership_share + - financial_investment_amount + - charter_capital_amount + - additional_capital_amount + - participation_amount + - goodwill_amount + - include_in_kfo + - comment + - updated_by + - updated_at + properties: + head_legal_entity: { $ref: '#/components/schemas/Ref' } + subsidiary_legal_entity: { $ref: '#/components/schemas/Ref' } + ownership_share: { $ref: '#/components/schemas/Share' } + financial_investment_amount: { $ref: '#/components/schemas/Money' } + charter_capital_amount: { $ref: '#/components/schemas/Money' } + additional_capital_amount: { $ref: '#/components/schemas/Money' } + participation_amount: { $ref: '#/components/schemas/Money' } + goodwill_amount: { $ref: '#/components/schemas/Money' } + include_in_kfo: { type: boolean } + comment: { type: [string, 'null'] } + updated_by: + oneOf: + - $ref: '#/components/schemas/Ref' + - type: 'null' + updated_at: { type: [string, 'null'], format: date-time } + + GoodwillTotalsOut: + type: object + required: [financial_investment_amount, participation_amount, goodwill_amount, goodwill_in_kfo_amount] + properties: + financial_investment_amount: { $ref: '#/components/schemas/Money' } + participation_amount: { $ref: '#/components/schemas/Money' } + goodwill_amount: { $ref: '#/components/schemas/Money' } + goodwill_in_kfo_amount: + allOf: [{ $ref: '#/components/schemas/Money' }] + description: Сумма goodwill_amount только по строкам include_in_kfo = true с положительным goodwill_amount + + GoodwillRowIn: + type: object + required: [ownership_share, include_in_kfo, comment] + properties: + ownership_share: { $ref: '#/components/schemas/ShareIn' } + include_in_kfo: { type: boolean } + comment: + type: [string, 'null'] + maxLength: 1000 + description: Обязателен (непустой после обрезки), если include_in_kfo = true + + GoodwillSettingsIn: + type: object + required: [goodwill_enabled] + properties: + goodwill_enabled: { type: boolean }