diff --git a/dev/api-contract-corrections.md b/dev/api-contract-corrections.md new file mode 100644 index 0000000..6f2d69c --- /dev/null +++ b/dev/api-contract-corrections.md @@ -0,0 +1,286 @@ +# Контракт 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 `; +- чтение — право `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"} +} +``` diff --git a/dev/api-contract-corrections.openapi.yaml b/dev/api-contract-corrections.openapi.yaml new file mode 100644 index 0000000..64b2d49 --- /dev/null +++ b/dev/api-contract-corrections.openapi.yaml @@ -0,0 +1,449 @@ +openapi: 3.1.0 +info: + title: АРМ КА — таблица корректировок + version: 0.1.0-draft + description: > + Черновик контракта AURORA-1540 (бэкенд) / AURORA-1541 (фронт). Текстовое описание, источники и открытые вопросы — + dev/api-contract-corrections.md; оба файла держатся одинаковыми. Деньги — строки с двумя знаками в рублях, проценты — + строки с двумя знаками. Чтение — CONSOLIDATION_VIEW, правки — CONSOLIDATION_EDIT. +servers: + - url: /api/v1 +security: + - bearerAuth: [] + +paths: + /consolidations/versions/{version_id}/corrections: + get: + operationId: getCorrectionTable + summary: Таблица корректировок версии + x-permission: CONSOLIDATION_VIEW + parameters: + - $ref: '#/components/parameters/VersionId' + - $ref: '#/components/parameters/StatementTypeQuery' + responses: + '200': + description: Агрегаты со строками, нераспределённые строки, итоги + content: + application/json: + schema: { $ref: '#/components/schemas/CorrectionTableOut' } + '401': { $ref: '#/components/responses/Unauthorized' } + '403': { $ref: '#/components/responses/Forbidden' } + '404': { $ref: '#/components/responses/DomainError' } + + /consolidations/versions/{version_id}/corrections/aggregates/{rule_id}/{company_1_id}/{company_2_id}: + put: + operationId: saveCorrection + summary: Сохранить корректировку агрегата (создать или заменить целиком) + description: > + Аудит CORRECTION_CREATE (первая) / CORRECTION_EDIT; version_no++; результат КФО устаревает. + Ошибки 409 — VERSION_NOT_EDITABLE, LOCK_REQUIRED, LOCKED_BY_OTHER, CORRECTION_VERSION_CONFLICT; + 422 — COMMENT_REQUIRED, EMPTY_CORRECTION, INVALID_LINE_CODE (detail.line_code). + x-permission: CONSOLIDATION_EDIT + parameters: + - $ref: '#/components/parameters/VersionId' + - $ref: '#/components/parameters/RuleId' + - $ref: '#/components/parameters/Company1Id' + - $ref: '#/components/parameters/Company2Id' + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CorrectionIn' } + responses: + '200': + description: Пересчитанный агрегат + content: + application/json: + schema: { $ref: '#/components/schemas/CorrectionAggregateOut' } + '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}/corrections/aggregates/{rule_id}/{company_1_id}/{company_2_id}/revoke: + post: + operationId: revokeCorrection + summary: Отозвать действующую корректировку агрегата + description: > + Статус REVOKED, запись остаётся в истории; аудит CORRECTION_REVOKE. + 409 — правки версии, CORRECTION_NOT_APPLIED; 422 — COMMENT_REQUIRED. + x-permission: CONSOLIDATION_EDIT + parameters: + - $ref: '#/components/parameters/VersionId' + - $ref: '#/components/parameters/RuleId' + - $ref: '#/components/parameters/Company1Id' + - $ref: '#/components/parameters/Company2Id' + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CorrectionRevokeIn' } + responses: + '200': + description: Пересчитанный агрегат + content: + application/json: + schema: { $ref: '#/components/schemas/CorrectionAggregateOut' } + '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}/corrections/allocations: + post: + operationId: allocateEntry + summary: Решение по строке сводной ОСВ (vgo_assignment) + description: > + ASSIGN — отнести/перенести в правило; UNASSIGN — убрать из агрегата; EXCLUDE / INCLUDE — «Не включать в КФО» и снятие; + RESET — вернуть к автоматике. 409 — правки версии; 422 — ENTRY_NOT_IN_VERSION, RULE_NOT_APPLICABLE (detail.reason), + RULE_REQUIRED. Ответ — вся таблица. + x-permission: CONSOLIDATION_EDIT + parameters: + - $ref: '#/components/parameters/VersionId' + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/AllocationIn' } + responses: + '200': + description: Пересчитанная таблица + content: + application/json: + schema: { $ref: '#/components/schemas/CorrectionTableOut' } + '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}/corrections/export: + get: + operationId: exportCorrectionTable + summary: Выгрузка таблицы корректировок в xlsx + description: Аудит EXPORT. + x-permission: CONSOLIDATION_VIEW + parameters: + - $ref: '#/components/parameters/VersionId' + - $ref: '#/components/parameters/StatementTypeQuery' + 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 } + RuleId: + name: rule_id + in: path + required: true + schema: { type: string, format: uuid } + Company1Id: + name: company_1_id + in: path + required: true + description: Компания стороны 1 правила (дебетовая сторона агрегата) + schema: { type: string, format: uuid } + Company2Id: + name: company_2_id + in: path + required: true + description: Компания стороны 2 правила (кредитовая сторона агрегата) + schema: { type: string, format: uuid } + StatementTypeQuery: + name: statement_type + in: query + required: false + schema: { $ref: '#/components/schemas/StatementType' } + + 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: Сумма в рублях, два знака + examples: ['1000000.00'] + MoneyIn: + type: string + pattern: '^\d+(\.\d{1,2})?$' + description: Неотрицательная сумма в рублях, не более двух знаков + Percent: + type: [string, 'null'] + pattern: '^\d+\.\d{2}$' + description: Процент с двумя знаками; null — не определён + StatementType: + type: string + enum: [balance, income] + default: balance + Side: + type: string + enum: [D, K] + 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 + - CORRECTION_VERSION_CONFLICT + - CORRECTION_NOT_APPLIED + - COMMENT_REQUIRED + - EMPTY_CORRECTION + - INVALID_LINE_CODE + - ENTRY_NOT_IN_VERSION + - RULE_NOT_APPLICABLE + - RULE_REQUIRED + message: { type: string, description: Текст по-русски } + additionalProperties: true + + CorrectionTableOut: + type: object + required: [version_id, statement_type, rules_version_no, editable, read_only_reason, rules, aggregates, unallocated, totals] + properties: + version_id: { type: string, format: uuid } + statement_type: { $ref: '#/components/schemas/StatementType' } + rules_version_no: { type: integer } + editable: { type: boolean, description: Может ли текущий пользователь править таблицу } + read_only_reason: + type: [string, 'null'] + enum: [VERSION_NOT_EDITABLE, LOCK_REQUIRED, LOCKED_BY_OTHER, null] + rules: + type: array + items: { $ref: '#/components/schemas/CorrectionRuleOut' } + aggregates: + type: array + items: { $ref: '#/components/schemas/CorrectionAggregateOut' } + unallocated: + type: array + items: { $ref: '#/components/schemas/UnallocatedEntryOut' } + totals: { $ref: '#/components/schemas/CorrectionTotalsOut' } + + CorrectionRuleOut: + type: object + required: [id, rule_no, article_1_line, article_2_line, side_1, measure, company_1_accounts, company_2_accounts, company_1_excluded, company_2_excluded] + properties: + id: { type: string, format: uuid } + rule_no: { type: integer } + article_1_line: { type: string, description: 'Код статьи; составная — коды через «/» ("1450/1550")' } + article_2_line: { type: string, description: 'Код статьи; составная — коды через «/» ("1310/1350")' } + side_1: { $ref: '#/components/schemas/Side' } + measure: { type: string, enum: [closing, turnover] } + company_1_accounts: { type: array, items: { type: string }, description: Коды счетов стороны 1 } + company_2_accounts: { type: array, items: { type: string } } + company_1_excluded: { type: array, items: { type: string } } + company_2_excluded: { type: array, items: { type: string } } + + CorrectionAggregateOut: + type: object + required: [rule_id, rule_no, company_1, company_2, side_1, side_2, deviation_amount, deviation_percent, exceeds_threshold, status, eliminated_amount, composition_changed, correction] + properties: + rule_id: { type: string, format: uuid } + rule_no: { type: integer } + company_1: { $ref: '#/components/schemas/Ref' } + company_2: { $ref: '#/components/schemas/Ref' } + side_1: { $ref: '#/components/schemas/AggregateSideOut' } + side_2: { $ref: '#/components/schemas/AggregateSideOut' } + deviation_amount: { $ref: '#/components/schemas/Money' } + deviation_percent: { $ref: '#/components/schemas/Percent' } + exceeds_threshold: + type: boolean + description: deviation_percent ≥ 5, или одна сторона пуста при ненулевой другой + status: { type: string, enum: [AUTO, DISCREPANCY, ONE_SIDED, MANUAL] } + eliminated_amount: { $ref: '#/components/schemas/Money' } + composition_changed: { type: boolean, description: Состав или суммы изменились после применённой корректировки } + correction: + oneOf: + - $ref: '#/components/schemas/CorrectionOut' + - type: 'null' + + AggregateSideOut: + type: object + required: [article_line, accounts, amount, entries] + properties: + article_line: { type: string } + accounts: { type: array, items: { type: string }, description: Коды счетов строк, вошедших в сторону } + amount: { $ref: '#/components/schemas/Money' } + entries: + type: array + items: { $ref: '#/components/schemas/CorrectionEntryOut' } + + CorrectionEntryOut: + type: object + required: [entry_ids, legal_entity_id, counterparty_id, account_code, side, amount, decision, excluded, candidate_rule_ids] + properties: + entry_ids: { type: array, items: { type: string, format: uuid }, minItems: 1 } + legal_entity_id: { type: string, format: uuid } + counterparty_id: { type: string, format: uuid } + account_code: { type: string } + side: { $ref: '#/components/schemas/Side' } + amount: { $ref: '#/components/schemas/Money' } + decision: { type: string, enum: [AUTO, ASSIGN] } + excluded: { type: boolean } + candidate_rule_ids: + type: array + items: { type: string, format: uuid } + description: Другие правила, под которые строка подходит по счёту и стороне + + UnallocatedEntryOut: + type: object + required: [entry_ids, owner, counterparty, account_code, side, amount, candidate_rule_ids, reason, excluded] + properties: + entry_ids: { type: array, items: { type: string, format: uuid }, minItems: 1 } + owner: { $ref: '#/components/schemas/Ref' } + counterparty: { $ref: '#/components/schemas/Ref' } + account_code: { type: string } + side: { $ref: '#/components/schemas/Side' } + amount: { $ref: '#/components/schemas/Money' } + candidate_rule_ids: { type: array, items: { type: string, format: uuid } } + reason: { type: string, enum: [MULTIPLE_RULES, UNASSIGNED] } + excluded: { type: boolean } + + CorrectionOut: + type: object + required: [status, version_no, debit, credit, comment, reason_code, balance, updated_by, updated_at] + properties: + status: { type: string, enum: [DRAFT, APPLIED] } + version_no: { type: integer } + debit: + type: array + items: { $ref: '#/components/schemas/CorrectionLineOut' } + description: Статьи стороны 1, первая — основная + credit: + type: array + items: { $ref: '#/components/schemas/CorrectionLineOut' } + description: Статьи стороны 2, первая — основная + comment: { type: string } + reason_code: { type: [string, 'null'] } + balance: { $ref: '#/components/schemas/CorrectionBalanceOut' } + updated_by: + oneOf: + - $ref: '#/components/schemas/Ref' + - type: 'null' + updated_at: { type: string, format: date-time } + + CorrectionLineOut: + type: object + required: [line_code, amount] + properties: + line_code: { type: string } + amount: { $ref: '#/components/schemas/Money' } + + CorrectionBalanceOut: + type: object + required: [debit_total, credit_total, difference, difference_percent, remainder_1, remainder_2] + properties: + debit_total: { $ref: '#/components/schemas/Money' } + credit_total: { $ref: '#/components/schemas/Money' } + difference: { $ref: '#/components/schemas/Money' } + difference_percent: { $ref: '#/components/schemas/Percent' } + remainder_1: { $ref: '#/components/schemas/Money', description: Сумма стороны 1 минус debit_total } + remainder_2: { $ref: '#/components/schemas/Money', description: Сумма стороны 2 минус credit_total } + + CorrectionTotalsOut: + type: object + required: [amount_1, amount_2, deviation_amount, correction_debit, correction_credit] + properties: + amount_1: { $ref: '#/components/schemas/Money' } + amount_2: { $ref: '#/components/schemas/Money' } + deviation_amount: { $ref: '#/components/schemas/Money' } + correction_debit: { $ref: '#/components/schemas/Money' } + correction_credit: { $ref: '#/components/schemas/Money' } + + CorrectionLineIn: + type: object + required: [line_code, amount] + properties: + line_code: { type: string, minLength: 1 } + amount: { $ref: '#/components/schemas/MoneyIn' } + + CorrectionIn: + type: object + required: [debit, credit, comment, expected_version_no] + properties: + debit: { type: array, items: { $ref: '#/components/schemas/CorrectionLineIn' } } + credit: { type: array, items: { $ref: '#/components/schemas/CorrectionLineIn' } } + comment: { type: string, maxLength: 1000 } + reason_code: { type: [string, 'null'] } + status: { type: string, enum: [APPLIED, DRAFT], default: APPLIED } + expected_version_no: + type: [integer, 'null'] + description: > + Редакция действующей (DRAFT/APPLIED) корректировки, которую видел аналитик; null — действующей нет + (в том числе после отзыва). Несовпадение — 409 CORRECTION_VERSION_CONFLICT. + + CorrectionRevokeIn: + type: object + required: [comment] + properties: + comment: { type: string, maxLength: 1000 } + + AllocationIn: + type: object + required: [statement_type, entry_ids, action] + properties: + statement_type: { $ref: '#/components/schemas/StatementType' } + entry_ids: { type: array, items: { type: string, format: uuid }, minItems: 1 } + action: { type: string, enum: [ASSIGN, UNASSIGN, EXCLUDE, INCLUDE, RESET] } + rule_id: { type: [string, 'null'], format: uuid, description: Обязателен для ASSIGN }