Markdown и OpenAPI для AURORA-1540 (бэкенд) и AURORA-1541 (фронт): таблица агрегатов и нераспределённых строк, сохранение и отзыв корректировки агрегата, решения по строке (vgo_assignment), экспорт. Основа — аналитика Лысенко (DOC-272613, ответы 1–7, DOC-274230) и дизайн 23.10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
287 lines
26 KiB
Markdown
287 lines
26 KiB
Markdown
# Контракт 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 с не более чем двумя знаками; это уменьшение статьи.
|
||
- `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`). Хранение — `vgo_assignment`
|
||
(DOC-272613 «Ручное добавление и перенос строк»), одно действующее решение на `osv_entry` в версии и типе отчётности,
|
||
повторное решение обновляет запись.
|
||
|
||
| `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), 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, rule_id (NULL),
|
||
action (ASSIGN | UNASSIGNED | EXCLUDE), created_by/at, updated_by/at`, уникальность `(consolidation_version_id,
|
||
statement_type, osv_entry_id)`. Нужна новая версия 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"}
|
||
}
|
||
```
|