arm-ka-docs/dev/api-contract-corrections.md
Andrey Epifancev 1514630075 Контракт корректировок: сторона в AllocationIn, предел суммы (#183)
AllocationIn.side — позиция строки (D/K), vgo_assignment получает side
и уникальность по ней. MoneyIn ограничен numeric(18,2): не больше
16 цифр до запятой.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 22:36:09 +04:00

292 lines
26 KiB
Markdown
Raw Permalink 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), DOC-274230 «Правила пар счетов».
2. Дизайн таблицы корректировок от 23.10.2026 (вложение к DOC-272613).
3. ТЗ 6.8, Прил. 9 — только там, где аналитика молчит.
Открытые вопросы к аналитику — комментарий `[design-23-10-check]` в AURORA-1395 (12 пунктов + 3 по ТЗ) и в AURORA-1396
(ОФР). Места контракта, которые от них зависят, помечены **(ждёт п. N)**.
## 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(а)):
1. ручное решение по строке (`vgo_assignment`) важнее автоматики;
2. строка подходит под одно правило — в это правило, даже без встречной стороны;
3. под несколько правил и у второй компании есть строки по счетам другой стороны ровно одного из них — в это правило;
4. пара есть в нескольких правилах — в правило с меньшим номером Прил. 3 (ответ 2(а));
5. иначе — нераспределённая строка.
Каждая строка учитывается не более чем в одном агрегате своего типа отчётности.
- **Сторона агрегата.** Сторона 1 — строки компании 1 по счетам стороны 1 правила, сторона 2 — строки компании 2 по счетам
стороны 2. Сумма стороны — сумма строк, не отмеченных «не включать в КФО».
- **Отклонение** (DOC-272613 «Расчёт»): `deviation_amount = |amount_1 − amount_2|`;
`deviation_percent = deviation_amount / min(amount_1, amount_2) × 100`, если меньшая сторона > 0, иначе `null`.
**Порог: строго меньше 5 % — в допуске, от 5 % включительно — на проверку** (`exceeds_threshold`). Внимание: нынешний
`calculate_rule_totals` считает `> 5`; по аналитике нужно `≥ 5`.
- **Корректировка** — решение аналитика по агрегату: какие суммы и из каких статей ББ (ОФР) исключить по каждой стороне,
комментарий. Делается **только на уровне агрегата** (DOC-272613); на строку и на нераспределённую строку корректировка не
вводится **(ждёт п. 2)**. Сохранённая корректировка не меняет суммы сторон — они всегда считаются по строкам.
## 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/aggregates/{rule_id}/{company_1_id}/{company_2_id}/revoke` | EDIT | `CorrectionRevokeIn {comment}` | 200 `CorrectionAggregateOut` | 404 `NOT_FOUND`, 409 правок версии, 409 `CORRECTION_NOT_APPLIED`, 422 `COMMENT_REQUIRED` |
| `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` |
| `GET /consolidations/versions/{version_id}/corrections/export` | VIEW | query `statement_type` | файл xlsx | 404 `NOT_FOUND` |
Импорта xlsx в контракте нет — ждёт ответа по ТЗ 6.8 (третий пункт по ТЗ в `[design-23-10-check]`).
### 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.8) **(ждёт п. 6)**; не длиннее
1000 символов (как комментарий смены статуса), длиннее — 422 массивом pydantic. То же у `revoke`.
- `status` — `APPLIED` (по умолчанию) или `DRAFT`. В дизайне одна кнопка «Сохранить корректировки», фронт шлёт `APPLIED`
**(ждёт п. 9)**.
- `expected_version_no` — номер редакции действующей корректировки, которую аналитик видел (`correction.version_no`).
Действующей (`DRAFT` / `APPLIED`) нет — в том числе после отзыва — `null`. Не совпал с текущим, или `null`, а действующая
есть — 409 `CORRECTION_VERSION_CONFLICT` (кто-то сохранил раньше; фронт перечитывает таблицу). Новая корректировка после
отзыва — новые записи `correction`; отозванные остаются в истории.
- `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 …/revoke` — отозвать корректировку
Действующая корректировка агрегата получает статус `REVOKED` и остаётся в истории; комментарий обязателен. Действующей
нет — 409 `CORRECTION_NOT_APPLIED`. Аудит `CORRECTION_REVOKE`. **(ждёт п. 9 — как отзывать в интерфейсе)**
### 3.4 `POST …/allocations` — решение по строке
Одно действие над одной позицией: строка сводной ОСВ (`entry_ids` — как в `CorrectionEntryOut`) **и сторона** (`side`).
Сторона нужна потому, что строка с дебетовым и кредитовым сальдо одновременно даёт две позиции (§2), и решение по одной не
должно задевать другую. Хранение — `vgo_assignment` (DOC-272613 «Ручное добавление и перенос строк»), одно действующее
решение на `osv_entry` и сторону в версии и типе отчётности, повторное решение обновляет запись. Позиции с такой стороной у
строки нет — 422 `ENTRY_NOT_IN_VERSION`.
| `action` | Что делает | `vgo_assignment` |
|---|---|---|
| `ASSIGN` | отнести к правилу `rule_id` или перенести в него из другого агрегата | `action = ASSIGN`, `rule_id` |
| `UNASSIGN` | убрать из агрегата; автоматика её обратно не вернёт | `action = UNASSIGNED`, `rule_id = NULL` |
| `EXCLUDE` | «Не включать в КФО»: строка не входит в суммы сторон и элиминацию, остаётся видна с отметкой | `action = EXCLUDE`, прежний `rule_id` сохраняется |
| `INCLUDE` | снять «Не включать в КФО»: вернуть прежнее ручное отнесение, если оно было, иначе — автоматика | `action = ASSIGN` с прежним `rule_id` или запись удаляется |
| `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.
### 3.5 `GET …/export` — xlsx
Таблица выбранного типа отчётности, как на экране: агрегаты с колонками Дебет / Кредит / Контроль / Корректировка, затем
нераспределённые строки; суммы в рублях. Выбор колонок и фильтры в первой версии не передаются. Аудит `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)
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" как попала в агрегат
excluded bool «Не включать в КФО»; в сумму стороны не входит
candidate_rule_ids[] uuid другие правила, под которые строка подходит по счёту и стороне (для «Перенести в правило…»)
```
**`UnallocatedEntryOut`** — нераспределённая строка:
```
entry_ids[], owner {id, name}, counterparty {id, name}, account_code, side ("D"|"K"), amount
candidate_rule_ids[] uuid правила, под которые строка подходит по счёту и стороне
reason "MULTIPLE_RULES" | "UNASSIGNED" несколько правил / убрана аналитиком
excluded bool
```
Строка баланса владельца у нераспределённой строки не передаётся (DOC-272613 §4.3) **(ждёт п. 11)**.
**`CorrectionOut`**
```
status "DRAFT" | "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),
status? ("APPLIED" по умолчанию | "DRAFT"), expected_version_no (int|null)}`; `CorrectionLineIn {line_code, amount}`.
**`CorrectionRevokeIn`** `{comment}`.
**`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`.
- `vgo_assignment` — новая таблица (DOC-272613): `consolidation_version_id, statement_type, osv_entry_id, side (D | K),
rule_id (NULL), action (ASSIGN | UNASSIGNED | EXCLUDE), created_by/at, updated_by/at`, уникальность
`(consolidation_version_id, statement_type, osv_entry_id, side)`. Поля `side` в DOC-272613 нет — добавлено контрактом,
иначе у строки с дебетовым и кредитовым сальдо решения по двум позициям неразличимы. Нужна новая версия DBML и
`audit_action` для решений по строкам.
- Разнесение — чистая функция в `api/src/domain/` (как `elimination_packing`), без хранения агрегатов. Нынешняя упаковка
считает каждое правило отдельно (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", "excluded": false}]},
"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", "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"}
}
```