docs(openspec): elimination-pairs-rule-totals — дельта вместо правки основной спеки, решения, README (ревью #175)
Основная спека consolidations-api возвращена к main: фиксация версии правил, пары тройки и список версии — в дельте change (MODIFIED «Создание консолидации», ADDED «Пары ВГО тройки», «Пары ВГО версии», «Правила элиминации зафиксированной версии», REMOVED старое требование правил); data-model — DBML 5.23 и миграция 0010. design.md — решения владельца 23.10 и «Decisions made without discussion» R-1…R-6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b4522addc0
commit
fdb1f179cc
18
README.md
18
README.md
@ -940,8 +940,9 @@ API: список счетов `GET /directories/accounts`, правила ве
|
||||
`GET …/intercompany-matrix/preset/{rule_id}` и его выгрузка `…/preset/{rule_id}/export`.
|
||||
Режим ALL — только балансовые счета (без 001–012), MANUAL принимает и забалансовые. `one_sided` — ровно одна сумма
|
||||
пары нулевая; пара подсвечивается (и в xlsx) при `exceeds_threshold` или `one_sided` (AURORA-1651).
|
||||
`GET …/elimination-rules` отдаёт объект `{version_no, is_version_fixed, rules}` (у правила есть `id`): при незаданной
|
||||
версии правил — открытые правила (`valid_to IS NULL`) с максимальным `version_no`; ответ сообщает этот номер. Ошибки PRESET: 422 `RULE_REQUIRED`, `INVALID_RULE`,
|
||||
`GET …/elimination-rules` отдаёт объект `{version_no, is_version_fixed, rules}` (у правила есть `id`): правила версии
|
||||
`consolidation_version.elimination_rules_version` — она фиксируется при создании версии консолидации (действующая на дату
|
||||
создания, OQ-174) и с DBML v5.23 есть всегда, `is_version_fixed = true`. Ошибки PRESET: 422 `RULE_REQUIRED`, `INVALID_RULE`,
|
||||
`ACCOUNTS_REQUIRED`; правило недействительное для версии или несуществующее — 404. Имя XLSX пресета оканчивается на
|
||||
`_rule-balance-<rule_no>.xlsx`; в шапке — номер, статьи и версия правила, строка «Показатель»,
|
||||
audit `EXPORT` пишет `mode` (`ALL`/`MANUAL`/`PRESET`), для PRESET — `rule_id`, `rule_no` и фактический
|
||||
@ -951,6 +952,17 @@ audit `EXPORT` пишет `mode` (`ALL`/`MANUAL`/`PRESET`), для PRESET — `r
|
||||
`openspec/specs/{directories-api,consolidations-api,consolidated-osv,audit-trail}/spec.md`.
|
||||
Расчёт читает сводную ОСВ версии, результат не хранится.
|
||||
|
||||
**Пары ВГО §5 (AURORA-1536, PR #175).** `GET …/elimination-pairs?rule_id=&company_a_id=&company_b_id=` — итог тройки
|
||||
(ΣA, ΣB со знаком, расхождение, процент, `eliminated_amount` по правилу 5 %) и упаковка строк 1:1 / 1:N / N:1
|
||||
(`matched`, `with_discrepancy`, `unpaired` с причиной); `GET …/elimination-totals` — итоги всех троек версии (правила ББ ×
|
||||
упорядоченные пары периметра) со статусом `AUTO` / `DISCREPANCY` / `ONE_SIDED` / `MANUAL`, сводная ОСВ строится один раз.
|
||||
Каскада по остатку нет: строка общего счёта входит в несколько правил до разнесения (AURORA-1751). Ошибки: 404 (версия,
|
||||
правило не из версии правил), 422 `INVALID_COMPANY_PAIR`, `INVALID_RULE`, `ACCOUNTS_REQUIRED`. Решения —
|
||||
`openspec/changes/elimination-pairs-rule-totals/design.md`; контракт — `../docs/dev/frontend-api-guide.md` §5.7.2.
|
||||
Поставка DBML v5.23: миграция `0010_dbml_v5_23` проставляет пустой `elimination_rules_version` по дате создания версии и
|
||||
делает колонку NOT NULL; нет правил вовсе — миграция падает до изменения схемы, поэтому seed правил должен быть применён
|
||||
раньше. Откат снимает только NOT NULL.
|
||||
|
||||
## Ошибки API
|
||||
|
||||
Доменные ошибки (`src/common/exceptions.py`, обработчик — `src/core/exception_handlers.py`) отдаются объектом:
|
||||
@ -1031,6 +1043,7 @@ email без учёта регистра проверяет только сер
|
||||
| `INVALID_IDS` | 422 | `GET /users/brief`: элемент `ids` — не UUID |
|
||||
| `FILE_SUPERSEDES_OTHERS` | 409 | удаление файла, на который ссылаются `superseded_by_file_id` других файлов |
|
||||
| `CONSOLIDATION_EXISTS` | 409 | консолидация этой ГК за год и квартал уже создана (в т. ч. гонка двух `POST /consolidations`) |
|
||||
| `ELIMINATION_RULES_MISSING` | 409 | `POST /consolidations`: нет правил элиминации, действующих на дату создания (DBML v5.23: версия правил обязательна) |
|
||||
| `PERIMETER_TOO_SMALL` | 422 | в ГСК (при создании без списка), в выбранных ЮЛ (`legal_entity_ids` при создании) или в новом периметре (`PUT …/members`) меньше двух ЮЛ; ничего не создаётся и не меняется |
|
||||
| `RELATED_GROUP_MISMATCH` | 422 | ГСК принадлежит другой ГК или имеет вид `gsz` (невидимая/неизвестная ГСК — 404 `NOT_FOUND`) |
|
||||
| `ENTITY_NOT_IN_GROUP` | 422 | `POST /consolidations` с `legal_entity_ids` и `PUT …/members`: ЮЛ не из ГК консолидации, без ГК или несуществующее (`legal_entity_ids` — перечень); проверка раньше `PERIMETER_TOO_SMALL` |
|
||||
@ -1041,6 +1054,7 @@ email без учёта регистра проверяет только сер
|
||||
| `ACCOUNT_KIND_MISMATCH` | 422 | MANUAL шахматки или её экспорта: вид счёта не подходит дебетовой/кредитовой стороне |
|
||||
| `ACCOUNTS_REQUIRED` | 422 | сервис шахматки: не выбран дебетовый или кредитовый набор; через HTTP пустые массивы отклоняются раньше валидацией тела |
|
||||
| `RESULT_ACCOUNTS_MISSING` | 422 | результирующая таблица ВГО: в плане счетов нет обязательных кодов статей |
|
||||
| `INVALID_COMPANY_PAIR` | 422 | пары ВГО тройки: компании совпадают или не входят в периметр версии |
|
||||
| `ACCOUNTS_NOT_ALLOWED` / `INVALID_MODE` | 422 | защита внутреннего вызова сервиса шахматки; маршруты ALL/MANUAL не дают вызвать эти ветки |
|
||||
| `COMMENT_REQUIRED` | 422 | `POST …/status` в `CANCELLED` без комментария (из одних пробелов — тоже) |
|
||||
| `VERSION_NOT_EDITABLE` | 409 | версия не «в работе» (правки) либо `CANCELLED` при «взять в работу» |
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
# elimination-pairs-rule-totals
|
||||
|
||||
Тесты упаковки и итог элиминации для тройки правила и компаний
|
||||
Пары ВГО §5 (AURORA-1536, PR #175): фиксация версии правил при создании версии консолидации (DBML v5.23, NOT NULL),
|
||||
итог и упаковка тройки «правило × A × B», список троек версии.
|
||||
|
||||
@ -1,10 +1,33 @@
|
||||
## Решение
|
||||
|
||||
Суммы выбранных строк считаются один раз до упаковки внутри тройки `(rule_id, A, B)` как чистая функция.
|
||||
`elimination_amount` сохраняет ранее согласованное значение абсолютного расхождения; новое `eliminated_amount`
|
||||
содержит `min(ΣA, ΣB)` при пороге не более 5 %, иначе 0, и всегда 0 для `manual`.
|
||||
Нулевая меньшая сторона даёт `deviation_percent = null` и нулевую элиминацию.
|
||||
Итог тройки `(rule_id, A, B)` считается чистой функцией по строкам обеих сторон до упаковки: суммы сторон со знаком,
|
||||
`deviation_amount = |ΣA − ΣB|`, процент к меньшей стороне, `eliminated_amount = min(ΣA, ΣB)` при расхождении ≤ 5 %,
|
||||
иначе и для `manual` — 0. Упаковка итог не меняет. Каждое правило считается отдельно (каскад по остатку отменён
|
||||
владельцем 21.10.2026): строка общего счёта входит в несколько правил, двойной учёт снимает AURORA-1751.
|
||||
|
||||
Сбор встречных строк тройки — одна функция `_collect_rule_sides` для итога, упаковки и списка версии: список версии
|
||||
(`build_totals`) строит сводную ОСВ один раз и проходит правила ББ × упорядоченные пары периметра без упаковки.
|
||||
|
||||
Версия правил фиксируется при создании версии консолидации (`latest_effective_version_no` на московскую дату) и дальше
|
||||
не меняется; ветки «правила не зафиксированы» в чтении правил, пресете шахматки и парах убраны.
|
||||
|
||||
Упаковка не влияет на итог. Для обратной направленной пары клиент переставляет `company_a_id` и `company_b_id`.
|
||||
Эталонный Excel используется только для сверки точных встречных строк: он списывает полную сумму каждой стороны,
|
||||
поэтому его общий итог не равен системному правилу минимума.
|
||||
поэтому его общий итог не равен системному правилу минимума (OQ-155).
|
||||
|
||||
## Решения владельца 23.10.2026 (ревью PR #175)
|
||||
|
||||
1. Суммы сторон — со знаком, как в шахматке; строки с отрицательной суммой в упаковку не идут (`NEGATIVE_AMOUNT`).
|
||||
2. Группы одного размера сортируются по проценту расхождения, а не по сумме.
|
||||
3. Кроме тройки — список пар всей версии одним запросом (делаем сами в ветке PR).
|
||||
4. `elimination_rules_version` — NOT NULL с миграцией существующих версий (DBML v5.23).
|
||||
|
||||
## Decisions made without discussion
|
||||
|
||||
| Маркер | Дата | Решение | Обоснование | Цена ошибки |
|
||||
|---|---|---|---|---|
|
||||
| R-1 (design #1) | 23.10.2026 | При `min(ΣA, ΣB) ≤ 0` процент `null`, элиминация 0, тройка подсвечивается при любом расхождении | Минимум со знаком дал бы элиминацию отрицательной суммы или отрицательный процент, который не пробивает порог; аналитик должен разобрать такой случай | Поменять одну функцию `calculate_rule_totals` и §5 |
|
||||
| R-2 (design #2) | 23.10.2026 | Список версии — `GET …/elimination-totals`, статусы `AUTO` / `DISCREPANCY` / `ONE_SIDED` / `MANUAL`; тройки без строк не возвращаются; только правила ББ с `measure = closing` | Повторяет три категории задачи («сопоставленные, с расхождением, без пары») плюс ручные правила; ОФР в §4 не формализован | Переименование маршрута и статусов — правка фронта до его разработки |
|
||||
| R-3 (design #3) | 23.10.2026 | Создание версии без правил, действующих на дату, — 409 `ELIMINATION_RULES_MISSING` | Поле NOT NULL; подставлять «последние открытые» значило бы взять правила, ещё не вступившие в силу | При пустом справочнике правил консолидацию не создать до seed — на практике seed всегда заводит v1 |
|
||||
| R-4 (design #4) | 23.10.2026 | Миграция: дата создания по Москве; пустой `created_at` — текущая дата; нет действующих на дату — последняя открытая; нет правил вовсе — отказ миграции | Совпадает с правилом создания; запасной вариант нужен только для данных, созданных до появления правил | Неверный номер у старых версий — исправляется UPDATE до верификации |
|
||||
| R-5 (design #5) | 23.10.2026 | `is_version_fixed` в ответе `elimination-rules` оставлен и всегда `true` | Фронт шахматки уже читает поле (PR #141, #158) | Удалить поле позже вместе с фронтом |
|
||||
| R-6 (design #6) | 23.10.2026 | Группа упаковки — `deviation_amount` вместо `elimination_amount`; отдельной суммы элиминации у группы нет | В КФО идёт итог тройки; упаковка его не меняет (§5), а сумма у группы читалась бы как вклад в элиминацию | Добавить поле у группы, если корректировкам понадобится |
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
## Why
|
||||
|
||||
Срез 5: упаковка ОСВ показывает группы строк, но итог элиминации тройки должен учитывать все выбранные строки,
|
||||
включая оставшиеся без пары. Это нужно закрепить тестами на реальных ОСВ группы «Русский бисквит» и отдельным
|
||||
контрактом для фронта.
|
||||
Срез 5, AURORA-1536: автоматическое сопоставление ВГО по `05-domain-rules.md` §5. Нужны итог элиминации по тройке
|
||||
«правило, компания A, компания B» (идёт в КФО), детализация строк ОСВ по группам и список всех троек версии для
|
||||
таблицы корректировок (AURORA-1540, AURORA-1541) и разнесения строк (AURORA-1751). Версия правил, по которой считается
|
||||
версия консолидации, должна быть зафиксирована всегда, иначе после публикации новых правил старые консолидации молча
|
||||
пересчитаются по ним.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Для запроса `(правило, A, B)` добавить общий итог ΣA/ΣB, расхождение, процент, признак порога и сумму
|
||||
элиминации по правилу 5 %; при `manual` сумма элиминации равна нулю.
|
||||
- Проверить упаковку 1:1, 1:N, N:1, ничьи и оба направления пары на реальных и модельных строках.
|
||||
- Проверить неизменность зафиксированной версии правил при повторных GET и сопоставить точные строки с эталонным Excel.
|
||||
- Описать новый ответ в гайде фронта.
|
||||
- При создании версии консолидации фиксировать `elimination_rules_version` — версию правил, действующую на дату
|
||||
создания (OQ-174); DBML v5.23 делает поле NOT NULL, миграция `0010_dbml_v5_23` заполняет существующие версии;
|
||||
без действующих правил версия не создаётся (409 `ELIMINATION_RULES_MISSING`).
|
||||
- `GET …/elimination-pairs` — итог тройки (суммы сторон со знаком, расхождение, процент, сумма элиминации по правилу 5 %)
|
||||
и упаковка 1:1 / 1:N / N:1 с причинами строк без пары.
|
||||
- `GET …/elimination-totals` — итоги всех троек версии одним запросом со статусом `AUTO` / `DISCREPANCY` / `ONE_SIDED` /
|
||||
`MANUAL`.
|
||||
- `GET …/elimination-rules` — всегда правила зафиксированной версии.
|
||||
|
||||
## Capabilities
|
||||
|
||||
@ -20,15 +25,17 @@
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `consolidations-api`: общий итог элиминации тройки независимо от упаковки строк.
|
||||
- `consolidations-api`: создание консолидации фиксирует версию правил; правила зафиксированной версии; пары ВГО тройки и
|
||||
версии.
|
||||
- `data-model`: DBML v5.23, миграция `0010_dbml_v5_23`.
|
||||
|
||||
## Impact
|
||||
|
||||
`arm-core`: чистый расчёт, сервис и схема ответа, тесты, OpenSpec. `arm-ka-docs`: гайд фронта.
|
||||
Читаются таблицы `consolidation_version`, `consolidation_member`, `consolidation_document`, `osv_entry`,
|
||||
`elimination_rule`, `elimination_rule_account`, `chart_of_account`; DBML и хранимые данные не меняются.
|
||||
Основание — `context/05-domain-rules.md` §4–§5, решение владельца 08.10.2026 о минимуме пары.
|
||||
`arm-core`: миграция, ORM, чистый расчёт, сервис, схемы, маршрут, тесты, OpenSpec. `arm-ka-docs` (#164): DBML v5.23,
|
||||
§4–§5 без каскада, OQ-173/174/175, гайд фронта §5.7.2. Основание — `context/05-domain-rules.md` §4–§5, решения
|
||||
владельца 08.10, 19.10, 21.10 и 23.10.2026.
|
||||
|
||||
## Non-goals
|
||||
|
||||
Пары всех правил версии в одном запросе, сохранение результатов в БД, корректировки, расчёт ОФР.
|
||||
Сохранение пар в БД (DECISION-5), разнесение строк по правилам и снятие двойного учёта (AURORA-1751), корректировки
|
||||
(AURORA-1540), правила и пары ОФР.
|
||||
|
||||
@ -1,21 +1,168 @@
|
||||
## 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: Итог элиминации тройки независимо от упаковки
|
||||
### 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`.
|
||||
|
||||
Для `(rule_id, company_a_id, company_b_id)` API SHALL возвращать суммы всех выбранных строк ОСВ обеих сторон,
|
||||
абсолютное расхождение `elimination_amount`, процент расхождения к меньшей сумме, `exceeds_threshold` и отдельное
|
||||
`eliminated_amount`. При расхождении не более 5 % `eliminated_amount` SHALL равняться `min(amount_a, amount_b)`;
|
||||
при превышении порога, нулевой стороне или `elimination_mode = manual` SHALL равняться нулю. Упаковка строк
|
||||
не меняет этот итог.
|
||||
#### 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`, расхождение округляется до 3,87 %.
|
||||
- **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`, независимо от результата упаковки.
|
||||
- **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
|
||||
|
||||
### 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` — одна из
|
||||
сумм нулевая; `DISCREPANCY` — `exceeds_threshold`; иначе `AUTO`. Порядок — `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` нет
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: Правила элиминации версии
|
||||
**Reason**: с DBML v5.23 `elimination_rules_version` задан у каждой версии консолидации; ветка «версия правил не
|
||||
зафиксирована — последние открытые правила» больше не существует.
|
||||
**Migration**: требование «Правила элиминации зафиксированной версии»; клиенту `is_version_fixed` всегда `true`.
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Версия DBML зафиксирована в коде
|
||||
Пакет моделей SHALL объявлять константу версии DBML (`DBML_VERSION = "5.23"`); скрипт сверки SHALL сравнивать её с версией в шапке файла
|
||||
DBML и падать при расхождении — смена модели без осознанного обновления ORM невозможна.
|
||||
|
||||
#### Scenario: Новая версия DBML без обновления ORM
|
||||
- **WHEN** в `docs/model/` появляется `data-model-v5.24.dbml`, а константа в ORM — `5.23`
|
||||
- **THEN** сверка против нового файла падает с сообщением о версии до сравнения таблиц
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Миграция DBML v5.23 — версия правил элиминации обязательна
|
||||
Миграция `0010_dbml_v5_23` после `0009_dbml_v5_21` SHALL заполнить пустой `consolidation_version.elimination_rules_version`
|
||||
номером версии правил, действовавшей на московскую дату `created_at` версии (пустой `created_at` — текущая дата), а если
|
||||
такой нет — последней открытой версии (`valid_to IS NULL`), затем сделать колонку NOT NULL. Если после заполнения
|
||||
остаются пустые значения (правил нет вовсе), миграция SHALL завершиться ошибкой с числом таких версий до изменения схемы.
|
||||
Откат SHALL снимать только NOT NULL, значения сохраняются. v5.22 структуру БД не меняла, отдельной миграции у неё нет.
|
||||
|
||||
#### Scenario: Заполнение по дате создания
|
||||
- **WHEN** правила v1 действуют до 30.06.2026, v2 — с 01.07.2026, версии созданы 01.03.2026, 30.06.2026 21:30 UTC и без даты
|
||||
- **THEN** после `upgrade head` у них 1, 2 и 2; колонка NOT NULL; `downgrade 0009_dbml_v5_21` снимает NOT NULL, значения остаются
|
||||
|
||||
#### Scenario: Версии без правил
|
||||
- **WHEN** в БД есть версия консолидации и нет ни одного правила элиминации
|
||||
- **THEN** `upgrade head` падает с ошибкой «миграция v5.23 невозможна…», ревизия остаётся `0009_dbml_v5_21`
|
||||
@ -1,7 +1,12 @@
|
||||
## Реализация
|
||||
|
||||
- [x] Добавить чистый расчёт общего итога и тесты порога, ручного режима и нулевой стороны.
|
||||
- [x] Проверить реальные строки правила № 6, точную пару и оставшиеся без пары по golden ОСВ.
|
||||
- [x] Проверить 42 млн по правилу № 4, ничью упаковки, обратную пару и фиксацию версии правил.
|
||||
- [x] Дополнить основной OpenSpec и гайд фронта.
|
||||
- [ ] Запустить `make check-full` и заархивировать change.
|
||||
- [x] Чистый расчёт итога тройки и тесты порога, ручного режима и нулевой стороны.
|
||||
- [x] Реальные строки правила № 6 (точная пара и строки без пары) по golden ОСВ; 42 млн по правилу № 4; ничья упаковки;
|
||||
обратная пара.
|
||||
- [x] Фиксация версии правил при создании версии; DBML v5.23, миграция `0010_dbml_v5_23` и её тесты; 409
|
||||
`ELIMINATION_RULES_MISSING`.
|
||||
- [x] Список троек версии `GET …/elimination-totals` и тест на тех же данных, что и тройка.
|
||||
- [x] OpenSpec-дельта, README, `05-domain-rules.md` §5 и гайд фронта (arm-ka-docs #164).
|
||||
- [ ] Суммы сторон со знаком, `NEGATIVE_AMOUNT`, сортировка групп по проценту, `deviation_amount` у группы и тройки, суммы
|
||||
с двумя знаками, правило с `measure ≠ closing` — 422 `INVALID_RULE` (раунд ревью 1, автор).
|
||||
- [ ] `make check-full` и архив change.
|
||||
|
||||
@ -15,13 +15,7 @@ Q3 → 01.10.year, Q4 → 01.01.year+1; ASSUMPTION(OQ-40)), `consolidation_versi
|
||||
`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 автоматически записать в `consolidation_version.elimination_rules_version` номер
|
||||
самой актуальной версии правил: максимальный `version_no` среди правил, действующих на московскую календарную дату
|
||||
создания версии (`valid_from <= дата` и `valid_to IS NULL` либо `valid_to >= дата`; OQ-174, docs PR #146).
|
||||
Пользователь не выбирает версию правил. Отчётные год, квартал и `reporting_date` не влияют на определение номера.
|
||||
Если на дату создания нет действующих правил, поле остаётся NULL.
|
||||
Периметр SHALL быть: без
|
||||
`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` чужих в порядке
|
||||
@ -39,10 +33,6 @@ SHALL оставаться NULL (фиксируется при верифика
|
||||
- **THEN** 201; версия №1 `INITIATED`, `reporting_date = 2024-10-01`, два `consolidation_member`, история с одной записью,
|
||||
`audit_event(CONSOLIDATION_CREATE)` с `consolidation_version_id`; активной блокировки нет
|
||||
|
||||
#### Scenario: Правила действуют на дату создания версии
|
||||
- **WHEN** в день создания консолидации за 2024 год действует версия правил 2, а версия 1 действовала в отчётном периоде
|
||||
- **THEN** `consolidation_version.elimination_rules_version = 2`; номер не меняется при последующей публикации правил
|
||||
|
||||
#### Scenario: Выбранный периметр шире ГСК
|
||||
- **WHEN** в ГК три ЮЛ, в ГСК два из них, создаётся консолидация с `legal_entity_ids` из всех трёх ЮЛ ГК
|
||||
- **THEN** 201; в `members` три ЮЛ; `after_value.members` события `CONSOLIDATION_CREATE` — три ЮЛ; состав ГСК не изменился
|
||||
@ -261,56 +251,6 @@ SHALL возвращать объект `{version_no, is_version_fixed, rules}`.
|
||||
- **WHEN** открыты правила с `version_no = 1` и `version_no = 2`, в том числе с одинаковым `rule_no`
|
||||
- **THEN** ответ содержит только правила версии 2 и `version_no = 2`
|
||||
|
||||
### Requirement: Сопоставленные строки сводной ОСВ по правилу и паре компаний
|
||||
`GET /api/v1/consolidations/versions/{version_id}/elimination-pairs` SHALL требовать `CONSOLIDATION_VIEW` и проверять
|
||||
видимость версии по ГК. Запрос SHALL принимать обязательные query-параметры `rule_id`, `company_a_id`, `company_b_id`.
|
||||
Компании SHALL быть разными участниками периметра версии, а правило — пресетом ББ её версии правил. Для этой тройки
|
||||
SHALL возвращаться только её результат; строки других правил и направленных пар компаний в расчёт не входят.
|
||||
Отбор счетов, раскрытие счетов-предков и их исключений SHALL совпадать с расчётом шахматки PRESET. Ответ SHALL
|
||||
содержать `version_no`, `rule_id`, `rule_no`, `company_a_id`, `company_b_id`, `matched[]` (точные группы),
|
||||
`with_discrepancy[]` (группы с допустимым ненулевым расхождением) и `unpaired[]` (строки без группы), без полного
|
||||
перечня допустимых пар счетов. На уровне тройки ответ SHALL содержать `amount_a`, `amount_b` (суммы всех выбранных
|
||||
строк, включая оставшиеся без пары), `elimination_amount` (абсолютное расхождение), `deviation_percent` (от меньшей
|
||||
суммы, `null` при нуле), `eliminated_amount` (сумма фактической элиминации) и `exceeds_threshold`.
|
||||
При расхождении не более 5 % `eliminated_amount` SHALL равняться `min(amount_a, amount_b)`; при превышении порога,
|
||||
нулевой стороне или `elimination_mode = manual` SHALL равняться нулю. Упаковка строк SHALL не менять этот итог.
|
||||
Каждая сопоставленная группа SHALL содержать `rule_id`, `company_a_id`,
|
||||
`company_b_id`, `amount_a`, `amount_b`, `elimination_amount`, `deviation_percent` и исходные `osv_entry_ids_a/b`.
|
||||
Суммы `amount_a/b` SHALL рассчитываться по ОСВ; `elimination_amount` SHALL равняться
|
||||
`abs(amount_a - amount_b)`.
|
||||
Невидимая и несуществующая версия или отсутствующее правило SHALL отвечать 404 `NOT_FOUND`;
|
||||
недопустимая пара компаний — 422 `INVALID_COMPANY_PAIR`. Пары и группы не сохраняются.
|
||||
|
||||
Строка сводной ОСВ неделима, даже если содержит несколько `entry_ids`. Внутри выбранной тройки строки с распознанным
|
||||
встречным ЮЛ и допустимыми счетами группируются как 1:1, 1:N или N:1 при N ≤ 5; N:M не допускается. Комбинации
|
||||
строятся из всех строк выбранных счетов соответствующей стороны правила, без дополнительного требования совпадения
|
||||
конкретных счетов внутри группы.
|
||||
Сравниваются модули остатков по соответствующей стороне. Нулевые строки остаются в `unpaired` с причиной
|
||||
`ZERO_AMOUNT`. Сначала выбираются группы с наименьшим общим числом строк: 1:1, затем 1:2 и 2:1, и так до 1:5 и 5:1.
|
||||
Внутри размера сначала точные, затем с расхождением не более 5 % относительно меньшей из двух сумм, по возрастанию
|
||||
расхождения. Строка входит не более чем в одну группу данного правила. Если группы одного размера с одинаковым
|
||||
расхождением пересекаются, все строки конкурирующих групп исключаются из автоматического поиска и возвращаются в
|
||||
`unpaired` с причиной `AMBIGUOUS` для ручного разбора. Остальные строки без группы возвращаются в `unpaired` с
|
||||
причиной `NO_MATCH` независимо от того, отсутствуют ли встречные суммы, превышен ли порог или подходящие строки уже
|
||||
заняты другой группой.
|
||||
Упаковка не меняет сумму элиминации.
|
||||
|
||||
#### Scenario: Исключённый счёт не входит в пары
|
||||
- **WHEN** правило включает родительский счёт 76 и исключает 76.07.5
|
||||
- **THEN** строки счёта 76.07.5 и его потомков не входят в сопоставленные группы и `unpaired`
|
||||
|
||||
#### Scenario: Две строки свода упакованы с одной встречной
|
||||
- **WHEN** по одному правилу есть дебетовые строки 40 и 60 и встречная кредитовая строка 100
|
||||
- **THEN** `matched[]` содержит пару с суммами 100 и 100, нулевым расхождением и исходными `osv_entry_ids_a/b`
|
||||
|
||||
#### Scenario: Пара с допустимым расхождением
|
||||
- **WHEN** группа имеет суммы 103 и 100 при пороге 5 %
|
||||
- **THEN** она входит в `with_discrepancy[]`, а не в `matched[]`
|
||||
|
||||
#### Scenario: Другая пара компаний не смешивается с выбранной
|
||||
- **WHEN** по одному правилу есть встречные строки для A→Б и A→В, а запрошены `company_a_id=A`, `company_b_id=Б`
|
||||
- **THEN** ответ содержит только строки A→Б и Б→A
|
||||
|
||||
### Requirement: Результирующая таблица ВГО
|
||||
`GET /api/v1/consolidations/versions/{version_id}/intercompany-matrix/result-table`
|
||||
(`CONSOLIDATION_VIEW`, policy) SHALL возвращать `{companies[], articles[]}`. Компании — все члены периметра
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user