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

29 KiB
Raw Blame History

Контракт 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 (сокращено):

{
  "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"}
}