Гудвилл: контракт в docs, README, OpenSpec под ревью

Тест контракта читает ../docs/dev/api-contract-goodwill.openapi.yaml (без docs — skip);
README: раздел гудвилла и коды INVALID_SHARE/DUPLICATE_ROW/MONEY_OUT_OF_RANGE;
OpenSpec goodwill-table: пакетное сохранение, доля два знака, итоги по ГО и УК,
единицы, Decisions made without discussion R-1…R-6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-06 18:41:20 +04:00
parent eeb0ce48da
commit 3bfcc2e2aa
6 changed files with 87 additions and 24 deletions

View File

@ -1057,7 +1057,10 @@ email без учёта регистра проверяет только сер
| `RESULT_ACCOUNTS_MISSING` | 422 | результирующая таблица ВГО: в плане счетов нет обязательных кодов статей | | `RESULT_ACCOUNTS_MISSING` | 422 | результирующая таблица ВГО: в плане счетов нет обязательных кодов статей |
| `INVALID_COMPANY_PAIR` | 422 | пары ВГО тройки: компании совпадают или не входят в периметр версии | | `INVALID_COMPANY_PAIR` | 422 | пары ВГО тройки: компании совпадают или не входят в периметр версии |
| `ACCOUNTS_NOT_ALLOWED` / `INVALID_MODE` | 422 | защита внутреннего вызова сервиса шахматки; маршруты ALL/MANUAL не дают вызвать эти ветки | | `ACCOUNTS_NOT_ALLOWED` / `INVALID_MODE` | 422 | защита внутреннего вызова сервиса шахматки; маршруты ALL/MANUAL не дают вызвать эти ветки |
| `COMMENT_REQUIRED` | 422 | `POST …/status` в `CANCELLED` без комментария (из одних пробелов — тоже) | | `COMMENT_REQUIRED` | 422 | `POST …/status` в `CANCELLED` без комментария (из одних пробелов — тоже); `PUT …/goodwill` — отмеченная строка без комментария (с ключом пары) |
| `INVALID_SHARE` | 422 | `PUT …/goodwill`: доля не 0–100, больше двух знаков после запятой или пустая у отмеченной строки (`head_legal_entity_id`, `subsidiary_legal_entity_id`) |
| `DUPLICATE_ROW` | 422 | `PUT …/goodwill`: пара дважды в одном пакете (ключ пары) |
| `MONEY_OUT_OF_RANGE` | 422 | `PUT …/goodwill`: сумма снимка строки не помещается в `numeric(18,2)` (ключ пары) |
| `VERSION_NOT_EDITABLE` | 409 | версия не «в работе» (правки) либо `CANCELLED` при «взять в работу» | | `VERSION_NOT_EDITABLE` | 409 | версия не «в работе» (правки) либо `CANCELLED` при «взять в работу» |
| `LOCK_REQUIRED` | 409 | правка версии без своей активной блокировки | | `LOCK_REQUIRED` | 409 | правка версии без своей активной блокировки |
| `LOCKED_BY_OTHER` | 409 | версию держит другой пользователь: из `POST …/lock` — `locked_by`, `locked_by_name`, `locked_at`; из правок (`ensure_version_editable`) — только `locked_by`, `locked_at`; гонка двух взятий (частичный UNIQUE) — то же 409 | | `LOCKED_BY_OTHER` | 409 | версию держит другой пользователь: из `POST …/lock` — `locked_by`, `locked_by_name`, `locked_at`; из правок (`ensure_version_editable`) — только `locked_by`, `locked_at`; гонка двух взятий (частичный UNIQUE) — то же 409 |
@ -1343,7 +1346,11 @@ Dockerfile только для локального compose
### Таблица гудвилла ### Таблица гудвилла
Бэкенд таблицы гудвилла версии: фактические вложения 58.01 между участниками периметра, построчные решения, Бэкенд таблицы гудвилла версии (§8 «Гудвилл», DOC-259025): пары ГО → ДО по фактическим вложениям 58.01 и потомков
переключатель и XLSX. `is_head_entity` не используется. Деньги в рублях, доля до четырёх знаков, нерассчитанные между участниками периметра (`is_head_entity` не используется), УК — 1310 ББ ДО на конец квартала (единицы — словарём
значения — `null`; исчезнувшие сохранённые пары остаются как «Нет вложений». `arm_statements`). `GET …/goodwill` — строки, итоги по ГО (`groups`) и по листу (`totals`); `PUT …/goodwill` — пакет
Контракт и ограничения этапа — [контракт API](goodwill/api-contract-goodwill.md). Фронт и построение/фиксация результата КФО — отдельные задачи. строк одной транзакцией (ошибка любой строки откатывает пакет и несёт её ключ, неизменённые строки не перезаписываются);
`PUT …/goodwill-settings` — переключатель `goodwill_enabled`; `GET …/goodwill/export?sign=` — xlsx. Деньги в рублях,
доля — до двух знаков, нерассчитанные значения — `null`; исчезнувшие сохранённые пары остаются как «Нет вложений».
Контракт — [`../docs/dev/api-contract-goodwill.md`](../docs/dev/api-contract-goodwill.md). Применение гудвилла в КФО —
AURORA-1546.

View File

@ -1,22 +1,29 @@
"""Локальный OpenAPI совпадает с четырьмя маршрутами приложения.""" """Контракт гудвилла в docs (`dev/api-contract-goodwill.openapi.yaml`) совпадает с маршрутами и схемами приложения."""
from decimal import Decimal
from pathlib import Path from pathlib import Path
import pytest
import yaml import yaml
from src.app import app from src.app import app
from src.schemas.goodwill import GoodwillRowIn from src.schemas.goodwill import GoodwillRowIn
from src.services.goodwill import validate_decision from src.services.goodwill import validate_decision
CONTRACT = Path(__file__).resolve().parents[3] / "docs/dev/api-contract-goodwill.openapi.yaml"
def test_contract_matches_application(): def test_contract_matches_application():
contract = yaml.safe_load( if not CONTRACT.exists():
(Path(__file__).resolve().parents[2] / "goodwill/api-contract-goodwill.openapi.yaml").read_text() pytest.skip("нет ../docs: контракт гудвилла не проверен")
) contract = yaml.safe_load(CONTRACT.read_text())
actual = app.openapi() actual = app.openapi()
paths = {key: value for key, value in actual["paths"].items() if "/goodwill" in key} paths = {key: value for key, value in actual["paths"].items() if "/goodwill" in key}
assert contract["paths"] == paths assert contract["paths"] == paths
for name, schema in contract["components"]["schemas"].items(): for name, schema in contract["components"]["schemas"].items():
assert schema == actual["components"]["schemas"][name] assert schema == actual["components"]["schemas"][name], name
data = GoodwillRowIn(ownership_share="80.1234", include_in_kfo=True, comment="Доля подтверждена")
assert str(validate_decision(data).ownership_share) == "80.1234"
def test_share_with_comma():
data = GoodwillRowIn(ownership_share="80,12", include_in_kfo=True, comment="Доля подтверждена")
assert validate_decision(data).ownership_share == Decimal("80.12")

View File

@ -14,7 +14,7 @@
### 1. Приоритет требований ### 1. Приоритет требований
Явные решения пользователя имеют приоритет над PDF, затем PDF, затем MD/OpenAPI. Четыре знака доли, построчное PUT и XLSX намеренно отличаются от PDF. Сохраняем четыре пути MD. is_head_entity полностью исключается. ASSUMPTION(OQ-10): источник УК — только ББ 1310; 1350 можно показывать справочно, но не включать в P. ASSUMPTION(OQ-37): вклад использует GOODWILL. Подтверждение другого источника УК или формата экспорта требует обновления этого change перед соответствующей реализацией. Явные решения пользователя имеют приоритет над PDF, затем PDF, затем MD/OpenAPI. XLSX вместо CSV намеренно отличается от PDF (по контракту). Доля — два знака, сохранение — пакетом (R-1, R-2). is_head_entity полностью исключается. ASSUMPTION(OQ-10): источник УК — только ББ 1310; 1350 можно показывать справочно, но не включать в P. ASSUMPTION(OQ-37): вклад использует GOODWILL. Подтверждение другого источника УК или формата экспорта требует обновления этого change перед соответствующей реализацией.
### 2. Слои и источники ### 2. Слои и источники
@ -32,13 +32,13 @@ has_investments — вычисляемый DTO-флаг, не колонка. П
### 4. Деньги и DTO ### 4. Деньги и DTO
Decimal, P.quantize(0.01, ROUND_HALF_UP), затем G=ФВ−P. Ограничение numeric(18,2) проверять до записи, переполнение отклонять 422 MONEY_OUT_OF_RANGE без частичного сохранения; float не использовать. Значения в API — рубли с двумя знаками, ownership_share — проценты с четырьмя. Максимум четыре дробных знака во входе, без автоматического округления; запятая и точка допустимы по PDF §5.2; null допускается для снятой галочки. P/G рассчитываются только у действующей отмеченной строки при наличии доли и капитала (PDF §6); итоги P/G учитывают только выбранные действующие строки. Для первоначальной строки include_in_kfo=false, доля/comment/updated_* = null. Decimal, P.quantize(0.01, ROUND_HALF_UP), затем G=ФВ−P. Ограничение numeric(18,2) проверять до записи, переполнение отклонять 422 MONEY_OUT_OF_RANGE без частичного сохранения; float не использовать. Значения в API — рубли с двумя знаками, ownership_share — проценты с двумя (R-1). Максимум два дробных знака во входе, без автоматического округления; запятая и точка допустимы по PDF §5.2; null допускается для снятой галочки. P/G рассчитываются только у действующей отмеченной строки при наличии доли и капитала (PDF §6); итоги P/G учитывают только выбранные действующие строки. Для первоначальной строки include_in_kfo=false, доля/comment/updated_* = null.
GoodwillTableOut и GoodwillRowOut повторяют MD с уточнениями spec: has_investments, warnings[] из {code, message}; charter_capital_amount и additional_capital_amount тоже nullable, чтобы отсутствие ББ не маскировать нулём. totals — поля MD; в этом этапе без отдельного API групповых итогов: они выводимы из всех строк. При goodwill_enabled=false потенциальные G видны, goodwill_in_kfo_amount=0.00. GoodwillTableOut и GoodwillRowOut повторяют MD с уточнениями spec: has_investments, warnings[] из {code, message}; charter_capital_amount и additional_capital_amount тоже nullable, чтобы отсутствие ББ не маскировать нулём. totals — поля MD; в этом этапе без отдельного API групповых итогов: они выводимы из всех строк. При goodwill_enabled=false потенциальные G видны, goodwill_in_kfo_amount=0.00.
### 5. Транзакции и безопасность ### 5. Транзакции и безопасность
Маршруты гудвилла используют зависимости по образцу API корректировок. Service использует текущие policy и lock_editable_version. PUT строки под блокировкой версии повторно загружает актуальные источники, проверяет пару и ввод, upsert существующей записи, записывает GOODWILL_EDIT before/after в той же транзакции. Вычисляемые суммы из тела не принимаются. PUT настроек не трогает строки; GOODWILL_EDIT имеет object_type=consolidation_version и changed_field=goodwill_enabled, строки — object_type=goodwill. Новые enum не нужны. Отдельная оптимистическая ревизия не добавляется: существующая блокировка версии сериализует операции, последнее сохранение владельца побеждает. Маршруты гудвилла используют зависимости по образцу API корректировок. Service использует текущие policy и lock_editable_version. PUT строки под блокировкой версии повторно загружает актуальные источники, проверяет пару и ввод, upsert существующей записи, записывает GOODWILL_EDIT before/after в той же транзакции. Вычисляемые суммы из тела не принимаются. PUT настроек не трогает строки; GOODWILL_EDIT имеет object_type=consolidation_version и changed_field=goodwill_enabled, строки — object_type=goodwill. Новые enum не нужны. PUT …/goodwill принимает набор строк и сохраняет его одной транзакцией (R-2): сначала проверяются все строки, потом пишутся изменённые. Отдельная оптимистическая ревизия не добавляется: существующая блокировка версии сериализует операции, последнее сохранение владельца побеждает.
### 6. Граница КФО ### 6. Граница КФО
@ -61,3 +61,27 @@ GoodwillTableOut и GoodwillRowOut повторяют MD с уточнениям
Миграция БД не требуется: существующие поля и enum достаточны. Выкладка добавляет маршруты и обновлённый контракт; перед подключением фронта проверить nullable поля. Откат кода оставляет goodwill и аудит в совместимой схеме. Пока фронт отдельно, существующие страницы не изменяются. Миграция БД не требуется: существующие поля и enum достаточны. Выкладка добавляет маршруты и обновлённый контракт; перед подключением фронта проверить nullable поля. Откат кода оставляет goodwill и аудит в совместимой схеме. Пока фронт отдельно, существующие страницы не изменяются.
По решению пользователя ФВ пары — сумма только заполненных closing_debit; пустые значения остальных строк не блокируют расчёт P/G и сохранение ручного ввода. По решению пользователя ФВ пары — сумма только заполненных closing_debit; пустые значения остальных строк не блокируют расчёт P/G и сохранение ручного ввода.
## Decisions made without discussion
Ревью arm-core #222 (06.11.2026, владелец: «давай всё сделаем сами»; варианты предложены владельцу, ответа по ним нет —
взяты рекомендованные).
- **R-1 (2026-11-06). Доля владения — не больше двух знаков после запятой.** Обоснование: DOC-259025 §4, §5.2, §9
(«максимум два знака»), фронт вводит два. DBML `numeric(7,4)` не меняется — хранение шире ввода. Цена ошибки: если
методологам нужны 4 знака — расширить регулярку `validate_decision` и сериализацию `Share`, данные не теряются.
- **R-2 (2026-11-06). Построчный `PUT …/goodwill/{head}/{sub}` заменён пакетным `PUT …/goodwill`.** Обоснование:
DOC-259025 §5.1 п. 4 — набор сохраняется целиком или не сохраняется; фронт слал строки `Promise.all` и мог сохранить
часть. Цена ошибки: если понадобится построчное — пакет из одной строки уже его покрывает.
- **R-3 (2026-11-06). Подготовка вклада в КФО (`contributions`, `load_for_kfo`, фильтры общего свода ОСВ) удалена,
перенесена в AURORA-1546.** Обоснование: код не вызывался, а менял общие `load_osv_rows`/`build_with_members`.
Цена ошибки: в 1546 написать заново по DOC-259025 §7, §10 (было ~150 строк с тестами, история — коммит 94895b6).
- **R-4 (2026-11-06). Итоги «Всего» по ГО (`groups`) и итог УК считает бэк.** Обоснование: агрегаты отдаёт бэк
(правило владельца), фронт считал их через `Number`; DOC-259025 §6 требует итоги ФВ и УК. Цена ошибки: лишние поля
ответа, вреда нет.
- **R-5 (2026-11-06). Единицы ББ — словарём единиц `arm_statements` (`StatementsEngine.canonical_units`).**
Обоснование: единицы правятся аналитиком свободным текстом, точное сравнение «тыс. руб.» теряло УК. Цена ошибки:
незнакомая подпись — УК `null` с предупреждением, как раньше.
- **R-6 (2026-11-06). Пометка результата КФО устаревшим при смене переключателя или строк не делается — в AURORA-1546.**
Обоснование: результата КФО (`kfo_result`) ещё не строится, помечать нечего. Цена ошибки: в 1546 добавить сброс
в `save_rows` и `settings`.

View File

@ -6,10 +6,10 @@
## What Changes ## What Changes
- Срез 6: бэкенд таблицы, построчное сохранение, переключатель версии и экспорт XLSX. - Срез 6: бэкенд таблицы, пакетное сохранение, переключатель версии и экспорт XLSX.
- ГО определяется по фактическим вложениям 58.01 и дочерних счетов в другие компании периметра; `is_head_entity` не используется. - ГО определяется по фактическим вложениям 58.01 и дочерних счетов в другие компании периметра; `is_head_entity` не используется.
- Найденные пары объединяются с сохранёнными `goodwill`; исчезнувшие остаются с вычисляемым `has_investments=false`, без участия в итогах. - Найденные пары объединяются с сохранёнными `goodwill`; исчезнувшие остаются с вычисляемым `has_investments=false`, без участия в итогах.
- Доля 0–100%, до четырёх знаков; нерассчитанные P/G допускают null. Сохранение одной строки атомарно. - Доля 0–100%, до двух знаков; нерассчитанные P/G допускают null. Пакет строк сохраняется одной транзакцией; итоги по ГО и УК отдаёт бэк.
- Расчёт по §8 domain-rules с уточнениями PDF §§3–10 и решениями пользователя: ASSUMPTION(OQ-10) — УК только из 1310 ББ, 1350 не входит; ASSUMPTION(OQ-37) — подготовленный вклад имеет технический код GOODWILL. - Расчёт по §8 domain-rules с уточнениями PDF §§3–10 и решениями пользователя: ASSUMPTION(OQ-10) — УК только из 1310 ББ, 1350 не входит; ASSUMPTION(OQ-37) — подготовленный вклад имеет технический код GOODWILL.
- Подготовка вклада в КФО и исключение ББ-2 — не в этом change, перенесено в AURORA-1546 (R-3). - Подготовка вклада в КФО и исключение ББ-2 — не в этом change, перенесено в AURORA-1546 (R-3).
- Уточнения PDF, отсутствующие в §8 (неактивные пары, выборка по дереву, итоги, исключение ББ-2), требуют отдельной синхронизации в docs; при реализации не подменять ими молча старую редакцию правил. - Уточнения PDF, отсутствующие в §8 (неактивные пары, выборка по дереву, итоги, исключение ББ-2), требуют отдельной синхронизации в docs; при реализации не подменять ими молча старую редакцию правил.
@ -28,7 +28,7 @@
Читаются таблицы DBML v5.24: consolidation, consolidation_version, consolidation_member, consolidation_document, document, osv_entry, chart_of_account, financial_statement_meta/line/column/value, legal_entity, app_user, elimination_rule. Изменяются goodwill, consolidation_version.goodwill_enabled и audit_event. Для аудита использовать существующий GOODWILL_EDIT, для выгрузки EXPORT; новых enum и колонок нет. `has_investments`, предупреждения и вклад КФО — вычисляемые DTO. Читаются таблицы DBML v5.24: consolidation, consolidation_version, consolidation_member, consolidation_document, document, osv_entry, chart_of_account, financial_statement_meta/line/column/value, legal_entity, app_user, elimination_rule. Изменяются goodwill, consolidation_version.goodwill_enabled и audit_event. Для аудита использовать существующий GOODWILL_EDIT, для выгрузки EXPORT; новых enum и колонок нет. `has_investments`, предупреждения и вклад КФО — вычисляемые DTO.
Затрагиваются domain/, repositories/, schemas/, services/ и endpoints/consolidations; четыре маршрута из MD. Изменение чернового контракта: новые признаки состояния и nullable P/G, обновлённая семантика пар и итогов. Используются существующие guard, Decimal и openpyxl. Затрагиваются domain/, repositories/, schemas/, services/ и endpoints/consolidations; маршруты GET/PUT …/goodwill, PUT …/goodwill-settings, GET …/goodwill/export; контракт — ../docs/dev/api-contract-goodwill.md. Изменение чернового контракта: новые признаки состояния и nullable P/G, обновлённая семантика пар и итогов. Используются существующие guard, Decimal и openpyxl.
## Non-goals ## Non-goals

View File

@ -25,36 +25,52 @@
- **THEN** строка остаётся с has_investments=false, прежними долей, комментарием и галочкой, но без вклада в итоги; повторное появление пары восстанавливает расчёт по текущим источникам - **THEN** строка остаётся с has_investments=false, прежними долей, комментарием и галочкой, но без вклада в итоги; повторное появление пары восстанавливает расчёт по текущим источникам
### Requirement: Источник капитала и расчёт ### Requirement: Источник капитала и расчёт
Система SHALL использовать 1310 ББ ДО на конец отчётного периода в рублях: P=round(УК×доля/100,2), G=ФВ−P. Дубликаты 1310 обрабатываются агрегированием формы. Ноль допустим; отсутствие доли или достоверного УК даёт null P/G. 1350 не участвует: ASSUMPTION(OQ-10). Система SHALL использовать 1310 ББ ДО на конец отчётного периода в рублях; единицы формы SHALL распознаваться словарём единиц arm_statements, нераспознанные единицы дают отсутствующий УК. P=round(УК×доля/100,2), G=ФВ−P. Дубликаты 1310 обрабатываются агрегированием формы. Ноль допустим; отсутствие доли или достоверного УК даёт null P/G. 1350 не участвует: ASSUMPTION(OQ-10).
#### Scenario: Ноль отличается от отсутствия расчёта #### Scenario: Ноль отличается от отсутствия расчёта
- **WHEN** из привязанного реального ББ получен УК U, доля ещё не введена - **WHEN** из привязанного реального ББ получен УК U, доля ещё не введена
- **THEN** P и G равны null; при доле 0 P равен 0.00 и G равен ФВ; отсутствующая 1310 не подменяется нулём - **THEN** P и G равны null; при доле 0 P равен 0.00 и G равен ФВ; отсутствующая 1310 не подменяется нулём
#### Scenario: Единицы, исправленные вручную
- **WHEN** у формы ББ единицы «тыс.руб.» или «в тысячах рублей»
- **THEN** УК переводится в рубли умножением на 1000; единицы «штук» дают УК null и предупреждение CAPITAL_SOURCE_MISSING
#### Scenario: Проверка отчётной колонки #### Scenario: Проверка отчётной колонки
- **WHEN** в привязанном ББ есть несколько сравнительных колонок и единицы «тыс. руб.» - **WHEN** в привязанном ББ есть несколько сравнительных колонок и единицы «тыс. руб.»
- **THEN** УК берётся по дате конца отчётного периода, а не только is_current; сумма переводится в рубли до расчёта - **THEN** УК берётся по дате конца отчётного периода, а не только is_current; сумма переводится в рубли до расчёта
### Requirement: Получение таблицы ### Requirement: Получение таблицы
GET /api/v1/consolidations/versions/{version_id}/goodwill SHALL возвращать version_id, goodwill_enabled, editable, read_only_reason, rows, totals. Строки отсортированы по именам ГО/ДО и ID; содержат поля MD-контракта, has_investments и warnings. Деньги — строки с двумя знаками в рублях или null, доли — строки с четырьмя. GET /api/v1/consolidations/versions/{version_id}/goodwill SHALL возвращать version_id, goodwill_enabled, editable, read_only_reason, rows, groups, totals. Строки отсортированы по именам ГО/ДО и ID; содержат поля MD-контракта, has_investments и warnings. Деньги — строки с двумя знаками в рублях или null, доли — строки с двумя знаками. groups — итог «Всего» по каждой ГО в порядке строк, totals — итог по листу.
#### Scenario: Пустая таблица #### Scenario: Пустая таблица
- **WHEN** нет подходящих вложений и сохранённых пар - **WHEN** нет подходящих вложений и сохранённых пар
- **THEN** rows пуст, денежные итоги 0.00; запрос не создаёт goodwill - **THEN** rows пуст, денежные итоги 0.00; запрос не создаёт goodwill
### Requirement: Итоги и предупреждения ### Requirement: Итоги и предупреждения
Система SHALL суммировать ФВ действующих строк независимо от выбора; P/G — только действующих выбранных рассчитанных строк. goodwill_in_kfo_amount включает только положительные применимые G и равен нулю при выключенном goodwill_enabled. Сумма заполненных долей действующих ГО в одной ДО свыше 100% даёт предупреждение, не ошибку. Система SHALL суммировать ФВ и УК действующих строк независимо от выбора; P/G — только действующих выбранных рассчитанных строк. goodwill_in_kfo_amount включает только положительные применимые G и равен нулю при выключенном goodwill_enabled. Сумма заполненных долей действующих ГО в одной ДО свыше 100% даёт предупреждение, не ошибку.
#### Scenario: Несколько ГО одной ДО #### Scenario: Несколько ГО одной ДО
- **WHEN** у действующих пар одной ДО заполнены доли 70% и 40% - **WHEN** у действующих пар одной ДО заполнены доли 70% и 40%
- **THEN** строки содержат предупреждение OWNERSHIP_TOTAL_EXCEEDED, сохранение не блокируется; проценты по разным ДО не складываются в общий процент - **THEN** строки содержат предупреждение OWNERSHIP_TOTAL_EXCEEDED, сохранение не блокируется; проценты по разным ДО не складываются в общий процент
### Requirement: Построчное сохранение #### Scenario: Пример DOC-259025 §11.1
PUT …/goodwill/{head_id}/{subsidiary_id} SHALL атомарно сохранять ownership_share, include_in_kfo, comment и расчётные суммы только одной пары. Доля допускает null только при снятом выборе, иначе 0–100 с точностью до четырёх знаков; превышение точности не округляется. Для выбранной действующей строки обязателен непустой комментарий до 1000 символов. Ошибки — INVALID_SHARE и COMMENT_REQUIRED (422). - **WHEN** у ГО «Альфа» отмечены Бета (ФВ 180 000, УК 100 000, 80 %) и Гамма (ФВ 50 000, УК 100 000, 80 %), Дельта (ФВ 60 000, УК 60 000) без доли и не отмечена
- **THEN** «Всего» и «Итого по листу»: ФВ 290 000, УК 260 000, P 160 000, G 70 000, в КФО 100 000 (при выключенном переключателе — 0)
### Requirement: Пакетное сохранение
PUT …/goodwill SHALL принимать набор строк {head_legal_entity_id, subsidiary_legal_entity_id, ownership_share, include_in_kfo, comment} и сохранять его в одной транзакции: все строки проверяются до записи, ошибка любой строки откатывает весь пакет и несёт ключ пары. Доля допускает null только при снятом выборе, иначе 0–100, не больше двух знаков после запятой, запятая или точка; превышение точности не округляется. Для выбранной действующей строки обязателен непустой комментарий до 1000 символов. Ошибки — 404 NOT_FOUND (пары нет в таблице), 422 INVALID_SHARE, COMMENT_REQUIRED, MONEY_OUT_OF_RANGE, DUPLICATE_ROW. Строка, ввод которой не изменился, MUST NOT перезаписываться и писать аудит. Ответ — пересчитанная таблица.
#### Scenario: Ошибка одной строки пакета
- **WHEN** пакет содержит корректную строку и строку отмеченной пары без комментария
- **THEN** ответ 422 COMMENT_REQUIRED с head_legal_entity_id и subsidiary_legal_entity_id этой строки; ни одна запись goodwill и ни одно событие GOODWILL_EDIT не созданы
#### Scenario: Пакет без изменений
- **WHEN** повторно отправлена сохранённая строка с той же долей («80,5» после «80.50»), галочкой и комментарием
- **THEN** updated_at строки прежний, событий GOODWILL_EDIT не добавилось
#### Scenario: Снятие выбора #### Scenario: Снятие выбора
- **WHEN** сохраняется действующая строка с include_in_kfo=false и прежними долей и комментарием - **WHEN** сохраняется действующая строка с include_in_kfo=false и прежними долей и комментарием
- **THEN** ввод сохраняется, строка исключается из итогов P/G и вклада КФО; ответ содержит пересчитанную строку - **THEN** ввод сохраняется, строка исключается из итогов P/G и вклада КФО; ответ содержит пересчитанную таблицу
#### Scenario: Повторное сохранение и неактивная строка #### Scenario: Повторное сохранение и неактивная строка
- **WHEN** повторно сохраняется существующая пара - **WHEN** повторно сохраняется существующая пара

View File

@ -53,3 +53,12 @@
## 8. Экспорт по экранной таблице ## 8. Экспорт по экранной таблице
- [x] 8.1 arm-core: привести XLSX к составу и порядку восьми колонок фронта со стандартным оформлением. Сохранить прежний контракт ручки и вызов экспорта на фронте. Проверить интеграционные сценарии выгрузки. - [x] 8.1 arm-core: привести XLSX к составу и порядку восьми колонок фронта со стандартным оформлением. Сохранить прежний контракт ручки и вызов экспорта на фронте. Проверить интеграционные сценарии выгрузки.
## 9. Доводка по ревью (06.11.2026, план docs/superpowers/plans/2026-11-06-goodwill-review-back.md)
- [x] 9.1 arm-core: заготовка вклада КФО удалена, общий свод ОСВ возвращён к main (R-3).
- [x] 9.2 arm-core: доля — два знака, `participation()` в домене, итоги по ГО (`groups`) и УК (R-1, R-4).
- [x] 9.3 arm-core: пакетный `PUT …/goodwill` одной транзакцией вместо построчного (R-2).
- [x] 9.4 arm-core: единицы ББ по словарю arm_statements (R-5).
- [x] 9.5 docs (#204) и arm-core: контракт `../docs/dev/api-contract-goodwill.*`, §8 правил, OQ-10/OQ-37, README.