Контракт API таблицы гудвилла (AURORA-1543/1544)

Черновик для параллельной разработки бэкенда и фронта, по аналогии с
api-contract-corrections: фронт строит экран на фейковых данных, не
дожидаясь методологов (AURORA-1369, срок 02.11) и бэкенда.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-30 13:30:52 +04:00
parent 727cb63ffc
commit 31b8e41674
2 changed files with 468 additions and 0 deletions

View File

@ -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 <JWT>`;
- чтение — право `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"
}
}
```

View File

@ -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 }