Andrey Epifancev 8940f32ae3 fix: пары ВГО — суммы со знаком, процент в упаковке, deviation_amount (ревью #175, AURORA-1536)
- суммы сторон со знаком, как в шахматке; строки с суммой < 0 в упаковку не идут, причина NEGATIVE_AMOUNT;
  при min(ΣA, ΣB) ≤ 0 процент null и элиминация 0, статус списка AUTO — только при элиминации > 0
- группы одного размера выбираются по проценту расхождения к меньшей сумме (точной дробью), ничья — по равному проценту
- elimination_amount группы и тройки переименован в deviation_amount; version_no тройки обязателен
- суммы и проценты пар в JSON — с двумя знаками
- elimination-pairs по правилу с measure ≠ closing — 422 INVALID_RULE
- дельта спеки, design.md (R-7, R-8), tasks, README

make check-full: exit 0 (1896 passed; integration 990 passed).

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

17 KiB
Raw Blame History

MODIFIED Requirements

Requirement: Создание консолидации

POST /consolidations (CONSOLIDATION_EDIT, policy по ГК) с телом {company_group_id, related_group_id, year, quarter, legal_entity_ids?} SHALL в одной транзакции создать consolidation (reporting_date: Q1 → 01.04.year, Q2 → 01.07.year, Q3 → 01.10.year, Q4 → 01.01.year+1; ASSUMPTION(OQ-40)), consolidation_version №1 (INITIATED, scenario = CREATE, approach = BASIC, goodwill_enabled = false, initiator_id и initiator_office_id из пользователя, is_in_work = true), consolidation_member периметра (is_head_entity = false), первую запись consolidation_status_history (from_status NULL → INITIATED), подхват подтверждённых документов ЮЛ периметра (см. documents-api «Подхват документов») и одно событие audit_event(CONSOLIDATION_CREATE), у которого after_value.members — отсортированный список ЮЛ периметра; ответ 201 с карточкой версии. Периметр SHALL быть: без legal_entity_ids (поле отсутствует или null) — текущие члены ГСК; с legal_entity_ids — ровно эти ЮЛ после схлопывания дублей (порядок первого вхождения), каждое SHALL принадлежать ГК консолидации (legal_entity.company_group_id), иначе 422 ENTITY_NOT_IN_GROUP с перечнем legal_entity_ids чужих в порядке запроса; состав ГСК при явном списке не проверяется и не меняется. ГСК SHALL быть обязательной, принадлежать ГК и иметь kind = gsk, иначе 422 RELATED_GROUP_MISMATCH. Периметр меньше двух ЮЛ (членов ГСК без списка либо выбранных ЮЛ после схлопывания) → 422 PERIMETER_TOO_SMALL; пара (ГК, год, квартал) уже есть → 409 CONSOLIDATION_EXISTS (в том числе при гонке двух запросов — UNIQUE (company_group_id, year, quarter)); year вне 2000..2100 → 422. Порядок проверок: ГК видима (404) → ГСК видима (404) и соответствует ГК (422) → периметр (422) → период свободен (409). Блокировка на создателя MUST NOT ставиться; MEMBERSHIP_CHANGE при создании MUST NOT писаться. related_group_id в consolidation SHALL оставаться NULL (фиксируется при верификации, Срез 7); related_group_member MUST NOT изменяться. В consolidation_version.elimination_rules_version SHALL записываться номер версии правил элиминации, действующей на московскую календарную дату создания: максимальный version_no среди правил с valid_from <= дата и valid_to IS NULL либо valid_to >= дата (OQ-174; DBML v5.23, поле NOT NULL). Пользователь версию правил не выбирает; год, квартал и reporting_date на номер не влияют; позже номер не меняется. Нет правил, действующих на дату, → 409 ELIMINATION_RULES_MISSING, консолидация не создаётся.

Scenario: Консолидация РБ за Q3 2024

  • WHEN ka_rf Санкт-Петербургского РФ создаёт консолидацию ГК «Русский бисквит», ГСК из двух ЮЛ, 2024, Q3, без legal_entity_ids
  • THEN 201; версия №1 INITIATED, reporting_date = 2024-10-01, два consolidation_member, история с одной записью, audit_event(CONSOLIDATION_CREATE) с consolidation_version_id; активной блокировки нет

Scenario: Выбранный периметр шире ГСК

  • WHEN в ГК три ЮЛ, в ГСК два из них, создаётся консолидация с legal_entity_ids из всех трёх ЮЛ ГК
  • THEN 201; в members три ЮЛ; after_value.members события CONSOLIDATION_CREATE — три ЮЛ; состав ГСК не изменился

Scenario: ГСК из одного ЮЛ, второе отмечено вручную

  • WHEN в ГСК один текущий член, в legal_entity_ids он и ещё одно ЮЛ той же ГК
  • THEN 201; в members два ЮЛ

Scenario: ГСК из одного ЮЛ

  • WHEN в ГСК один текущий член, legal_entity_ids не передан
  • THEN 422 PERIMETER_TOO_SMALL; consolidation и версия не созданы

Scenario: ЮЛ другой ГК в списке

  • WHEN в legal_entity_ids попало ЮЛ, company_group_id которого не равен ГК консолидации
  • THEN 422 ENTITY_NOT_IN_GROUP с этим ЮЛ в legal_entity_ids; consolidation не создана

Scenario: Дубли схлопываются до одного ЮЛ

  • WHEN legal_entity_ids — один и тот же идентификатор дважды
  • THEN 422 PERIMETER_TOO_SMALL

Scenario: Повтор периода

  • WHEN консолидация ГК за 2024 Q3 уже есть, создаётся ещё одна
  • THEN 409 CONSOLIDATION_EXISTS

Scenario: Правила действуют на дату создания версии

  • WHEN в день создания консолидации за 2024 год действует версия правил 2, а версия 1 действовала в отчётном периоде
  • THEN consolidation_version.elimination_rules_version = 2; номер не меняется при последующей публикации правил

Scenario: Нет действующих правил

  • WHEN у всех правил valid_from позже даты создания
  • THEN 409 ELIMINATION_RULES_MISSING; consolidation и версия не созданы

ADDED Requirements

Requirement: Правила элиминации зафиксированной версии

GET /api/v1/consolidations/versions/{version_id}/elimination-rules (CONSOLIDATION_VIEW, policy по ГК) SHALL возвращать объект {version_no, is_version_fixed, rules}: version_no — consolidation_version.elimination_rules_version (с DBML v5.23 задан у каждой версии), is_version_fixed = true (поле оставлено для совместимости клиента), rules — правила этой версии; правила других версий, в том числе более новых, MUST NOT попадать в ответ. Каждое правило SHALL содержать id, rule_no, version_no, statement_type, side_1 (D/K), measure (closing/turnover), company_1, company_2; каждая сторона SHALL содержать accounts: UUID[] и excluded: UUID[] до раскрытия иерархии счетов. Невидимая и несуществующая версия SHALL отвечать одинаковым 404 NOT_FOUND.

Scenario: Правило с исключением

  • WHEN у правила первая сторона включает счёт 76 и исключает 76.07.5
  • THEN ID 76 находится в company_1.accounts, ID 76.07.5 — в company_1.excluded

Scenario: Версия правил зафиксирована

  • WHEN у версии консолидации elimination_rules_version = 1
  • THEN ответ содержит is_version_fixed = true, version_no = 1 и только правила с version_no = 1

Scenario: Опубликована новая версия правил

  • WHEN после создания версии консолидации с elimination_rules_version = 1 открыта версия правил 2
  • THEN ответ по-прежнему содержит version_no = 1 и правила версии 1; пресет правила версии 2 для этой версии — 404

Requirement: Пары ВГО тройки

GET /api/v1/consolidations/versions/{version_id}/elimination-pairs?rule_id=&company_a_id=&company_b_id= SHALL требовать CONSOLIDATION_VIEW и видимость версии по ГК; три параметра обязательны. Компании SHALL быть разными участниками периметра версии (иначе 422 INVALID_COMPANY_PAIR), правило — правилом ББ версии правил версии консолидации с measure = closing (иначе 404 NOT_FOUND или 422 INVALID_RULE). Отбор счетов, раскрытие счетов-предков и исключения SHALL совпадать с шахматкой PRESET. Расчёт — по 05-domain-rules.md §5, на лету, без сохранения.

Итог тройки: amount_a — сумма строк A с контрагентом B по счетам стороны 1, amount_b — строк B с контрагентом A по счетам стороны 2, со знаком (как в шахматке); deviation_amount = |amount_a − amount_b|; deviation_percent — к min(amount_a, amount_b), null, если он ≤ 0; exceeds_threshold — процент > 5 % или меньшая сторона ≤ 0 при ненулевом расхождении; eliminated_amount = min(amount_a, amount_b) при расхождении ≤ 5 %, иначе, при elimination_mode = manual и при меньшей стороне ≤ 0 — 0. Упаковка итог не меняет. Каждое правило считается отдельно: строка общего счёта может войти в тройки нескольких правил (каскад отменён, AURORA-1751).

Упаковка: строка свода неделима; в группы идут только строки с суммой > 0; группы 1:1, 1:N, N:1 при N ≤ 5, N:M нет; сначала меньшее число строк, внутри размера — точные, затем с расхождением ≤ 5 % к меньшей сумме по возрастанию процента расхождения; строка — не более чем в одной группе правила; группы одного размера с одинаковым процентом, делящие строку, не сопоставляются. Ответ SHALL содержать version_no, rule_id, rule_no, company_a_id, company_b_id, итог тройки, matched[] (точные группы), with_discrepancy[] (расхождение ≤ 5 %) и unpaired[]. Группа — {rule_id, company_a_id, company_b_id, amount_a, amount_b, deviation_amount, deviation_percent, osv_entry_ids_a[], osv_entry_ids_b[]}; строка без пары — {row, reason}, reason: NO_MATCH, AMBIGUOUS (ничья), ZERO_AMOUNT, NEGATIVE_AMOUNT (сумма < 0, в упаковку не идёт). Суммы и проценты в JSON SHALL быть строками с двумя знаками.

Scenario: Точная строка среди несопоставленных остатков

  • WHEN правило № 6 для Счастливочной и АО «Русский бисквит» даёт суммы 110 429 113,15 и 106 311 116,46 руб., а точная группа 62.01 ↔ 60.01 составляет 106 311 116,46 руб.
  • THEN точная группа в matched, три остальные строки стороны A — в unpaired, eliminated_amount = 106311116.46, deviation_percent = 3.87

Scenario: Отрицательная строка

  • WHEN у A по B строки 150 и −50, у B по A строка 100
  • THEN amount_a = 100.00, amount_b = 100.00, eliminated_amount = 100.00; строка −50 — в unpaired с причиной NEGATIVE_AMOUNT

Scenario: Две строки свода упакованы с одной встречной

  • WHEN по одному правилу есть дебетовые строки 40 и 60 и встречная кредитовая строка 100
  • THEN matched[] содержит группу с суммами 100.00 и 100.00, deviation_amount = 0.00 и исходными osv_entry_ids_a/b

Scenario: Ручное правило и превышение порога

  • WHEN правило ручное либо расхождение больше 5 %
  • THEN eliminated_amount = 0.00 независимо от упаковки

Scenario: Исключённый счёт не входит в пары

  • WHEN правило включает родительский счёт 76 и исключает 76.07.5
  • THEN строки счёта 76.07.5 и его потомков не входят ни в группы, ни в unpaired

Scenario: Другая пара компаний не смешивается с выбранной

  • WHEN по одному правилу есть встречные строки для A→Б и A→В, а запрошены company_a_id=A, company_b_id=Б
  • THEN ответ содержит только строки A→Б и Б→A

Scenario: Расхождение сравнивается в процентах

  • WHEN со строкой 100 стороны A могут сойтись строки B 95,50 (4,50 руб., 4,71 %) и 104,60 (4,60 руб., 4,60 %)
  • THEN группа — 100 ↔ 104,60; строка 95,50 — в unpaired с причиной NO_MATCH

Scenario: Правило по оборотам

  • WHEN запрошена тройка по правилу ББ с measure = turnover
  • THEN 422 INVALID_RULE

Requirement: Пары ВГО версии

GET /api/v1/consolidations/versions/{version_id}/elimination-totals (CONSOLIDATION_VIEW, policy по ГК) SHALL возвращать {version_no, items[]} — итоги всех троек версии: правила ББ с measure = closing версии правил × упорядоченные пары разных компаний периметра, у которых есть строки хотя бы на одной стороне. Сводная ОСВ SHALL строиться один раз на запрос. Элемент — {rule_id, rule_no, elimination_mode, company_a_id, company_b_id, amount_a, amount_b, deviation_amount, deviation_percent, eliminated_amount, exceeds_threshold, status, osv_entry_ids_a[], osv_entry_ids_b[]} с теми же суммами, что у тройки («Пары ВГО тройки»); status: MANUAL — elimination_mode = manual; ONE_SIDED — одна из сумм нулевая; AUTO — eliminated_amount > 0; иначе DISCREPANCY (расхождение > 5 % или меньшая сторона < 0). Порядок — rule_no, затем порядок компаний периметра. Суммы и проценты — строки с двумя знаками. Невидимая и несуществующая версия — 404 NOT_FOUND.

Scenario: Список совпадает с тройкой

  • WHEN по правилу № 6 у A на B 300, у B на A 100, у B на A дебиторка 70 и у A на B кредиторка 70
  • THEN в items тройка (6, A, B) — DISCREPANCY, eliminated_amount = 0.00; тройка (6, B, A) — AUTO, eliminated_amount = 70.00; те же суммы возвращает elimination-pairs для каждой тройки

Scenario: Пересечение правил

  • WHEN дебиторка A на B по счетам № 6 есть, а займов B у A (67) нет
  • THEN в items есть и тройка (6, A, B), и тройка (7, A, B) со статусом ONE_SIDED — двойной учёт до разнесения строк (AURORA-1751)

Scenario: Правило без строк

  • WHEN по счетам правила № 3 строк ни у A, ни у B нет
  • THEN тройки (3, A, B) в items нет

Scenario: Обе стороны отрицательные

  • WHEN у A по B −50, у B по A −50
  • THEN тройка DISCREPANCY, deviation_percent = null, eliminated_amount = 0.00

REMOVED Requirements

Requirement: Правила элиминации версии

Reason: с DBML v5.23 elimination_rules_version задан у каждой версии консолидации; ветка «версия правил не зафиксирована — последние открытые правила» больше не существует. Migration: требование «Правила элиминации зафиксированной версии»; клиенту is_version_fixed всегда true.