diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index aa147a2..be21d13 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -1198,7 +1198,7 @@ OQ — менять по ответу. | VERIFICATION_RF | CONSOLIDATION_RF | РКА РФ | вернул (комментарий обязателен) | | CONSOLIDATION_GO | CONSOLIDATION_RF | КА ГО | вернул на доработку (комментарий) | | CONSOLIDATION_GO | VERIFICATION_GO | КА ГО | если инициатор — КА ГО, верифицирует РКА ГО [открыто] | -| CONSOLIDATION_GO / VERIFICATION_GO | VERIFIED | КА ГО / РКА ГО | внесены № и дата решения УОБ; версия read-only; пишется `ex_snapshot` | +| CONSOLIDATION_GO / VERIFICATION_GO | VERIFIED | КА ГО / РКА ГО | внесены № и дата решения УОБ; версия read-only; пишется `ex_snapshot` (§14) | | любой нефинальный | INITIATED | КА | нужен другой периметр/сценарий/комплект | | любой нефинальный | CANCELLED | КА | аннулировано | | VERIFIED | — | — | только «новая версия» (`previous_version_id`) | @@ -1696,9 +1696,6 @@ OQ-92 (пустые «Организация»/«Группа компаний» Все мутации пишут `audit_event` (`GROUP_CHANGE`, `ASSIGNMENT_CHANGE`, `MEMBERSHIP_CHANGE`, `LEGAL_ENTITY_CHANGE` — v5.14, `DIRECTORY_IMPORT`) с `before/after`. ---- - - --- ## §14. Срез ЕХ при верификации (DOC-274687, DOC-274686, DOC-274685; ЕХ ТЗ 5.2, 6.2–6.4; AURORA-1560 — запись, AURORA-2001 — чтение и архив, AURORA-2002 — переход; ещё не реализовано) @@ -1731,8 +1728,15 @@ OQ-92 (пустые «Организация»/«Группа компаний» сопоставления свода, строки ОСВ агрегатов, сводка корректировок КФО) — срез выгружается на лету из payload и должен совпадать с выгрузкой рабочей версии. Шахматка — режим «все счета», пресет каждого правила ББ версии правил и результирующая таблица; ручной выбор счетов не замораживается. Корректировки — только применённые (`APPLIED`; - `REVOKED` нет, `DRAFT` не пишется). Карточка версии (`version_card`) — участники маршрута с ФИО, решение УОБ, причины - отклонений, состав с ИНН: версия КФО удаляется раньше среза. + `REVOKED` нет, `DRAFT` не пишется; агрегат с неприменённой корректировкой — с `correction = null`). Сводная ОСВ — + весь свод без фильтров и страниц, в порядке API. Карточка версии (`version_card`) — участники маршрута с ФИО, + решение УОБ, причины отклонений, состав с ИНН: версия КФО удаляется раньше среза. Пользователи в DTO — как в API + (`Ref {id, name}`, ФИО на дату верификации); в карточке — `UserRef {user_id, full_name}`. + **Ошибка экрана при сборке.** Пресет правила, для которого экран отвечает 422 (`INVALID_RULE`, + `ACCOUNTS_REQUIRED`), в `chess.presets` не пишется; любая другая ошибка сборки обязательного элемента (в том числе + `RESULT_ACCOUNTS_MISSING` результирующей таблицы) — сбой записи среза (п. 9). + **Проверка.** Писатель проверяет каждый payload схемой `ex-payload-v1` с проверкой форматов (`uuid`, `date`, + `date-time`); ядро держит копию схемы у себя, как схемы контракта распознавания (копия + проверка идентичности). 5. **Не входят:** строки форм без кода (документ — целиком, ссылкой; решение 17.10.2026), документы, не привязанные к версии, и документы заменённых файлов, черновики корректировок. 6. **Самодостаточность.** Ссылки допускаются только на то, что живёт не меньше среза: `document`, `legal_entity`, diff --git a/contracts/ex-payload-v1.schema.json b/contracts/ex-payload-v1.schema.json index 1aae387..1573762 100644 --- a/contracts/ex-payload-v1.schema.json +++ b/contracts/ex-payload-v1.schema.json @@ -444,7 +444,7 @@ "string", "null" ], - "pattern": "^-?\\d{1,5}\\.\\d{2}$", + "pattern": "^-?\\d+\\.\\d{2}$", "description": "deviation_total / base_amount × 100; null при нулевой базе" }, "exceeded": { @@ -687,154 +687,196 @@ ] }, "KfoBalanceData": { - "type": "object", - "properties": { - "version_id": { - "type": "string", - "format": "uuid" - }, - "computed_at": { - "type": "string", - "format": "date-time" - }, - "is_frozen": { - "type": "boolean" - }, - "units": { - "type": "string", - "const": "thousand_rub" - }, - "companies": { - "type": "array", - "items": { - "$ref": "#/$defs/KfoCompanyOut" - } - }, - "requisites": { - "$ref": "#/$defs/KfoRequisitesOut" - }, - "summary": { - "$ref": "#/$defs/KfoSummaryOut" - }, - "justification_required": { - "type": "boolean" - }, - "threshold_justification": { - "type": [ - "string", - "null" + "allOf": [ + { + "type": "object", + "properties": { + "version_id": { + "type": "string", + "format": "uuid" + }, + "computed_at": { + "type": "string", + "format": "date-time" + }, + "is_frozen": { + "type": "boolean" + }, + "units": { + "type": "string", + "const": "thousand_rub" + }, + "companies": { + "type": "array", + "items": { + "$ref": "#/$defs/KfoCompanyOut" + } + }, + "requisites": { + "$ref": "#/$defs/KfoRequisitesOut" + }, + "summary": { + "$ref": "#/$defs/KfoSummaryOut" + }, + "justification_required": { + "type": "boolean" + }, + "threshold_justification": { + "type": [ + "string", + "null" + ], + "maxLength": 4000 + }, + "goodwill": { + "$ref": "#/$defs/KfoGoodwillOut" + }, + "warnings": { + "type": "array", + "items": { + "$ref": "#/$defs/KfoWarningOut" + } + }, + "balance": { + "$ref": "#/$defs/KfoStatementOut" + } + }, + "required": [ + "version_id", + "computed_at", + "is_frozen", + "units", + "companies", + "requisites", + "summary", + "justification_required", + "threshold_justification", + "goodwill", + "warnings", + "balance" ], - "maxLength": 4000 - }, - "goodwill": { - "$ref": "#/$defs/KfoGoodwillOut" - }, - "warnings": { - "type": "array", - "items": { - "$ref": "#/$defs/KfoWarningOut" + "additionalProperties": true, + "description": "KfoResultOut без editable/read_only_reason, только отчёт ББ", + "not": { + "required": [ + "income" + ] } }, - "balance": { - "$ref": "#/$defs/KfoStatementOut" + { + "properties": { + "is_frozen": { + "const": true + }, + "companies": { + "minItems": 1 + }, + "requisites": { + "properties": { + "version_status": { + "const": "VERIFIED" + } + } + } + } } - }, - "required": [ - "version_id", - "computed_at", - "is_frozen", - "units", - "companies", - "requisites", - "summary", - "justification_required", - "threshold_justification", - "goodwill", - "warnings", - "balance" - ], - "additionalProperties": true, - "description": "KfoResultOut без editable/read_only_reason, только отчёт ББ", - "not": { - "required": [ - "income" - ] - } + ] }, "KfoIncomeData": { - "type": "object", - "properties": { - "version_id": { - "type": "string", - "format": "uuid" - }, - "computed_at": { - "type": "string", - "format": "date-time" - }, - "is_frozen": { - "type": "boolean" - }, - "units": { - "type": "string", - "const": "thousand_rub" - }, - "companies": { - "type": "array", - "items": { - "$ref": "#/$defs/KfoCompanyOut" - } - }, - "requisites": { - "$ref": "#/$defs/KfoRequisitesOut" - }, - "summary": { - "$ref": "#/$defs/KfoSummaryOut" - }, - "justification_required": { - "type": "boolean" - }, - "threshold_justification": { - "type": [ - "string", - "null" + "allOf": [ + { + "type": "object", + "properties": { + "version_id": { + "type": "string", + "format": "uuid" + }, + "computed_at": { + "type": "string", + "format": "date-time" + }, + "is_frozen": { + "type": "boolean" + }, + "units": { + "type": "string", + "const": "thousand_rub" + }, + "companies": { + "type": "array", + "items": { + "$ref": "#/$defs/KfoCompanyOut" + } + }, + "requisites": { + "$ref": "#/$defs/KfoRequisitesOut" + }, + "summary": { + "$ref": "#/$defs/KfoSummaryOut" + }, + "justification_required": { + "type": "boolean" + }, + "threshold_justification": { + "type": [ + "string", + "null" + ], + "maxLength": 4000 + }, + "goodwill": { + "$ref": "#/$defs/KfoGoodwillOut" + }, + "warnings": { + "type": "array", + "items": { + "$ref": "#/$defs/KfoWarningOut" + } + }, + "income": { + "$ref": "#/$defs/KfoStatementOut" + } + }, + "required": [ + "version_id", + "computed_at", + "is_frozen", + "units", + "companies", + "requisites", + "summary", + "justification_required", + "threshold_justification", + "goodwill", + "warnings", + "income" ], - "maxLength": 4000 - }, - "goodwill": { - "$ref": "#/$defs/KfoGoodwillOut" - }, - "warnings": { - "type": "array", - "items": { - "$ref": "#/$defs/KfoWarningOut" + "additionalProperties": true, + "description": "KfoResultOut без editable/read_only_reason, только отчёт ОФР", + "not": { + "required": [ + "balance" + ] } }, - "income": { - "$ref": "#/$defs/KfoStatementOut" + { + "properties": { + "is_frozen": { + "const": true + }, + "companies": { + "minItems": 1 + }, + "requisites": { + "properties": { + "version_status": { + "const": "VERIFIED" + } + } + } + } } - }, - "required": [ - "version_id", - "computed_at", - "is_frozen", - "units", - "companies", - "requisites", - "summary", - "justification_required", - "threshold_justification", - "goodwill", - "warnings", - "income" - ], - "additionalProperties": true, - "description": "KfoResultOut без editable/read_only_reason, только отчёт ОФР", - "not": { - "required": [ - "balance" - ] - } + ] }, "Ref": { "type": "object", @@ -1030,9 +1072,13 @@ "missing_quarters": { "type": "array", "items": { - "type": "integer", - "minimum": 1, - "maximum": 4 + "type": "string", + "enum": [ + "Q1", + "Q2", + "Q3", + "Q4" + ] } } }, @@ -1085,6 +1131,14 @@ "CorrectionEntryOut": { "type": "object", "properties": { + "entry_ids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "minItems": 1 + }, "legal_entity_id": { "type": "string", "format": "uuid" @@ -1112,6 +1166,7 @@ } }, "required": [ + "entry_ids", "legal_entity_id", "counterparty_id", "account_code", @@ -1173,6 +1228,154 @@ ], "additionalProperties": true }, + "CorrectionBalanceOut": { + "type": "object", + "properties": { + "debit_total": { + "type": "string", + "pattern": "^-?\\d{1,16}\\.\\d{2}$" + }, + "credit_total": { + "type": "string", + "pattern": "^-?\\d{1,16}\\.\\d{2}$" + }, + "difference": { + "type": "string", + "pattern": "^-?\\d{1,16}\\.\\d{2}$" + }, + "difference_percent": { + "type": [ + "string", + "null" + ], + "pattern": "^-?\\d+\\.\\d{2}$" + }, + "remainder_1": { + "type": [ + "string", + "null" + ], + "pattern": "^-?\\d{1,16}\\.\\d{2}$" + }, + "remainder_2": { + "type": [ + "string", + "null" + ], + "pattern": "^-?\\d{1,16}\\.\\d{2}$" + } + }, + "required": [ + "debit_total", + "credit_total", + "difference", + "difference_percent", + "remainder_1", + "remainder_2" + ], + "additionalProperties": true + }, + "CorrectionPairOut": { + "type": "object", + "properties": { + "side_1": { + "$ref": "#/$defs/AggregateSideOut" + }, + "side_2": { + "$ref": "#/$defs/AggregateSideOut" + }, + "deviation_amount": { + "type": "string", + "pattern": "^-?\\d{1,16}\\.\\d{2}$" + }, + "deviation_percent": { + "type": [ + "string", + "null" + ], + "pattern": "^-?\\d+\\.\\d{2}$" + } + }, + "required": [ + "side_1", + "side_2", + "deviation_amount", + "deviation_percent" + ], + "additionalProperties": true + }, + "CorrectionRuleOut": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "rule_no": { + "type": "integer" + }, + "article_1_line": { + "type": [ + "string", + "null" + ] + }, + "article_2_line": { + "type": [ + "string", + "null" + ] + }, + "side_1": { + "type": "string", + "enum": [ + "D", + "K" + ] + }, + "measure": { + "type": "string" + }, + "company_1_accounts": { + "type": "array", + "items": { + "type": "string" + } + }, + "company_2_accounts": { + "type": "array", + "items": { + "type": "string" + } + }, + "company_1_excluded": { + "type": "array", + "items": { + "type": "string" + } + }, + "company_2_excluded": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "rule_no", + "article_1_line", + "article_2_line", + "side_1", + "measure", + "company_1_accounts", + "company_2_accounts", + "company_1_excluded", + "company_2_excluded" + ], + "additionalProperties": true, + "description": "Правило элиминации на дату верификации (CorrectionRuleOut)" + }, "CorrectionOut": { "type": "object", "properties": { @@ -1200,7 +1403,19 @@ "comment": { "type": "string" }, - "updated_by": {}, + "balance": { + "$ref": "#/$defs/CorrectionBalanceOut" + }, + "updated_by": { + "anyOf": [ + { + "$ref": "#/$defs/Ref" + }, + { + "type": "null" + } + ] + }, "updated_at": { "type": "string", "format": "date-time" @@ -1212,6 +1427,7 @@ "debit", "credit", "comment", + "balance", "updated_by", "updated_at" ], @@ -1236,6 +1452,12 @@ "side_2": { "$ref": "#/$defs/AggregateSideOut" }, + "pairs": { + "type": "array", + "items": { + "$ref": "#/$defs/CorrectionPairOut" + } + }, "deviation_amount": { "type": "string", "pattern": "^-?\\d{1,16}\\.\\d{2}$" @@ -1245,7 +1467,7 @@ "string", "null" ], - "pattern": "^-?\\d{1,5}\\.\\d{2}$" + "pattern": "^-?\\d+\\.\\d{2}$" }, "exceeds_threshold": { "type": "boolean" @@ -1280,6 +1502,7 @@ "company_2", "side_1", "side_2", + "pairs", "deviation_amount", "deviation_percent", "exceeds_threshold", @@ -1292,6 +1515,14 @@ "UnallocatedEntryOut": { "type": "object", "properties": { + "entry_ids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "minItems": 1 + }, "owner": { "$ref": "#/$defs/Ref" }, @@ -1320,6 +1551,7 @@ } }, "required": [ + "entry_ids", "owner", "counterparty", "account_code", @@ -1376,6 +1608,13 @@ "rules_version_no": { "type": "integer" }, + "rules": { + "type": "array", + "items": { + "$ref": "#/$defs/CorrectionRuleOut" + }, + "minItems": 1 + }, "aggregates": { "type": "array", "items": { @@ -1395,6 +1634,7 @@ "required": [ "statement_type", "rules_version_no", + "rules", "aggregates", "unallocated", "totals" @@ -1497,13 +1737,16 @@ "string", "null" ], - "pattern": "^-?\\d{1,5}\\.\\d{2}$" + "pattern": "^-?\\d+\\.\\d{2}$" }, "exceeds_threshold": { "type": "boolean" }, "one_sided": { "type": "boolean" + }, + "skipped_osv_entry_count": { + "type": "integer" } }, "required": [ @@ -1514,7 +1757,8 @@ "variance_amount", "variance_percent", "exceeds_threshold", - "one_sided" + "one_sided", + "skipped_osv_entry_count" ], "additionalProperties": true }, @@ -1525,18 +1769,41 @@ "type": "array", "items": { "$ref": "#/$defs/Ref" - } + }, + "minItems": 1 }, "cells": { "type": "array", "items": { "$ref": "#/$defs/MatrixCellOut" } + }, + "total_debit": { + "type": "object", + "propertyNames": { + "format": "uuid" + }, + "additionalProperties": { + "type": "string", + "pattern": "^-?\\d{1,16}\\.\\d{2}$" + } + }, + "total_credit": { + "type": "object", + "propertyNames": { + "format": "uuid" + }, + "additionalProperties": { + "type": "string", + "pattern": "^-?\\d{1,16}\\.\\d{2}$" + } } }, "required": [ "companies", - "cells" + "cells", + "total_debit", + "total_credit" ], "additionalProperties": true, "description": "IntercompanyMatrixOut; osv_entry_ids_* — справочно" @@ -1617,12 +1884,28 @@ "rule_no": { "type": "integer" }, + "statement_type": { + "type": "string", + "enum": [ + "balance", + "income" + ] + }, + "rules_version_no": { + "type": "integer" + }, + "rule": { + "$ref": "#/$defs/CorrectionRuleOut" + }, "matrix": { "$ref": "#/$defs/IntercompanyMatrixOut" } }, "required": [ "rule_no", + "statement_type", + "rules_version_no", + "rule", "matrix" ], "additionalProperties": true @@ -1929,9 +2212,13 @@ "type": "integer" }, "quarter": { - "type": "integer", - "minimum": 1, - "maximum": 4 + "type": "string", + "enum": [ + "Q1", + "Q2", + "Q3", + "Q4" + ] }, "version_no": { "type": "integer" @@ -1961,6 +2248,7 @@ }, "members": { "type": "array", + "minItems": 1, "items": { "type": "object", "properties": { @@ -1989,6 +2277,7 @@ }, "history": { "type": "array", + "minItems": 1, "items": { "type": "object", "properties": { diff --git a/contracts/examples/ex-payload-chess.json b/contracts/examples/ex-payload-chess.json index 2ae86c3..95af084 100644 --- a/contracts/examples/ex-payload-chess.json +++ b/contracts/examples/ex-payload-chess.json @@ -32,11 +32,58 @@ "one_sided": false, "skipped_osv_entry_count": 0 } - ] + ], + "total_debit": { + "11111111-1111-4111-8111-111111111111": "102000000.00", + "22222222-2222-4222-8222-222222222222": "0.00", + "33333333-3333-4333-8333-333333333333": "0.00" + }, + "total_credit": { + "11111111-1111-4111-8111-111111111111": "0.00", + "22222222-2222-4222-8222-222222222222": "100000400.50", + "33333333-3333-4333-8333-333333333333": "0.00" + } }, "presets": [ { "rule_no": 6, + "statement_type": "balance", + "rules_version_no": 1, + "rule": { + "id": "66666666-6666-4666-8666-000000000006", + "rule_no": 6, + "article_1_line": "1230", + "article_2_line": "1520", + "side_1": "D", + "measure": "closing", + "company_1_accounts": [ + "60.02", + "60.22", + "60.32", + "62.01", + "62.03", + "62.21", + "62.31", + "76" + ], + "company_2_accounts": [ + "60.01", + "60.03", + "60.21", + "60.31", + "62.02", + "62.22", + "62.32", + "76" + ], + "company_1_excluded": [ + "76.7.5", + "76.7.9" + ], + "company_2_excluded": [ + "76.7.1" + ] + }, "matrix": { "companies": [ { @@ -64,7 +111,17 @@ "one_sided": false, "skipped_osv_entry_count": 0 } - ] + ], + "total_debit": { + "11111111-1111-4111-8111-111111111111": "102000000.00", + "22222222-2222-4222-8222-222222222222": "0.00", + "33333333-3333-4333-8333-333333333333": "0.00" + }, + "total_credit": { + "11111111-1111-4111-8111-111111111111": "0.00", + "22222222-2222-4222-8222-222222222222": "100000400.50", + "33333333-3333-4333-8333-333333333333": "0.00" + } } } ], @@ -87,15 +144,19 @@ { "code": "1230", "name": "Дебиторская задолженность", - "group_total": "112000000.00", + "group_total": "142000000.00", "amounts": [ { "company_id": "11111111-1111-4111-8111-111111111111", - "amount": "102000000.00" + "amount": "132000000.00" }, { "company_id": "22222222-2222-4222-8222-222222222222", "amount": "10000000.00" + }, + { + "company_id": "33333333-3333-4333-8333-333333333333", + "amount": "0.00" } ] } diff --git a/contracts/examples/ex-payload-corrections.json b/contracts/examples/ex-payload-corrections.json index 387a6ca..b849d55 100644 --- a/contracts/examples/ex-payload-corrections.json +++ b/contracts/examples/ex-payload-corrections.json @@ -6,41 +6,6 @@ "source_version_no": 1, "data": { "items": [ - { - "statement_type": "balance", - "rule_no": 6, - "company_1": { - "id": "22222222-2222-4222-8222-222222222222", - "name": "ООО «Бета»" - }, - "company_2": { - "id": "11111111-1111-4111-8111-111111111111", - "name": "ООО «Альфа»" - }, - "correction": { - "status": "APPLIED", - "version_no": 1, - "debit": [ - { - "line_code": "1230", - "amount": "10000000.00" - } - ], - "credit": [ - { - "line_code": "1520", - "amount": "10000000.00" - } - ], - "comment": "Сверка Бета → Альфа: разница — аванс, исключаем подтверждённое", - "reason_code": null, - "updated_by": { - "user_id": "44444444-4444-4444-8444-444444444444", - "full_name": "Петрова Мария Ивановна" - }, - "updated_at": "2025-11-12T14:30:00Z" - } - }, { "statement_type": "balance", "rule_no": 6, @@ -73,12 +38,63 @@ ], "comment": "Часть долга Гаммы переведена в займ — разнесено по статьям", "reason_code": null, + "balance": { + "debit_total": "25000000.00", + "credit_total": "22500000.00", + "difference": "2500000.00", + "difference_percent": "11.11", + "remainder_1": "5000000.00", + "remainder_2": "6600000.00" + }, "updated_by": { - "user_id": "44444444-4444-4444-8444-444444444444", - "full_name": "Петрова Мария Ивановна" + "id": "44444444-4444-4444-8444-444444444444", + "name": "Петрова Мария Ивановна" }, "updated_at": "2025-11-13T09:10:00Z" } + }, + { + "statement_type": "balance", + "rule_no": 6, + "company_1": { + "id": "22222222-2222-4222-8222-222222222222", + "name": "ООО «Бета»" + }, + "company_2": { + "id": "11111111-1111-4111-8111-111111111111", + "name": "ООО «Альфа»" + }, + "correction": { + "status": "APPLIED", + "version_no": 1, + "debit": [ + { + "line_code": "1230", + "amount": "10000000.00" + } + ], + "credit": [ + { + "line_code": "1520", + "amount": "10000000.00" + } + ], + "comment": "Сверка Бета → Альфа: разница — аванс, исключаем подтверждённое", + "reason_code": null, + "balance": { + "debit_total": "10000000.00", + "credit_total": "10000000.00", + "difference": "0.00", + "difference_percent": "0.00", + "remainder_1": "0.00", + "remainder_2": "40000000.00" + }, + "updated_by": { + "id": "44444444-4444-4444-8444-444444444444", + "name": "Петрова Мария Ивановна" + }, + "updated_at": "2025-11-12T14:30:00Z" + } } ] } diff --git a/contracts/examples/ex-payload-goodwill.json b/contracts/examples/ex-payload-goodwill.json index 6b285d3..83245bb 100644 --- a/contracts/examples/ex-payload-goodwill.json +++ b/contracts/examples/ex-payload-goodwill.json @@ -28,8 +28,8 @@ "has_investments": true, "warnings": [], "updated_by": { - "user_id": "44444444-4444-4444-8444-444444444444", - "full_name": "Петрова Мария Ивановна" + "id": "44444444-4444-4444-8444-444444444444", + "name": "Петрова Мария Ивановна" }, "updated_at": "2025-11-12T11:00:00Z" } diff --git a/contracts/examples/ex-payload-summary-osv.json b/contracts/examples/ex-payload-summary-osv.json index 8e60ef0..216c166 100644 --- a/contracts/examples/ex-payload-summary-osv.json +++ b/contracts/examples/ex-payload-summary-osv.json @@ -7,7 +7,9 @@ "data": { "rows": [ { - "entry_ids": [], + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000013" + ], "legal_entity_id": "11111111-1111-4111-8111-111111111111", "legal_entity_name": "ООО «Альфа»", "legal_entity_inn": "7701000001", @@ -33,7 +35,9 @@ "warnings": [] }, { - "entry_ids": [], + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000014" + ], "legal_entity_id": "22222222-2222-4222-8222-222222222222", "legal_entity_name": "ООО «Бета»", "legal_entity_inn": "7701000002", @@ -59,7 +63,9 @@ "warnings": [] }, { - "entry_ids": [], + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000012" + ], "legal_entity_id": "11111111-1111-4111-8111-111111111111", "legal_entity_name": "ООО «Альфа»", "legal_entity_inn": "7701000001", @@ -96,7 +102,7 @@ "legal_entity_name": "ООО «Гамма»", "account_code": "76", "missing_quarters": [ - 1 + "Q1" ] } ], diff --git a/contracts/examples/ex-payload-version-card.json b/contracts/examples/ex-payload-version-card.json index 451ec87..e54a9e1 100644 --- a/contracts/examples/ex-payload-version-card.json +++ b/contracts/examples/ex-payload-version-card.json @@ -15,7 +15,7 @@ }, "reporting_date": "2025-10-01", "year": 2025, - "quarter": 3, + "quarter": "Q3", "version_no": 1, "scenario": "CREATE", "approach": "BASIC", diff --git a/contracts/examples/ex-payload-vgo-pairs.json b/contracts/examples/ex-payload-vgo-pairs.json index 1bba063..ca83325 100644 --- a/contracts/examples/ex-payload-vgo-pairs.json +++ b/contracts/examples/ex-payload-vgo-pairs.json @@ -9,8 +9,170 @@ "version_id": "99999999-9999-4999-8999-999999999999", "statement_type": "balance", "rules_version_no": 1, - "rules": [], + "rules": [ + { + "id": "66666666-6666-4666-8666-000000000002", + "rule_no": 2, + "article_1_line": "1170", + "article_2_line": "1310/1350/1370", + "side_1": "D", + "measure": "closing", + "company_1_accounts": [ + "58.01" + ], + "company_2_accounts": [ + "80", + "83" + ], + "company_1_excluded": [], + "company_2_excluded": [] + }, + { + "id": "66666666-6666-4666-8666-000000000006", + "rule_no": 6, + "article_1_line": "1230", + "article_2_line": "1520", + "side_1": "D", + "measure": "closing", + "company_1_accounts": [ + "60.02", + "60.22", + "60.32", + "62.01", + "62.03", + "62.21", + "62.31", + "76" + ], + "company_2_accounts": [ + "60.01", + "60.03", + "60.21", + "60.31", + "62.02", + "62.22", + "62.32", + "76" + ], + "company_1_excluded": [ + "76.7.5", + "76.7.9" + ], + "company_2_excluded": [ + "76.7.1" + ] + } + ], "aggregates": [ + { + "rule_id": "66666666-6666-4666-8666-000000000002", + "rule_no": 2, + "company_1": { + "id": "11111111-1111-4111-8111-111111111111", + "name": "ООО «Альфа»" + }, + "company_2": { + "id": "22222222-2222-4222-8222-222222222222", + "name": "ООО «Бета»" + }, + "side_1": { + "article_line": "1170", + "accounts": [ + "58.01" + ], + "amount": "50000000.00", + "entries": [ + { + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000003" + ], + "legal_entity_id": "11111111-1111-4111-8111-111111111111", + "counterparty_id": "22222222-2222-4222-8222-222222222222", + "account_code": "58.01", + "side": "D", + "amount": "50000000.00", + "decision": "AUTO", + "candidate_rule_ids": [] + } + ] + }, + "side_2": { + "article_line": "1310/1350/1370", + "accounts": [ + "80" + ], + "amount": "40000000.00", + "entries": [ + { + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000004" + ], + "legal_entity_id": "22222222-2222-4222-8222-222222222222", + "counterparty_id": "11111111-1111-4111-8111-111111111111", + "account_code": "80", + "side": "K", + "amount": "40000000.00", + "decision": "AUTO", + "candidate_rule_ids": [] + } + ] + }, + "pairs": [], + "deviation_amount": "10000000.00", + "deviation_percent": "25.00", + "exceeds_threshold": false, + "status": "AUTO", + "eliminated_amount": "40000000.00", + "composition_changed": false, + "correction": null + }, + { + "rule_id": "66666666-6666-4666-8666-000000000002", + "rule_no": 2, + "company_1": { + "id": "11111111-1111-4111-8111-111111111111", + "name": "ООО «Альфа»" + }, + "company_2": { + "id": "33333333-3333-4333-8333-333333333333", + "name": "ООО «Гамма»" + }, + "side_1": { + "article_line": "1170", + "accounts": [ + "58.01" + ], + "amount": "15000000.00", + "entries": [ + { + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000005" + ], + "legal_entity_id": "11111111-1111-4111-8111-111111111111", + "counterparty_id": "33333333-3333-4333-8333-333333333333", + "account_code": "58.01", + "side": "D", + "amount": "15000000.00", + "decision": "AUTO", + "candidate_rule_ids": [] + } + ] + }, + "side_2": { + "article_line": "1310/1350/1370", + "accounts": [], + "amount": "0.00", + "entries": [] + }, + "pairs": [], + "deviation_amount": "15000000.00", + "deviation_percent": null, + "exceeds_threshold": false, + "status": "ONE_SIDED", + "eliminated_amount": "15000000.00", + "composition_changed": false, + "correction": null + }, { "rule_id": "66666666-6666-4666-8666-000000000006", "rule_no": 6, @@ -30,7 +192,9 @@ "amount": "102000000.00", "entries": [ { - "entry_ids": [], + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000001" + ], "legal_entity_id": "11111111-1111-4111-8111-111111111111", "counterparty_id": "22222222-2222-4222-8222-222222222222", "account_code": "62.01", @@ -49,7 +213,9 @@ "amount": "100000400.50", "entries": [ { - "entry_ids": [], + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000002" + ], "legal_entity_id": "22222222-2222-4222-8222-222222222222", "counterparty_id": "11111111-1111-4111-8111-111111111111", "account_code": "60.01", @@ -60,6 +226,54 @@ } ] }, + "pairs": [ + { + "side_1": { + "article_line": "1230", + "accounts": [ + "62.01" + ], + "amount": "102000000.00", + "entries": [ + { + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000001" + ], + "legal_entity_id": "11111111-1111-4111-8111-111111111111", + "counterparty_id": "22222222-2222-4222-8222-222222222222", + "account_code": "62.01", + "side": "D", + "amount": "102000000.00", + "decision": "AUTO", + "candidate_rule_ids": [] + } + ] + }, + "side_2": { + "article_line": "1520", + "accounts": [ + "60.01" + ], + "amount": "100000400.50", + "entries": [ + { + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000002" + ], + "legal_entity_id": "22222222-2222-4222-8222-222222222222", + "counterparty_id": "11111111-1111-4111-8111-111111111111", + "account_code": "60.01", + "side": "K", + "amount": "100000400.50", + "decision": "AUTO", + "candidate_rule_ids": [] + } + ] + }, + "deviation_amount": "1999599.50", + "deviation_percent": "2.00" + } + ], "deviation_amount": "1999599.50", "deviation_percent": "2.00", "exceeds_threshold": false, @@ -68,6 +282,102 @@ "composition_changed": false, "correction": null }, + { + "rule_id": "66666666-6666-4666-8666-000000000006", + "rule_no": 6, + "company_1": { + "id": "11111111-1111-4111-8111-111111111111", + "name": "ООО «Альфа»" + }, + "company_2": { + "id": "33333333-3333-4333-8333-333333333333", + "name": "ООО «Гамма»" + }, + "side_1": { + "article_line": "1230", + "accounts": [ + "62.01" + ], + "amount": "30000000.00", + "entries": [ + { + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000006" + ], + "legal_entity_id": "11111111-1111-4111-8111-111111111111", + "counterparty_id": "33333333-3333-4333-8333-333333333333", + "account_code": "62.01", + "side": "D", + "amount": "30000000.00", + "decision": "AUTO", + "candidate_rule_ids": [] + } + ] + }, + "side_2": { + "article_line": "1520", + "accounts": [ + "60.01" + ], + "amount": "29100000.00", + "entries": [ + { + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000007" + ], + "legal_entity_id": "33333333-3333-4333-8333-333333333333", + "counterparty_id": "11111111-1111-4111-8111-111111111111", + "account_code": "60.01", + "side": "K", + "amount": "29100000.00", + "decision": "AUTO", + "candidate_rule_ids": [] + } + ] + }, + "pairs": [], + "deviation_amount": "900000.00", + "deviation_percent": "3.09", + "exceeds_threshold": false, + "status": "AUTO", + "eliminated_amount": "29100000.00", + "composition_changed": false, + "correction": { + "status": "APPLIED", + "version_no": 2, + "debit": [ + { + "line_code": "1230", + "amount": "20000000.00" + }, + { + "line_code": "1170", + "amount": "5000000.00" + } + ], + "credit": [ + { + "line_code": "1520", + "amount": "22500000.00" + } + ], + "comment": "Часть долга Гаммы переведена в займ — разнесено по статьям", + "reason_code": null, + "balance": { + "debit_total": "25000000.00", + "credit_total": "22500000.00", + "difference": "2500000.00", + "difference_percent": "11.11", + "remainder_1": "5000000.00", + "remainder_2": "6600000.00" + }, + "updated_by": { + "id": "44444444-4444-4444-8444-444444444444", + "name": "Петрова Мария Ивановна" + }, + "updated_at": "2025-11-13T09:10:00Z" + } + }, { "rule_id": "66666666-6666-4666-8666-000000000006", "rule_no": 6, @@ -87,7 +397,9 @@ "amount": "10000000.00", "entries": [ { - "entry_ids": [], + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000008" + ], "legal_entity_id": "22222222-2222-4222-8222-222222222222", "counterparty_id": "11111111-1111-4111-8111-111111111111", "account_code": "62.01", @@ -107,7 +419,9 @@ "amount": "50000000.00", "entries": [ { - "entry_ids": [], + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000009" + ], "legal_entity_id": "11111111-1111-4111-8111-111111111111", "counterparty_id": "22222222-2222-4222-8222-222222222222", "account_code": "60.01", @@ -117,7 +431,9 @@ "candidate_rule_ids": [] }, { - "entry_ids": [], + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000010" + ], "legal_entity_id": "11111111-1111-4111-8111-111111111111", "counterparty_id": "22222222-2222-4222-8222-222222222222", "account_code": "76.05", @@ -128,6 +444,7 @@ } ] }, + "pairs": [], "deviation_amount": "40000000.00", "deviation_percent": "400.00", "exceeds_threshold": true, @@ -151,9 +468,17 @@ ], "comment": "Сверка Бета → Альфа: разница — аванс, исключаем подтверждённое", "reason_code": null, + "balance": { + "debit_total": "10000000.00", + "credit_total": "10000000.00", + "difference": "0.00", + "difference_percent": "0.00", + "remainder_1": "0.00", + "remainder_2": "40000000.00" + }, "updated_by": { - "user_id": "44444444-4444-4444-8444-444444444444", - "full_name": "Петрова Мария Ивановна" + "id": "44444444-4444-4444-8444-444444444444", + "name": "Петрова Мария Ивановна" }, "updated_at": "2025-11-12T14:30:00Z" } @@ -161,7 +486,9 @@ ], "unallocated": [ { - "entry_ids": [], + "entry_ids": [ + "aaaaaaaa-0000-4000-8000-000000000011" + ], "owner": { "id": "33333333-3333-4333-8333-333333333333", "name": "ООО «Гамма»" @@ -174,17 +501,20 @@ "line_code": null, "side": "D", "amount": "350000.00", - "candidate_rule_ids": [], + "candidate_rule_ids": [ + "66666666-6666-4666-8666-000000000002", + "66666666-6666-4666-8666-000000000006" + ], "reason": "MULTIPLE_RULES", "excluded": true } ], "totals": { - "amount_1": "112000000.00", - "amount_2": "150000400.50", - "deviation_amount": "41999599.50", - "correction_debit": "10000000.00", - "correction_credit": "10000000.00" + "amount_1": "207000000.00", + "amount_2": "219100400.50", + "deviation_amount": "67899599.50", + "correction_debit": "35000000.00", + "correction_credit": "32500000.00" } }, "income": null diff --git a/model/data-model-v5.26.dbml b/model/data-model-v5.26.dbml index fb9ba58..72f47df 100644 --- a/model/data-model-v5.26.dbml +++ b/model/data-model-v5.26.dbml @@ -1559,7 +1559,7 @@ Table ex_snapshot { superseded_by_snapshot_id uuid [ref: > ex_snapshot.id, note: 'v5.12: для ARCHIVE — ссылка на актуальный срез, который его заместил'] verified_at timestamptz [not null] written_at timestamptz [not null] - written_by uuid [ref: > app_user.id, note: 'инициатор записи'] + written_by uuid [ref: > app_user.id, note: 'инициатор записи = пользователь, выполнивший переход «Верифицирован» (v5.26, §14)'] archived_at timestamptz data_lake_ref varchar [note: 'ссылка на архив в data lake'] Indexes { diff --git a/openspec/changes/ex-payload-schema/design.md b/openspec/changes/ex-payload-schema/design.md index cd75bd2..bdc883d 100644 --- a/openspec/changes/ex-payload-schema/design.md +++ b/openspec/changes/ex-payload-schema/design.md @@ -69,7 +69,7 @@ proposal, AURORA-1562). Обязательные поля задаёт схем | `goodwill` | `rub` | только при `goodwill_enabled`: `GoodwillTableOut` (`dev/api-contract-goodwill.*`) без `editable`/`read_only_reason` — строки, группы, итоги | | `corrections` | `rub` | применённые корректировки (`APPLIED`; `REVOKED` нет, `DRAFT` не пишется): `{items: [{statement_type, rule_no, company_1: Ref, company_2: Ref, correction: CorrectionOut}]}` — `CorrectionOut` как в контракте корректировок (`updated_by`/`updated_at` — кто и когда сохранил) | | `other_report` | `thousand_rub` | только при `scenario = CONFIRM`: `{report_kind: "client_kfo_comparison", lines[] {statement_type, line_code, line_name, client_amount, client_vgo_amount, bank_amount, bank_vgo_amount, deviation_amount, deviation_percent, deviation_reason, comment}}` по `client_kfo_line` (контракта API ещё нет — AURORA-1549; имена — как в DBML) | -| `version_card` | `none` | один на срез, **новый тип**: `company_group {id, name}`, `related_group {id, name}`, `reporting_date`, `year`, `quarter`, `version_no`, `scenario`, `approach`, `goodwill_enabled`, `initiator` (UserRef), `initiator_office {id, name}`, `members[]` `{legal_entity_id, name, inn, is_head_entity}`, `history[]` как `VersionCard.history` (`{from_status, to_status, changed_by (UserRef), changed_at, comment}`), `verified_by` (UserRef), `verified_at`, `uob_decision_no`, `uob_decision_date`, `methodology_deviations` | +| `version_card` | `none` | один на срез, **новый тип**: `company_group {id, name}`, `related_group {id, name}`, `reporting_date`, `year`, `quarter`, `version_no`, `scenario`, `approach`, `goodwill_enabled`, `initiator` (UserRef); `quarter` — `"Q1"…"Q4"`, `initiator_office {id, name}`, `members[]` `{legal_entity_id, name, inn, is_head_entity}`, `history[]` как `VersionCard.history` (`{from_status, to_status, changed_by (UserRef), changed_at, comment}`), `verified_by` (UserRef), `verified_at`, `uob_decision_no`, `uob_decision_date`, `methodology_deviations` | `UserRef {user_id, full_name}` — ФИО на дату верификации (пользователь может быть отключён, но не удаляется). @@ -122,4 +122,8 @@ proposal, AURORA-1562). Обязательные поля задаёт схем | 9 | 06.11.2026 | Payload покрывает и выгрузку xlsx рабочей версии (пробелы, способ сопоставления, строки ОСВ агрегатов, сводка корректировок), не только экран | Срез выгружается на лету из payload, выгрузка должна совпасть с рабочей (1562); ответ на вопрос проверки исполнителем 1 | Срез заметно больше (строки ОСВ агрегатов) | | 10 | 06.11.2026 | `data` — DTO API без изменений имён и вложенности; таблица §4.2 подчинена принципу | Рендер и выгрузка ЕХ переиспользуют виджеты и выгрузки рабочей версии без маппинга; вопрос проверки 2 | Изменение DTO API после 1.0 требует новой версии схемы или совместимого расширения | | 11 | 06.11.2026 | Нумерация среза — по ГСК; «один актуальный» и `superseded_by` — по ГК и дате | Как в DBML и DOC-274686 (решения Р-35/Р-36 по ГК); вопрос проверки 3 | При ключе «ГСК» у заказчика — правка правила связи и проверки 2002 | +| 12 | 06.11.2026 | Схема обязывает то, что читают выгрузки xlsx рабочей версии: итоги шахматки `total_debit`/`total_credit`, правило пресета и `rules` таблицы корректировок, `pairs`, `balance` корректировки, `entry_ids` строк | Ревью C/I: без них выгрузка среза не совпадёт с рабочей (решение #9) | Больше обязательных полей — писателю нельзя «урезать» DTO | +| 13 | 06.11.2026 | Проценты к меньшей стороне (шахматка, агрегаты, корректировки) — без ограничения разрядов; квартал — `"Q1"…"Q4"`; `updated_by` — `Ref`, как в DTO | Ревью C-1, C-2, I-2: настоящие ответы API не проходили схему | — | +| 14 | 06.11.2026 | КФО в срезе — только замороженная и `VERIFIED`; карточка — непустые состав и история | Ревью M-1: пустой или незамороженный payload не должен проходить | Срез версии без истории маршрута (миграция старых данных) схему не пройдёт | +| 15 | 06.11.2026 | Ошибка пресета правила (422 `INVALID_RULE`/`ACCOUNTS_REQUIRED`) — пресет пропускается; другая ошибка сборки — сбой записи среза | Ревью M-5 | Срез без пресета правила, которое на экране тоже не строится | | 8 | 06.11.2026 | Без отдельного согласования дизайна с владельцем: схема описана в чате 06.11 («все так»), владелец сказал «делай схему» | Срок 12.11, владелец просил не останавливаться | Расхождение с ожиданием владельца видно только в PR | diff --git a/scripts/kfo_contract/gen_ex_payload.py b/scripts/kfo_contract/gen_ex_payload.py index b41b409..f24dec8 100644 --- a/scripts/kfo_contract/gen_ex_payload.py +++ b/scripts/kfo_contract/gen_ex_payload.py @@ -17,9 +17,12 @@ import gen_kfo_openapi as kfo # noqa: E402 MONEY_RE = r"^-?\d{1,16}\.\d{2}$" M = {"type": "string", "pattern": MONEY_RE} MN = {"type": ["string", "null"], "pattern": MONEY_RE} -PN = {"type": ["string", "null"], "pattern": r"^-?\d{1,5}\.\d{2}$"} +PN = {"type": ["string", "null"], "pattern": r"^-?\d{1,5}\.\d{2}$"} # доля, % (ограничена) +PF = {"type": ["string", "null"], "pattern": r"^-?\d+\.\d{2}$"} # процент к меньшей стороне — не ограничен +QUARTER = {"type": "string", "enum": ["Q1", "Q2", "Q3", "Q4"]} UUID = {"type": "string", "format": "uuid"} UUIDN = {"type": ["string", "null"], "format": "uuid"} +UUIDS1 = {"type": "array", "items": UUID, "minItems": 1} STR = {"type": "string"} STRN = {"type": ["string", "null"]} DATE = {"type": "string", "format": "date"} @@ -76,9 +79,13 @@ def defs(): out["KfoBalanceData"] = obj({**base, "balance": d("KfoStatementOut")}, desc="KfoResultOut без editable/read_only_reason, только отчёт ББ") out["KfoBalanceData"]["not"] = {"required": ["income"]} + frozen = {"properties": {"is_frozen": {"const": True}, "companies": {"minItems": 1}, + "requisites": {"properties": {"version_status": {"const": "VERIFIED"}}}}} + out["KfoBalanceData"] = {"allOf": [out["KfoBalanceData"], frozen]} out["KfoIncomeData"] = obj({**base, "income": d("KfoStatementOut")}, desc="KfoResultOut без editable/read_only_reason, только отчёт ОФР") out["KfoIncomeData"]["not"] = {"required": ["balance"]} + out["KfoIncomeData"] = {"allOf": [out["KfoIncomeData"], frozen]} out["Ref"] = obj({"id": UUID, "name": STR}) out["UserRef"] = obj({"user_id": UUIDN, "full_name": STR}, desc="Пользователь с ФИО на дату верификации (user_id — справочно, app_user не удаляется)") @@ -94,35 +101,46 @@ def defs(): "closing_debit": MN, "closing_credit": MN, }, desc="ConsolidatedRowOut (dev/api-contract-slice-4.md §3); entry_ids, warnings — справочно, необязательны") out["OsvGapOut"] = obj({"legal_entity_id": UUID, "legal_entity_name": STR, "account_code": STR, - "missing_quarters": arr({"type": "integer", "minimum": 1, "maximum": 4})}) + "missing_quarters": arr(QUARTER)}) out["OsvSummaryOut"] = obj({"rows_total": {"type": "integer"}, "gaps": arr(d("OsvGapOut"))}) out["SummaryOsvData"] = obj({"rows": arr(d("ConsolidatedRowOut")), "summary": d("OsvSummaryOut")}) # Таблица корректировок — dev/api-contract-corrections.* - out["CorrectionEntryOut"] = obj({"legal_entity_id": UUID, "counterparty_id": UUID, "account_code": STR, + out["CorrectionEntryOut"] = obj({"entry_ids": UUIDS1, "legal_entity_id": UUID, "counterparty_id": UUID, "account_code": STR, "side": {"type": "string", "enum": ["D", "K"]}, "amount": M, "decision": STR}, desc="строка ОСВ агрегата (нужна выгрузке таблицы корректировок)") out["AggregateSideOut"] = obj({"article_line": STRN, "accounts": arr(STR), "amount": M, "entries": arr(d("CorrectionEntryOut"))}) out["CorrectionLineOut"] = obj({"line_code": STR, "amount": M}) + out["CorrectionBalanceOut"] = obj({"debit_total": M, "credit_total": M, "difference": M, "difference_percent": PF, + "remainder_1": MN, "remainder_2": MN}) + out["CorrectionPairOut"] = obj({"side_1": d("AggregateSideOut"), "side_2": d("AggregateSideOut"), + "deviation_amount": M, "deviation_percent": PF}) + out["CorrectionRuleOut"] = obj({"id": UUID, "rule_no": {"type": "integer"}, "article_1_line": STRN, + "article_2_line": STRN, "side_1": {"type": "string", "enum": ["D", "K"]}, + "measure": STR, "company_1_accounts": arr(STR), "company_2_accounts": arr(STR), + "company_1_excluded": arr(STR), "company_2_excluded": arr(STR)}, + desc="Правило элиминации на дату верификации (CorrectionRuleOut)") out["CorrectionOut"] = obj({"status": {"type": "string", "enum": ["APPLIED"]}, "version_no": {"type": "integer"}, "debit": arr(d("CorrectionLineOut")), "credit": arr(d("CorrectionLineOut")), - "comment": STR, "updated_by": {}, "updated_at": DT}, + "comment": STR, "balance": d("CorrectionBalanceOut"), + "updated_by": {"anyOf": [d("Ref"), {"type": "null"}]}, "updated_at": DT}, desc="CorrectionOut; в срезе только APPLIED") out["CorrectionAggregateOut"] = obj({ "rule_no": {"type": "integer"}, "company_1": d("Ref"), "company_2": d("Ref"), - "side_1": d("AggregateSideOut"), "side_2": d("AggregateSideOut"), - "deviation_amount": M, "deviation_percent": PN, "exceeds_threshold": {"type": "boolean"}, + "side_1": d("AggregateSideOut"), "side_2": d("AggregateSideOut"), "pairs": arr(d("CorrectionPairOut")), + "deviation_amount": M, "deviation_percent": PF, "exceeds_threshold": {"type": "boolean"}, "status": {"type": "string", "enum": ["AUTO", "DISCREPANCY", "ONE_SIDED", "MANUAL"]}, "eliminated_amount": M, "correction": {"anyOf": [d("CorrectionOut"), {"type": "null"}]}, }) - out["UnallocatedEntryOut"] = obj({"owner": d("Ref"), "counterparty": d("Ref"), "account_code": STR, + out["UnallocatedEntryOut"] = obj({"entry_ids": UUIDS1, "owner": d("Ref"), "counterparty": d("Ref"), "account_code": STR, "side": {"type": "string", "enum": ["D", "K"]}, "amount": M, "reason": STR, "excluded": {"type": "boolean"}}) out["CorrectionTotalsOut"] = obj({"amount_1": M, "amount_2": M, "deviation_amount": M, "correction_debit": M, "correction_credit": M}) out["CorrectionTableData"] = obj({ "statement_type": {"type": "string", "enum": ["balance", "income"]}, "rules_version_no": {"type": "integer"}, + "rules": {"type": "array", "items": d("CorrectionRuleOut"), "minItems": 1}, "aggregates": arr(d("CorrectionAggregateOut")), "unallocated": arr(d("UnallocatedEntryOut")), "totals": d("CorrectionTotalsOut"), }, desc="CorrectionTableOut без editable/read_only_reason") @@ -133,14 +151,19 @@ def defs(): "company_1": d("Ref"), "company_2": d("Ref"), "correction": d("CorrectionOut")}))}) # Шахматка — dev/frontend-api-guide.md §5.7.1 out["MatrixCellOut"] = obj({"company_a_id": UUID, "company_b_id": UUID, "amount_a": M, "amount_b": M, - "variance_amount": M, "variance_percent": PN, "exceeds_threshold": {"type": "boolean"}, - "one_sided": {"type": "boolean"}}) - out["IntercompanyMatrixOut"] = obj({"companies": arr(d("Ref")), "cells": arr(d("MatrixCellOut"))}, + "variance_amount": M, "variance_percent": PF, "exceeds_threshold": {"type": "boolean"}, + "one_sided": {"type": "boolean"}, "skipped_osv_entry_count": {"type": "integer"}}) + totals = {"type": "object", "propertyNames": {"format": "uuid"}, "additionalProperties": M} + out["IntercompanyMatrixOut"] = obj({"companies": {"type": "array", "items": d("Ref"), "minItems": 1}, + "cells": arr(d("MatrixCellOut")), "total_debit": totals, + "total_credit": totals}, desc="IntercompanyMatrixOut; osv_entry_ids_* — справочно") out["ResultTableOut"] = obj({"companies": arr(d("Ref")), "articles": arr(obj({ "code": STR, "name": STR, "amounts": arr(obj({"company_id": UUID, "amount": M})), "group_total": M}))}) out["ChessData"] = obj({"matrix_all": d("IntercompanyMatrixOut"), - "presets": arr(obj({"rule_no": {"type": "integer"}, "matrix": d("IntercompanyMatrixOut")})), + "presets": arr(obj({"rule_no": {"type": "integer"}, "statement_type": {"type": "string", "enum": ["balance", "income"]}, + "rules_version_no": {"type": "integer"}, "rule": d("CorrectionRuleOut"), + "matrix": d("IntercompanyMatrixOut")})), "result_table": d("ResultTableOut")}) # Гудвилл — dev/api-contract-goodwill.* out["GoodwillTotalsOut"] = obj({"financial_investment_amount": MN, "charter_capital_amount": MN, @@ -161,14 +184,15 @@ def defs(): statuses = kfo.SCHEMAS["KfoRequisitesOut"]["properties"]["version_status"]["enum"] out["VersionCardData"] = obj({ "company_group": d("Ref"), "related_group": d("Ref"), "reporting_date": DATE, - "year": {"type": "integer"}, "quarter": {"type": "integer", "minimum": 1, "maximum": 4}, + "year": {"type": "integer"}, "quarter": QUARTER, "version_no": {"type": "integer"}, "scenario": {"type": "string", "enum": ["CREATE", "CONFIRM"]}, "approach": {"type": "string", "enum": ["BASIC", "EXTENDED"]}, "goodwill_enabled": {"type": "boolean"}, "initiator": d("UserRef"), "initiator_office": d("Ref"), - "members": arr(obj({"legal_entity_id": UUID, "name": STR, "inn": STR, "is_head_entity": {"type": "boolean"}})), - "history": arr(obj({"from_status": {"type": ["string", "null"], "enum": statuses + [None]}, + "members": {"type": "array", "minItems": 1, "items": obj({"legal_entity_id": UUID, "name": STR, "inn": STR, + "is_head_entity": {"type": "boolean"}})}, + "history": {"type": "array", "minItems": 1, "items": obj({"from_status": {"type": ["string", "null"], "enum": statuses + [None]}, "to_status": {"type": "string", "enum": statuses}, - "changed_by": d("UserRef"), "changed_at": DT, "comment": STRN})), + "changed_by": d("UserRef"), "changed_at": DT, "comment": STRN})}, "verified_by": d("UserRef"), "verified_at": DT, "uob_decision_no": STRN, "uob_decision_date": {"type": ["string", "null"], "format": "date"}, "methodology_deviations": STRN, @@ -225,51 +249,86 @@ def examples(): ki = {k: v for k, v in res.items() if k != "balance"} out = {"kfo_balance": env("kfo_balance", kb), "kfo_income": env("kfo_income", ki)} + eid = iter(f"aaaaaaaa-0000-4000-8000-{n:012d}" for n in range(1, 1000)) + R2 = "66666666-6666-4666-8666-000000000002" + R6 = "66666666-6666-4666-8666-000000000006" + REFU = {"id": USER["user_id"], "name": USER["full_name"]} # updated_by — Ref, как в DTO API + def entry(c, cp, acc, side, amt): - return {"entry_ids": [], "legal_entity_id": IDS[c], "counterparty_id": IDS[cp], "account_code": acc, + return {"entry_ids": [next(eid)], "legal_entity_id": IDS[c], "counterparty_id": IDS[cp], "account_code": acc, "side": side, "amount": amt, "decision": "AUTO", "candidate_rule_ids": []} + + def side(line, accs, amt, entries): + return {"article_line": line, "accounts": accs, "amount": amt, "entries": entries} + + def bal(dt, kt, a1, a2): + from decimal import Decimal as Dd + dt, kt, a1, a2 = Dd(dt), Dd(kt), Dd(a1), Dd(a2) + diff = abs(dt - kt) + return {"debit_total": f"{dt:.2f}", "credit_total": f"{kt:.2f}", "difference": f"{diff:.2f}", + "difference_percent": f"{diff / min(dt, kt) * 100:.2f}" if min(dt, kt) > 0 else None, + "remainder_1": f"{a1 - dt:.2f}", "remainder_2": f"{a2 - kt:.2f}"} corr_ba = {"status": "APPLIED", "version_no": 1, "debit": [{"line_code": "1230", "amount": "10000000.00"}], "credit": [{"line_code": "1520", "amount": "10000000.00"}], "comment": "Сверка Бета → Альфа: разница — аванс, исключаем подтверждённое", "reason_code": None, - "updated_by": USER, "updated_at": "2025-11-12T14:30:00Z"} + "balance": bal("10000000", "10000000", "10000000", "50000000"), + "updated_by": REFU, "updated_at": "2025-11-12T14:30:00Z"} corr_ac = {"status": "APPLIED", "version_no": 2, "debit": [{"line_code": "1230", "amount": "20000000.00"}, {"line_code": "1170", "amount": "5000000.00"}], "credit": [{"line_code": "1520", "amount": "22500000.00"}], "comment": "Часть долга Гаммы переведена в займ — разнесено по статьям", "reason_code": None, - "updated_by": USER, "updated_at": "2025-11-13T09:10:00Z"} + "balance": bal("25000000", "22500000", "30000000", "29100000"), + "updated_by": REFU, "updated_at": "2025-11-13T09:10:00Z"} + + def agg(rule_id, rule_no, c1, c2, s1, s2, dev, pct, exc, status, elim, corr): + return {"rule_id": rule_id, "rule_no": rule_no, "company_1": REF[c1], "company_2": REF[c2], "side_1": s1, + "side_2": s2, "pairs": [], "deviation_amount": dev, "deviation_percent": pct, + "exceeds_threshold": exc, "status": status, "eliminated_amount": elim, "composition_changed": False, + "correction": corr} + ab_1 = side("1230", ["62.01"], "102000000.00", [entry("A", "B", "62.01", "D", "102000000.00")]) + ab_2 = side("1520", ["60.01"], "100000400.50", [entry("B", "A", "60.01", "K", "100000400.50")]) + a_ab = agg(R6, 6, "A", "B", ab_1, ab_2, "1999599.50", "2.00", False, "AUTO", "100000400.50", None) + a_ab["pairs"] = [{"side_1": ab_1, "side_2": ab_2, "deviation_amount": "1999599.50", "deviation_percent": "2.00"}] aggs = [ - {"rule_id": "66666666-6666-4666-8666-000000000006", "rule_no": 6, "company_1": REF["A"], "company_2": REF["B"], - "side_1": {"article_line": "1230", "accounts": ["62.01"], "amount": "102000000.00", - "entries": [entry("A", "B", "62.01", "D", "102000000.00")]}, - "side_2": {"article_line": "1520", "accounts": ["60.01"], "amount": "100000400.50", - "entries": [entry("B", "A", "60.01", "K", "100000400.50")]}, - "deviation_amount": "1999599.50", "deviation_percent": "2.00", "exceeds_threshold": False, "status": "AUTO", - "eliminated_amount": "100000400.50", "composition_changed": False, "correction": None}, - {"rule_id": "66666666-6666-4666-8666-000000000006", "rule_no": 6, "company_1": REF["B"], "company_2": REF["A"], - "side_1": {"article_line": "1230", "accounts": ["62.01"], "amount": "10000000.00", - "entries": [entry("B", "A", "62.01", "D", "10000000.00")]}, - "side_2": {"article_line": "1520", "accounts": ["60.01", "76.05"], "amount": "50000000.00", - "entries": [entry("A", "B", "60.01", "K", "12000000.00"), entry("A", "B", "76.05", "K", "38000000.00")]}, - "deviation_amount": "40000000.00", "deviation_percent": "400.00", "exceeds_threshold": True, - "status": "DISCREPANCY", "eliminated_amount": "0.00", "composition_changed": False, "correction": corr_ba}, + agg(R2, 2, "A", "B", side("1170", ["58.01"], "50000000.00", [entry("A", "B", "58.01", "D", "50000000.00")]), + side("1310/1350/1370", ["80"], "40000000.00", [entry("B", "A", "80", "K", "40000000.00")]), + "10000000.00", "25.00", False, "AUTO", "40000000.00", None), + agg(R2, 2, "A", "C", side("1170", ["58.01"], "15000000.00", [entry("A", "C", "58.01", "D", "15000000.00")]), + side("1310/1350/1370", [], "0.00", []), "15000000.00", None, False, "ONE_SIDED", "15000000.00", None), + a_ab, + agg(R6, 6, "A", "C", side("1230", ["62.01"], "30000000.00", [entry("A", "C", "62.01", "D", "30000000.00")]), + side("1520", ["60.01"], "29100000.00", [entry("C", "A", "60.01", "K", "29100000.00")]), + "900000.00", "3.09", False, "AUTO", "29100000.00", corr_ac), + agg(R6, 6, "B", "A", side("1230", ["62.01"], "10000000.00", [entry("B", "A", "62.01", "D", "10000000.00")]), + side("1520", ["60.01", "76.05"], "50000000.00", + [entry("A", "B", "60.01", "K", "12000000.00"), entry("A", "B", "76.05", "K", "38000000.00")]), + "40000000.00", "400.00", True, "DISCREPANCY", "0.00", corr_ba), ] - vgo = {"balance": {"version_id": kfo.VERSION, "statement_type": "balance", "rules_version_no": 1, "rules": [], - "aggregates": aggs, - "unallocated": [{"entry_ids": [], "owner": REF["C"], "counterparty": REF["A"], + rule2 = {"id": R2, "rule_no": 2, "article_1_line": "1170", "article_2_line": "1310/1350/1370", "side_1": "D", + "measure": "closing", "company_1_accounts": ["58.01"], "company_2_accounts": ["80", "83"], + "company_1_excluded": [], "company_2_excluded": []} + rule6 = {"id": R6, "rule_no": 6, "article_1_line": "1230", "article_2_line": "1520", "side_1": "D", + "measure": "closing", + "company_1_accounts": ["60.02", "60.22", "60.32", "62.01", "62.03", "62.21", "62.31", "76"], + "company_2_accounts": ["60.01", "60.03", "60.21", "60.31", "62.02", "62.22", "62.32", "76"], + "company_1_excluded": ["76.7.5", "76.7.9"], "company_2_excluded": ["76.7.1"]} + vgo = {"balance": {"version_id": kfo.VERSION, "statement_type": "balance", "rules_version_no": 1, + "rules": [rule2, rule6], "aggregates": aggs, + "unallocated": [{"entry_ids": [next(eid)], "owner": REF["C"], "counterparty": REF["A"], "account_code": "76.09", "line_code": None, "side": "D", "amount": "350000.00", - "candidate_rule_ids": [], "reason": "MULTIPLE_RULES", "excluded": True}], - "totals": {"amount_1": "112000000.00", "amount_2": "150000400.50", - "deviation_amount": "41999599.50", "correction_debit": "10000000.00", - "correction_credit": "10000000.00"}}, + "candidate_rule_ids": [R2, R6], "reason": "MULTIPLE_RULES", "excluded": True}], + "totals": {"amount_1": "207000000.00", "amount_2": "219100400.50", + "deviation_amount": "67899599.50", "correction_debit": "35000000.00", + "correction_credit": "32500000.00"}}, "income": None} out["vgo_pairs"] = env("vgo_pairs", vgo) out["corrections"] = env("corrections", {"items": [ - {"statement_type": "balance", "rule_no": 6, "company_1": REF["B"], "company_2": REF["A"], "correction": corr_ba}, {"statement_type": "balance", "rule_no": 6, "company_1": REF["A"], "company_2": REF["C"], "correction": corr_ac}, + {"statement_type": "balance", "rule_no": 6, "company_1": REF["B"], "company_2": REF["A"], "correction": corr_ba}, ]}) def row(c, acc, cp, ctype, cd=None, cc=None): - return {"entry_ids": [], "legal_entity_id": IDS[c], "legal_entity_name": kfo.ex.NAMES[c], + return {"entry_ids": [next(eid)], "legal_entity_id": IDS[c], "legal_entity_name": kfo.ex.NAMES[c], "legal_entity_inn": kfo.ex.INN[c], "account_code": acc, "account_root": acc.split(".")[0], "period": "2025 Q3", "is_cumulative": False, "row_number": None, "counterparty_name": kfo.ex.NAMES.get(cp, cp), "counterparty_inn": kfo.ex.INN.get(cp), @@ -285,16 +344,22 @@ def examples(): "rows": [row("A", "62.01", "B", "GSK", cd="102000000.00"), row("B", "60.01", "A", "GSK", cc="100000400.50"), ext], "summary": {"rows_total": 3, "unmapped_total": 0, "auto_matched_total": 0, "documents_total": 9, "gaps": [{"legal_entity_id": IDS["C"], "legal_entity_name": kfo.ex.NAMES["C"], - "account_code": "76", "missing_quarters": [1]}], "counter_pairs": []}}) + "account_code": "76", "missing_quarters": ["Q1"]}], "counter_pairs": []}}) cell = {"company_a_id": IDS["A"], "company_b_id": IDS["B"], "amount_a": "102000000.00", "amount_b": "100000400.50", "variance_amount": "1999599.50", "variance_percent": "2.00", "exceeds_threshold": False, "one_sided": False, "skipped_osv_entry_count": 0} - matrix = {"companies": [REF[c] for c in kfo.ex.COMPANIES], "cells": [cell]} - out["chess"] = env("chess", {"matrix_all": matrix, "presets": [{"rule_no": 6, "matrix": matrix}], - "result_table": {"companies": [REF[c] for c in kfo.ex.COMPANIES], "articles": [ - {"code": "1230", "name": "Дебиторская задолженность", "group_total": "112000000.00", - "amounts": [{"company_id": IDS["A"], "amount": "102000000.00"}, - {"company_id": IDS["B"], "amount": "10000000.00"}]}]}}) + tot_d = {IDS["A"]: "102000000.00", IDS["B"]: "0.00", IDS["C"]: "0.00"} + tot_k = {IDS["A"]: "0.00", IDS["B"]: "100000400.50", IDS["C"]: "0.00"} + matrix = {"companies": [REF[c] for c in kfo.ex.COMPANIES], "cells": [cell], "total_debit": tot_d, + "total_credit": tot_k} + out["chess"] = env("chess", { + "matrix_all": matrix, + "presets": [{"rule_no": 6, "statement_type": "balance", "rules_version_no": 1, "rule": rule6, "matrix": matrix}], + "result_table": {"companies": [REF[c] for c in kfo.ex.COMPANIES], "articles": [ + {"code": "1230", "name": "Дебиторская задолженность", "group_total": "142000000.00", + "amounts": [{"company_id": IDS["A"], "amount": "132000000.00"}, + {"company_id": IDS["B"], "amount": "10000000.00"}, + {"company_id": IDS["C"], "amount": "0.00"}]}]}}) gw_tot = {"financial_investment_amount": "50000000.00", "charter_capital_amount": "40000000.00", "participation_amount": "32000000.00", "goodwill_amount": "18000000.00", "goodwill_in_kfo_amount": "18000000.00"} @@ -305,12 +370,12 @@ def examples(): "additional_capital_amount": "0.00", "participation_amount": "32000000.00", "goodwill_amount": "18000000.00", "include_in_kfo": True, "comment": "Доля 80 % по выписке ЕГРЮЛ на 01.10.2025", "has_investments": True, "warnings": [], - "updated_by": USER, "updated_at": "2025-11-12T11:00:00Z"}], + "updated_by": REFU, "updated_at": "2025-11-12T11:00:00Z"}], "groups": [{"head_legal_entity": REF["A"], "totals": gw_tot}], "totals": gw_tot}) out["version_card"] = env("version_card", { "company_group": {"id": "77777777-7777-4777-8777-777777777777", "name": "ГК «Альфа»"}, "related_group": {"id": "88888888-8888-4888-8888-888888888888", "name": "ГСК «Альфа»"}, - "reporting_date": "2025-10-01", "year": 2025, "quarter": 3, "version_no": 1, "scenario": "CREATE", + "reporting_date": "2025-10-01", "year": 2025, "quarter": "Q3", "version_no": 1, "scenario": "CREATE", "approach": "BASIC", "goodwill_enabled": True, "initiator": USER, "initiator_office": {"id": "99999999-0000-4000-8000-000000000001", "name": "Ростовский РФ"}, "members": [{"legal_entity_id": IDS[c], "name": kfo.ex.NAMES[c], "inn": kfo.ex.INN[c], diff --git a/scripts/kfo_contract/test_ex_payload.py b/scripts/kfo_contract/test_ex_payload.py index 34bd786..af0b01f 100644 --- a/scripts/kfo_contract/test_ex_payload.py +++ b/scripts/kfo_contract/test_ex_payload.py @@ -10,6 +10,8 @@ from pathlib import Path from jsonschema import Draft202012Validator ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "scripts")) +from validate_contracts import make_format_checker # noqa: E402 — та же проверка форматов, что в make check SCHEMA = ROOT / "contracts" / "ex-payload-v1.schema.json" EXAMPLES = sorted((ROOT / "contracts" / "examples").glob("ex-payload-*.json")) REQUIRED_TYPES = {"summary_osv", "vgo_pairs", "chess", "kfo_balance", "kfo_income", "goodwill", "corrections", @@ -19,7 +21,7 @@ REQUIRED_TYPES = {"summary_osv", "vgo_pairs", "chess", "kfo_balance", "kfo_incom def main(): schema = json.loads(SCHEMA.read_text(encoding="utf-8")) Draft202012Validator.check_schema(schema) - v = Draft202012Validator(schema) + v = Draft202012Validator(schema, format_checker=make_format_checker()) by_type = {} for p in EXAMPLES: doc = json.loads(p.read_text(encoding="utf-8")) @@ -55,7 +57,50 @@ def main(): bad = copy.deepcopy(by_type["summary_osv"]) bad["units"] = "thousand_rub" rejected(bad, "summary_osv в тысячах") - print(f"OK: примеров {len(EXAMPLES)}, отрицательных случаев 6") + # ревью #208: легитимные ответы API проходят + ok = copy.deepcopy(by_type["summary_osv"]) + ok["data"]["summary"]["gaps"][0]["missing_quarters"] = ["Q1", "Q2"] + assert not list(v.iter_errors(ok)), "C-1: пробелы свода как в API (Q1…Q4)" + ok = copy.deepcopy(by_type["chess"]) + ok["data"]["matrix_all"]["cells"][0]["variance_percent"] = "10199959.95" + assert not list(v.iter_errors(ok)), "C-2: большой процент шахматки" + ok = copy.deepcopy(by_type["vgo_pairs"]) + ok["data"]["balance"]["aggregates"][0]["deviation_percent"] = "10199959.95" + assert not list(v.iter_errors(ok)), "C-2: большой процент агрегата" + # ревью #208: то, что нужно выгрузкам, обязательно + for path, why in [(("chess", "matrix_all", "total_debit"), "I-1 итоги шахматки"), + (("vgo_pairs", "balance", "rules"), "I-1 правила таблицы корректировок"), + (("chess", "presets", 0, "rule"), "I-1 правило пресета")]: + bad = copy.deepcopy(by_type[path[0]]) + node = bad["data"] + for k in path[1:-1]: + node = node[k] + del node[path[-1]] + rejected(bad, why) + bad = copy.deepcopy(by_type["corrections"]) + del bad["data"]["items"][0]["correction"]["balance"] + rejected(bad, "I-1 balance корректировки") + bad = copy.deepcopy(by_type["vgo_pairs"]) + bad["data"]["balance"]["aggregates"][0]["side_1"]["entries"][0]["entry_ids"] = [] + rejected(bad, "I-1 строка агрегата без entry_ids") + bad = copy.deepcopy(by_type["corrections"]) + bad["data"]["items"][0]["correction"]["updated_by"] = {"user_id": "x", "full_name": "y"} + rejected(bad, "I-2 updated_by не Ref") + # ревью #208: пустой и незамороженный payload + bad = copy.deepcopy(by_type["version_card"]) + bad["data"]["members"] = [] + rejected(bad, "M-1 карточка без состава") + bad = copy.deepcopy(by_type["kfo_balance"]) + bad["data"]["is_frozen"] = False + rejected(bad, "M-1 КФО не заморожена") + bad = copy.deepcopy(by_type["version_card"]) + bad["data"]["quarter"] = 3 + rejected(bad, "M-3 квартал числом") + # форматы проверяются + bad = copy.deepcopy(by_type["version_card"]) + bad["data"]["verified_at"] = "вчера" + rejected(bad, "M-6 date-time") + print(f"OK: примеров {len(EXAMPLES)}, отрицательных случаев 17, положительных граничных 3") return 0