arm-ka-docs/dev/api-contract-corrections.md
Andrey Epifancev 9e2e9f55df Контракт корректировок: ответы Лысенко 29.10 и разделение с 1751 (#186)
- отзыв корректировки и status в PUT убраны: сохранение сразу APPLIED (ответ п. 9);
- «не учитывать в КФО» — только у нераспределённой строки, у строк агрегата excluded
  нет; EXCLUDE/INCLUDE строки агрегата — 422 ENTRY_ALLOCATED, ASSIGN исключённой —
  422 ENTRY_EXCLUDED; UNASSIGN уводит строку в нераспределённые (ответ п. 4);
- у нераспределённой строки — line_code, строка баланса владельца (ответ п. 11);
- vgo_assignment — только ручные решения (action ASSIGN | UNASSIGNED, is_exclude, side),
  автоматика — чистая функция 1751; кто что делает 1540/1751 ([allocation-split]);
- экспорт: один лист, имя Корректировки_<ГК>_<год>_<квартал>.xlsx, как в WIP #208;
- снято «ждёт п. N» там, где ответ есть; граница ровно 5 % — открытый вопрос.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 23:20:36 +04:00

298 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Контракт API таблицы корректировок (AURORA-1540 / AURORA-1541)
Черновик контракта для бэкенда (AURORA-1540) и фронта (AURORA-1541, arm-core #206). Машиночитаемая копия —
`dev/api-contract-corrections.openapi.yaml`; при расхождении двух файлов правится тот, что ошибся, и оба держатся одинаковыми.
Источники, в порядке приоритета:
1. Аналитика Лысенко: DOC-272613 «Корректировка данных агрегатом», ответы на вопросы 1–7 (комментарий от 27.10.2026 в
AURORA-1395), ответы на `[design-23-10-check]` (комментарий #9920570 от 29.10.2026 там же), DOC-274230 «Правила пар
счетов».
2. Дизайн таблицы корректировок от 23.10.2026 (вложение к DOC-272613).
3. ТЗ 6.8, Прил. 9 — только там, где аналитика молчит.
На `[design-23-10-check]` (12 пунктов + 3 по ТЗ) Лысенко ответил 29.10; ссылки вида «ответ п. N» — на этот комментарий.
Открыты: ОФР (AURORA-1396) и граница ровно 5 % (§2). Разделение работ с бэкендом — комментарии `[allocation-split]` в
AURORA-1540 и AURORA-1751 (29.10): разнесение строк и решения по строкам — 1751, корректировка и таблица — 1540.
## 1. Общее
Всё как в `dev/api-contract-slice-4.md` §1:
- префикс `/api/v1`, `Authorization: Bearer <JWT>`;
- чтение — право `CONSOLIDATION_VIEW`, правки — `CONSOLIDATION_EDIT`;
- версия вне видимости — 404 `NOT_FOUND`, как несуществующая;
- доменные ошибки — `{"detail": {"code", "message", ...}}`, 422 валидации тела — массив pydantic;
- **деньги — строки с двумя знаками** (`"1000000.00"`), всегда в **рублях** (суммы ОСВ); перевод в тыс. руб. и округление
для показа — на фронте;
- проценты — строки с двумя знаками (`"394.27"`), `null` — процент не определён.
**Правки.** Порядок проверок: право (403) → видимость (404) → версия редактируема (409) → операция. Какие статусы версии
разрешают корректировки, решает сервер (по §9; статус «Верифицирован» и закрытые — только чтение, подтверждено ответом по ТЗ); фронт не повторяет
это правило, а читает `editable` / `read_only_reason` из ответа таблицы. Ошибки правок версии — те же коды, что везде:
409 `VERSION_NOT_EDITABLE`, `LOCK_REQUIRED`, `LOCKED_BY_OTHER`.
## 2. Понятия
- **Строка** — неделимая строка сводной ОСВ версии (как `ConsolidatedRowOut`): `entry_ids` — её исходные `osv_entry`.
В разнесении участвуют строки уровня «счёт — контрагент», владелец и контрагент — компании периметра, тип данных — БУ
(DOC-272613 §1). Для ББ сумма строки — сальдо на конец, для ОФР — обороты по правилу (`measure`). Строка с дебетовым и
кредитовым сальдо одновременно даёт две позиции — `side = "D"` и `side = "K"`.
- **Агрегат** — правило × компания 1 × компания 2 (× тип отчётности, он следует из правила). Отдельно не хранится, считается
при каждом запросе (DOC-272613 §3). Ключ агрегата — `(rule_id, company_1_id, company_2_id)`.
- **Разнесение** (DOC-272613 «Распределение строк по правилам», ответы 1(б), 2(а)) — чистая функция от версии консолидации,
считается на каждый запрос и не хранится (DECISION-5); хранятся только ручные решения аналитика (`vgo_assignment`, §5):
1. ручное решение по строке (`vgo_assignment`) важнее автоматики;
2. строка подходит под одно правило — в это правило, даже без встречной стороны;
3. под несколько правил и у второй компании есть строки по счетам другой стороны ровно одного из них — в это правило;
4. пара есть в нескольких правилах — в правило с меньшим номером Прил. 3 (ответ 2(а));
5. иначе — нераспределённая строка.
Каждая строка учитывается не более чем в одном агрегате своего типа отчётности.
- **Сторона агрегата.** Сторона 1 — строки компании 1 по счетам стороны 1 правила, сторона 2 — строки компании 2 по счетам
стороны 2. Сумма стороны — сумма всех её строк: агрегат учитывается в КФО целиком, лишнюю строку убирают из правила, и она
становится нераспределённой (ответ п. 4).
- **«Не учитывать в КФО»** — отметка только у нераспределённой строки (ответ п. 4: «функционал нужен для нераспределённых
строк, чтобы суммы совпадали в КФО»). У строк агрегата отметки нет.
- **Отклонение** (DOC-272613 «Расчёт»): `deviation_amount = |amount_1 − amount_2|`;
`deviation_percent = deviation_amount / min(amount_1, amount_2) × 100`, если меньшая сторона > 0, иначе `null`.
**Порог: строго меньше 5 % — в допуске, от 5 % включительно — на проверку** (`exceeds_threshold`, DOC-272613).
**Открытый вопрос:** в описании AURORA-1540 и в WIP API (#208) ровно 5 % — в допуске (`> 5`); аналитик на это не отвечал.
Отклонение у отдельных строк агрегата не считается (ответ п. 3).
- **Корректировка** — решение аналитика по агрегату: какие суммы и из каких статей ББ (ОФР) исключить по каждой стороне,
комментарий. Делается **только на уровне агрегата** (DOC-272613, ответ п. 2); на строку и на нераспределённую строку
корректировка не вводится. Можно корректировать и агрегат в допуске 5 %, и с отклонением больше 5 % — с обязательным
комментарием (ответ п. 8 и по ТЗ). Сохранённая корректировка не меняет суммы сторон — они всегда считаются по строкам.
## 3. Эндпоинты
| Метод и путь | Право | Запрос | Ответ | Ошибки |
|---|---|---|---|---|
| `GET /consolidations/versions/{version_id}/corrections` | VIEW | query `statement_type` (`balance` по умолчанию / `income`) | 200 `CorrectionTableOut` | 404 `NOT_FOUND` |
| `PUT /consolidations/versions/{version_id}/corrections/aggregates/{rule_id}/{company_1_id}/{company_2_id}` | EDIT | `CorrectionIn` | 200 `CorrectionAggregateOut` | 404 `NOT_FOUND` (версия, правило или агрегата нет в версии), 409 правок версии, 409 `CORRECTION_VERSION_CONFLICT`, 422 `COMMENT_REQUIRED`, 422 `EMPTY_CORRECTION`, 422 `INVALID_LINE_CODE` |
| `POST /consolidations/versions/{version_id}/corrections/allocations` | EDIT | `AllocationIn` | 200 `CorrectionTableOut` | 404 `NOT_FOUND` (версия или правило), 409 правок версии, 422 `ENTRY_NOT_IN_VERSION`, 422 `RULE_NOT_APPLICABLE`, 422 `RULE_REQUIRED`, 422 `ENTRY_ALLOCATED`, 422 `ENTRY_EXCLUDED` |
| `GET /consolidations/versions/{version_id}/corrections/export` | VIEW | query `statement_type` | файл xlsx | 404 `NOT_FOUND` |
Импорта xlsx нет — не нужен (ответ по ТЗ). Отзыва корректировки (`REVOKED`) нет — её меняют повторным сохранением (ответ п. 9).
### 3.1 `GET …/corrections` — таблица
Вся таблица одним ответом: агрегаты со строками, нераспределённые строки, итоги. Без страниц — агрегатов в версии десятки,
строк — сотни; фильтры и сортировку делает фронт.
Порядок агрегатов: по `rule_no`, затем по имени компании 1, затем компании 2. Порядок нераспределённых: по имени владельца,
затем по коду счёта.
Пустой ответ (`aggregates = []`, `unallocated = []`) — внутригрупповых строк нет; это не ошибка.
### 3.2 `PUT …/aggregates/{rule_id}/{company_1_id}/{company_2_id}` — сохранить корректировку
Создаёт корректировку агрегата или заменяет действующую целиком (все статьи обеих сторон и комментарий приходят каждый раз).
- `debit[]` — статьи стороны 1 (компания 1), `credit[]` — стороны 2 (компания 2); первая статья стороны — основная, остальные
— дочерние (хранение — `correction.parent_correction_id`, глубина 1). Пустая сторона допустима, обе пустые — 422
`EMPTY_CORRECTION`.
- `line_code` — код строки формы ББ (ОФР) этой компании; не из формы — 422 `INVALID_LINE_CODE` с `detail.line_code`.
Сумма — строка ≥ 0, не больше 16 цифр до запятой и двух после (`numeric(18,2)`); это уменьшение статьи. Иное — 422
массивом pydantic.
- `comment` обязателен всегда, пустой после обрезки — 422 `COMMENT_REQUIRED` (DOC-272613, ответ п. 6); не длиннее
1000 символов (как комментарий смены статуса), длиннее — 422 массивом pydantic.
- Сохранение сразу применяет корректировку — `status = APPLIED`; поля `status` в теле нет (ответ п. 9). `DRAFT` есть в модели,
сервер его пока не выставляет.
- `expected_version_no` — номер редакции действующей корректировки, которую аналитик видел (`correction.version_no`).
Действующей нет — `null`. Не совпал с текущим, или `null`, а действующая есть — 409 `CORRECTION_VERSION_CONFLICT` (кто-то
сохранил раньше; фронт перечитывает таблицу).
- `line_code` — один код. У правил 1 и 2 статья стороны 2 составная (`article_2_line = "1450/1550"`, `"1310/1350"`, коды через
«/»); в корректировку пишется один из кодов.
- Корректировку можно сохранить, даже если стороны не сошлись; ответ показывает `balance` (контроль распределения). В КФО по
выбранным статьям уходит меньшая сумма (ответ п. 8). Ограничения «только при отклонении > 5 %» нет (ответ по ТЗ).
- Каждое сохранение увеличивает `version_no`; аудит `CORRECTION_CREATE` (первая) / `CORRECTION_EDIT` (следующие) с `before` /
`after`; результат КФО помечается устаревшим (AURORA-1417).
Ответ — пересчитанный агрегат.
### 3.3 `POST …/allocations` — решение по строке
Одно действие над одной позицией: строка сводной ОСВ (`entry_ids` — как в `CorrectionEntryOut`) **и сторона** (`side`).
Сторона нужна потому, что строка с дебетовым и кредитовым сальдо одновременно даёт две позиции (§2), и решение по одной не
должно задевать другую. Хранение — `vgo_assignment` (DOC-272613 «Ручное добавление и перенос строк», §5): только ручные
решения, одно действующее на `osv_entry` и сторону в версии и типе отчётности, повторное решение обновляет запись. Позиции с
такой стороной у строки нет — 422 `ENTRY_NOT_IN_VERSION`.
| `action` | Что делает | `vgo_assignment` |
|---|---|---|
| `ASSIGN` | отнести к правилу `rule_id` или перенести в него из другого агрегата; исключённую из КФО нераспределённую строку — нельзя, 422 `ENTRY_EXCLUDED` (сначала `INCLUDE`) | `action = ASSIGN`, `rule_id` |
| `UNASSIGN` | убрать из агрегата — строка становится нераспределённой (`reason = UNASSIGNED`), автоматика её обратно не вернёт (ответ п. 4) | `action = UNASSIGNED`, `rule_id = NULL` |
| `EXCLUDE` | «Не учитывать в КФО» — **только у нераспределённой строки**; строка остаётся видна с отметкой. У строки агрегата — 422 `ENTRY_ALLOCATED` (ответ п. 4) | `is_exclude = true`; записи не было — она создаётся ради отметки, `reason` строки не меняется |
| `INCLUDE` | снять «Не учитывать в КФО»; у строки агрегата — 422 `ENTRY_ALLOCATED` | `is_exclude = false`; запись, созданная только ради отметки, удаляется |
| `RESET` | снять ручное решение — строка снова разносится автоматически | запись удаляется |
Проверки `ASSIGN` (DOC-272613, шаг 2; ответ 3(а)): правило той же версии правил и того же типа отчётности; счёт и сторона
строки входят в сторону правила с учётом исключений; пара компаний агрегата — владелец и контрагент строки (её не выбирают),
обе в периметре. Не подходит — 422 `RULE_NOT_APPLICABLE` с `detail.reason`. `ASSIGN` без `rule_id` — 422 `RULE_REQUIRED`.
Строки нет среди строк версии — 422 `ENTRY_NOT_IN_VERSION`.
После решения пересчитываются затронутые агрегаты; у агрегата с применённой корректировкой ставится
`composition_changed = true` (ответ 5). Ответ — вся таблица (решение меняет и агрегаты, и нераспределённые строки).
Аудит: в `audit_action` DBML v5.23 подходящего действия нет — нужно новое (например, `VGO_ASSIGNMENT_CHANGE`) вместе с
таблицей `vgo_assignment` в DBML v5.24 (AURORA-1751).
### 3.4 `GET …/export` — xlsx
Таблица выбранного типа отчётности одним листом, как на экране: строка агрегата с колонками Дебет / Кредит / Контроль /
Корректировка, под ней его строки ОСВ (группировка, колонка «Правило» у них пустая), затем нераспределённые строки и итог;
суммы в рублях. Имя файла — `Корректировки_<название ГК>_<год>_<квартал>.xlsx` в `Content-Disposition` (`filename*`, ASCII
запасное `corrections.xlsx`), как в WIP API (#208). Выбор колонок и фильтры в первой версии не передаются. Аудит `EXPORT`.
## 4. Схемы
Поля без `?` есть всегда; `null` указан явно.
**`CorrectionTableOut`**
```
version_id uuid
statement_type "balance" | "income"
rules_version_no int версия правил элиминации версии консолидации
editable bool можно ли текущему пользователю править таблицу
read_only_reason null | "VERSION_NOT_EDITABLE" | "LOCK_REQUIRED" | "LOCKED_BY_OTHER"
rules[] CorrectionRuleOut правила типа отчётности (для пресетов и выбора правила)
aggregates[] CorrectionAggregateOut
unallocated[] UnallocatedEntryOut
totals CorrectionTotalsOut
```
**`CorrectionRuleOut`** `{id, rule_no, article_1_line, article_2_line, side_1 ("D"|"K"), measure ("closing"|"turnover"),
company_1_accounts[] (коды счетов стороны 1), company_2_accounts[], company_1_excluded[], company_2_excluded[]}`.
Подпись пресета фронт строит сам: «Правило N: <статья 1> — <статья 2>». Статья может быть составной — коды через «/»
(`"1450/1550"`), как в `elimination_rule`.
**`CorrectionAggregateOut`**
```
rule_id, rule_no
company_1 {id, name}
company_2 {id, name}
side_1 AggregateSideOut дебетовая сторона (компания 1)
side_2 AggregateSideOut кредитовая сторона (компания 2)
deviation_amount money
deviation_percent percent | null к меньшей стороне; null — меньшая сторона ≤ 0
exceeds_threshold bool deviation_percent ≥ 5, или одна сторона пуста при ненулевой другой
status "AUTO" | "DISCREPANCY" | "ONE_SIDED" | "MANUAL" как EliminationTotalItemOut
eliminated_amount money автоматическая сумма элиминации агрегата (§5), справочно
composition_changed bool состав или суммы изменились после применённой корректировки (ответ 5; ответ п. 10 — подсвечивать)
correction CorrectionOut | null действующая корректировка (APPLIED; DRAFT — зарезервирован)
```
**`AggregateSideOut`** `{article_line (статья правила), accounts[] (коды счетов строк, вошедших в сторону; не весь перечень
правила), amount (money), entries[] (CorrectionEntryOut)}`.
**`CorrectionEntryOut`** — строка внутри агрегата:
```
entry_ids[] uuid исходные osv_entry строки свода
legal_entity_id uuid владелец строки
counterparty_id uuid
account_code string
side "D" | "K"
amount money сальдо (оборот) строки по этой стороне
decision "AUTO" | "ASSIGN" как попала в агрегат
candidate_rule_ids[] uuid другие правила, под которые строка подходит по счёту и стороне (для «Перенести в правило…»)
```
**`UnallocatedEntryOut`** — нераспределённая строка:
```
entry_ids[], owner {id, name}, counterparty {id, name}, account_code, side ("D"|"K"), amount
line_code string | null строка баланса владельца, к которой привязан счёт (ответ п. 11); null — не определена
candidate_rule_ids[] uuid правила, под которые строка подходит по счёту и стороне
reason "MULTIPLE_RULES" | "UNASSIGNED" несколько правил / убрана аналитиком
excluded bool «Не учитывать в КФО» (только у нераспределённой)
```
**`CorrectionOut`**
```
status "DRAFT" | "APPLIED" сохранение даёт APPLIED
version_no int
debit[] CorrectionLineOut статьи стороны 1, первая — основная
credit[] CorrectionLineOut статьи стороны 2, первая — основная
comment string
reason_code string | null перечень DBML (ТЗ 6.4), для ОФР
balance CorrectionBalanceOut
updated_by {id, name} | null
updated_at datetime
```
**`CorrectionLineOut`** `{line_code, amount}`.
**`CorrectionBalanceOut`** — контроль распределения (DOC-272613 «Запись значений», шаг 3):
`{debit_total, credit_total, difference (|debit_total − credit_total|), difference_percent (к меньшему, null — меньший ≤ 0),
remainder_1 (сумма стороны 1 − debit_total), remainder_2 (сумма стороны 2 − credit_total)}`.
**`CorrectionTotalsOut`** — итоги по всем агрегатам ответа (без нераспределённых): `{amount_1, amount_2, deviation_amount,
correction_debit, correction_credit}`. При фильтрах на экране фронт считает итоги видимых строк сам.
**`CorrectionIn`** `{debit[] (CorrectionLineIn), credit[] (CorrectionLineIn), comment (string), reason_code? (string|null),
expected_version_no (int|null)}`; `CorrectionLineIn {line_code, amount}`.
**`AllocationIn`** `{statement_type, entry_ids[] (≥ 1), side ("D"|"K"), action ("ASSIGN"|"UNASSIGN"|"EXCLUDE"|"INCLUDE"|"RESET"),
rule_id? (uuid, для ASSIGN)}`.
## 5. Для бэкенда (AURORA-1540)
- Модель: `correction` — как в DBML v5.23; одна запись на сторону агрегата (основная, `parent_correction_id = NULL`) и дочерние
статьи; поля `debit_*` заполняются у записей стороны 1, `credit_*` — у стороны 2; `legal_entity_id` — компания стороны,
`counterparty_id` — другая компания агрегата. `deviation_*` записываются на момент сохранения — по ним считается
`composition_changed`.
- **Кто что делает** (`[allocation-split]`, 29.10): AURORA-1751 — разнесение строк (чистая функция), ручные решения
(`POST …/allocations`, `vgo_assignment`, аудит, DBML v5.24); AURORA-1540 — таблица, корректировка, экспорт поверх функции
1751 (источник агрегатов в WIP #208 — `CorrectionAggregateSource`, его реализация вызывает функцию 1751).
- `vgo_assignment` — **только ручные решения**, автоматическое разнесение не хранится (DECISION-5): иначе ручное решение не
отличить от автоматического и при пересчёте после замены ОСВ оно затрётся. Поля (миграция AURORA-1751 + сторона):
`consolidation_version_id, statement_type, osv_entry_id, side (D | K), rule_id (NULL), action (ASSIGN | UNASSIGNED),
is_exclude (bool), created_by/at, updated_by/at`; `ASSIGN` — с `rule_id`, `UNASSIGNED` — без; уникальность
`(consolidation_version_id, statement_type, osv_entry_id, side)`. Поля `side` в DOC-272613 и в миграции нет — добавлено
контрактом, иначе у строки с дебетовым и кредитовым сальдо решения по двум позициям неразличимы. Нужны DBML v5.24, ORM-модель
в `src/models` и `audit_action` для решений по строкам.
- Разнесение — чистая функция в `api/src/domain/` (как `elimination_packing`): строки свода × правила × ручные решения →
агрегаты и нераспределённые строки (с `line_code` и `candidate_rule_ids`). Нынешняя упаковка считает каждое правило
отдельно (TODO в `intercompany_matrix.build_pairs`) — для таблицы корректировок строка должна попадать не более чем в одно
правило.
- Правила ББ 1 и 2 (DOC-274230) имеют свой расчёт и не наследуют min; что идёт в `eliminated_amount` по ним — по DOC-274230.
- ОФР: тот же ответ с `statement_type = income` **(ждёт ответа в AURORA-1396)**; до него сервер может отдавать пустые
`aggregates` и `unallocated` для `income`.
## 6. Пример
`GET /api/v1/consolidations/versions/7b…/corrections?statement_type=balance` (сокращено):
```json
{
"version_id": "7b1c…",
"statement_type": "balance",
"rules_version_no": 1,
"editable": true,
"read_only_reason": null,
"rules": [{"id": "r6…", "rule_no": 6, "article_1_line": "1230", "article_2_line": "1520", "side_1": "D",
"measure": "closing", "company_1_accounts": ["60.02", "62.01", "76"], "company_2_accounts": ["60.01", "62.02", "76"],
"company_1_excluded": ["76.07.5", "76.07.9"], "company_2_excluded": ["76.07.1"]}],
"aggregates": [{
"rule_id": "r6…", "rule_no": 6,
"company_1": {"id": "a…", "name": "ЭПТ"}, "company_2": {"id": "b…", "name": "ИЛС"},
"side_1": {"article_line": "1230", "accounts": ["62.01"], "amount": "1000000.00",
"entries": [{"entry_ids": ["e1…"], "legal_entity_id": "a…", "counterparty_id": "b…", "account_code": "62.01",
"side": "D", "amount": "1000000.00", "decision": "AUTO"}]},
"side_2": {"article_line": "1520", "accounts": ["60.02"], "amount": "202320.00", "entries": ["…"]},
"deviation_amount": "797680.00", "deviation_percent": "394.27", "exceeds_threshold": true,
"status": "DISCREPANCY", "eliminated_amount": "0.00", "composition_changed": false,
"correction": {"status": "APPLIED", "version_no": 1,
"debit": [{"line_code": "1230", "amount": "1000000.00"}],
"credit": [{"line_code": "1520", "amount": "202320.00"}],
"comment": "Требуется сверка с контрагентом", "reason_code": null,
"balance": {"debit_total": "1000000.00", "credit_total": "202320.00", "difference": "797680.00",
"difference_percent": "394.27", "remainder_1": "0.00", "remainder_2": "0.00"},
"updated_by": {"id": "u…", "name": "Иванов И. И."}, "updated_at": "2026-10-28T12:00:00+03:00"}
}],
"unallocated": [{"entry_ids": ["e9…"], "owner": {"id": "c…", "name": "ООО «БСК»"},
"counterparty": {"id": "d…", "name": "ПМП ООО"}, "account_code": "76.05", "line_code": "1520", "side": "K",
"amount": "142878.00", "candidate_rule_ids": ["r6…", "r7…"], "reason": "MULTIPLE_RULES", "excluded": false}],
"totals": {"amount_1": "1000000.00", "amount_2": "202320.00", "deviation_amount": "797680.00",
"correction_debit": "1000000.00", "correction_credit": "202320.00"}
}
```