docs(context): Срез 3b — правки по финальному ревью arm-core (#48)
inn=null — 422, удаление файла удаляет формы и комментарии, заголовки исходного файла (no-store, CSP sandbox), повторы колонок и пар, присутствие цели §7, ссылки на решения №1–22. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
d82c7f9a2d
commit
4287d583e6
@ -461,7 +461,7 @@ variance = |amount_a − amount_b|; variance_pct = variance / min(amount_a, amou
|
||||
словаря tabular-parser (те же жёсткие и мягкие соотношения, допуск ±1 в единицах формы, округление 0,001 %, `abs_terms`,
|
||||
мягкая «сумма пятизначных подстрок = родитель»); тест идентичности копий между репозиториями не ставится, расхождение —
|
||||
известный риск (`03-architecture.md`). Соотношение считается по каждой колонке и только если присутствует его цель
|
||||
(левая часть); отсутствующий код в правой части = 0. Результат в ответе API: `failed` (жёсткие), `soft_failed` (мягкие, в
|
||||
(левая часть — сумма в этой колонке непуста; пустая ячейка цели соотношение пропускает); отсутствующий код в правой части = 0. Результат в ответе API: `failed` (жёсткие), `soft_failed` (мягкие, в
|
||||
том числе «в пределах округления»), `cross_form`, `accepted`.
|
||||
Межформенная `1370(отчётная дата) − 1370(начало года) = 2400` — **мягкая** и не блокирует подтверждение: пара — другой
|
||||
документ формы того же ЮЛ с тем же годом и кварталом периода (файл не `SUPERSEDED`; подтверждённый предпочтительнее, далее
|
||||
@ -601,7 +601,8 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
`financial_statement_line` (строки с кодом, `(meta_id, line_code)` уникален: повтор кода — остаётся первая строка,
|
||||
предупреждение) + `financial_statement_value` — **на каждую пару «строка × колонка» одна запись** (ячейки, которых нет
|
||||
в результате, — с пустыми суммой и уверенностью; значение с `column_index` вне колонок формы пропускается с
|
||||
предупреждением). `checks` результата ядро игнорирует: §7 считает само (§7). **Период формы:** дата = `header.report_date`,
|
||||
предупреждением; повтор `column_index` в колонках результата или повтор пары «строка × колонка» — остаётся первое, пишется
|
||||
предупреждение, документ `NEEDS_CHECK`). `checks` результата ядро игнорирует: §7 считает само (§7). **Период формы:** дата = `header.report_date`,
|
||||
а если даты нет — **самая поздняя `period_date` колонок** (дата отчёта в OCR часто мусор; колонки надёжнее); `period_year` —
|
||||
год даты, `period_quarter` — только если дата — последний день марта, июня, сентября или декабря, иначе квартал пуст и
|
||||
предупреждение «дата … не конец квартала»; без даты год и квартал пусты. У колонки с самой поздней датой `is_current = true`.
|
||||
@ -634,7 +635,7 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
как у ОСВ (§3 п. 1, §9.1): ББ/ОФР за квартал версии (`Q1` — только версии за `Q1`, R-14a Среза 4).
|
||||
5. **Удаление** (`DELETE /files/{id}`, `DOCUMENT_EDIT`): запрещено при `CONFIRMED` документе
|
||||
(`FILE_HAS_CONFIRMED_DOCUMENTS`) и если файл заменил другие (`FILE_SUPERSEDES_OTHERS`); `SUPERSEDED` файл удалить
|
||||
можно. Удаляются строки, документы, задания и `stored_file`; объектам S3 ставится `expires_at = now()` (физически не
|
||||
можно. Удаляются строки, документы (вместе с данными форм `financial_statement_*` и комментариями документов), задания и `stored_file`; объектам S3 ставится `expires_at = now()` (физически не
|
||||
стираются — OQ-111); аудит `FILE_DELETE` с карточкой файла в `before_value`. **OQ-110 закрыт для Среза 4:** к версии
|
||||
привязываются только `CONFIRMED` документы, а файл с таким документом удалить нельзя — удалить файл с документом в
|
||||
версии невозможно; расширять удаление не требуется.
|
||||
@ -653,7 +654,7 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
нужна.
|
||||
9. **Правка формы ББ/ОФР** (`PATCH /documents/{id}/statement`, `DOCUMENT_EDIT`, Срез 3b; решения владельца 07.10.2026).
|
||||
Правятся **суммы** ячеек (`values[] {line_id, column_id, amount}`) и три поля шапки: `inn` (10 или 12 цифр, иначе 422
|
||||
`INVALID_INN`; пересчитывается `counterparty_check`), `report_date` (пересчитываются `period_year`, `period_quarter`,
|
||||
`INVALID_INN`, в том числе `null` — очистить ИНН нельзя; пересчитывается `counterparty_check`), `report_date` (пересчитываются `period_year`, `period_quarter`,
|
||||
`reporting_date` по правилу периода п. 2) и `units` (непустая строка до 50 символов). Код и название строки, добавление и
|
||||
удаление строк и колонок не правятся. **Только до подтверждения:** документ `CONFIRMED` — 409 `DOCUMENT_READONLY`
|
||||
(исправление подтверждённой формы — новым файлом через автозамену, п. 3); файл `SUPERSEDED` — 409 `FILE_SUPERSEDED`; не
|
||||
@ -666,10 +667,10 @@ replay: dlq → queued (`attempt + 1`). `max_attempts`: OCR 1, парсер 3; `
|
||||
документ).
|
||||
10. **Исходный файл и комментарии (Срез 3b).** `GET /files/{id}/content` (`DOCUMENT_VIEW`, policy по ЮЛ) отдаёт исходный
|
||||
объект из S3 потоком через ядро (в контуре MinIO для пользователя может быть недоступен, а через ядро действует
|
||||
видимость); работает и для `SUPERSEDED`; объект истёк или пропал — 404 `FILE_CONTENT_NOT_FOUND`. Комментарии к документу —
|
||||
видимость); работает и для `SUPERSEDED`; объекта нет в хранилище — 404 `FILE_CONTENT_NOT_FOUND`. Комментарии к документу —
|
||||
`POST/GET /documents/{id}/comments` (таблица `comment`, `object_type = 'document'`); добавлять можно к документу в любом
|
||||
статусе; правки и удаления нет; при удалении файла (`DELETE /files/{id}`) комментарии его документов остаются в таблице
|
||||
(связь полиморфная, наружу не отдаются).
|
||||
статусе; правки и удаления нет; при удалении файла (`DELETE /files/{id}`, п. 5) комментарии его документов удаляются вместе с ними
|
||||
(связь полиморфная, FK нет — удаляет сервис).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -191,7 +191,7 @@ without discussion» №1–№35). Реализация — по `05-domain-rul
|
||||
|
||||
**Статус: бэкенд и API реализованы в arm-core, октябрь 2026; UI экрана «Проверка распознавания» в срез не входит.** Объём по
|
||||
решению владельца 07.10.2026: весь API пути пользователя до сводной ОСВ включительно. Issue arm-core#36, docs#48; спецификация —
|
||||
`arm-core/openspec/changes/slice-3b-statements-api/` (design D-1…D-14, «Decisions made without discussion» №1–№19); документы
|
||||
`arm-core/openspec/changes/slice-3b-statements-api/` (design D-1…D-14, «Decisions made without discussion» №1–№22); документы
|
||||
workspace — bounded-PR в docs (без сквозного change: DBML и контракт не менялись).
|
||||
|
||||
**Сделано (по факту):**
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
Источник — реальный код arm-core (`api/src/schemas/statements.py`, `schemas/comments.py`, `api/endpoints/files/statements.py`,
|
||||
`comments.py`, `read.py`, `users.py`; Swagger — `/docs`) и спека `arm-core/openspec/changes/slice-3b-statements-api/` (design
|
||||
D-1…D-14, «Decisions made without discussion» №1–№19). Правила предметной области — `context/05-domain-rules.md` §7, §9.3, §9.5
|
||||
D-1…D-14, «Decisions made without discussion» №1–№22). Правила предметной области — `context/05-domain-rules.md` §7, §9.3, §9.5
|
||||
(п. 2, 4, 9, 10); экраны — `context/10-user-journey.md` (пути 1–2, шаг 3.1), номера в `[…]` — снимки макета (`mockup/INDEX.md`).
|
||||
Общие правила (префикс, токен, формат ошибок, права) — как в `dev/api-contract-slice-4.md` §1; здесь — то, что добавил Срез 3b.
|
||||
|
||||
@ -74,7 +74,7 @@ D-1…D-14, «Decisions made without discussion» №1–№19). Правила
|
||||
межформенной проверки колонки нет); `accepted` — `{by, at, comment}` (в коде схема `ChecksAcceptedOut`) или `null`.
|
||||
|
||||
**`StatementPatch`** `{header?: {inn?, report_date?, units?}, values?: [{line_id, column_id, amount}]}` — частичное тело, лишние поля
|
||||
запрещены (422). `header.inn` — строка из 10 или 12 цифр, `null` очищает; `header.report_date` — дата, `null` очищает; `header.units` — непустая
|
||||
запрещены (422). `header.inn` — строка из 10 или 12 цифр (любое другое значение, включая `null`, — 422 `INVALID_INN`: ИНН очистить нельзя); `header.report_date` — дата, `null` очищает; `header.units` — непустая
|
||||
строка после обрезки пробелов, ≤ 50 (`null` — «не менять», единицы очистить нельзя; пробельная строка — 422 pydantic-массивом). Поле шапки, которого нет в теле, не меняется. `values` — до 5000 ячеек; одна ячейка дважды в запросе — 422
|
||||
`DUPLICATE_CELL`; `line_id`/`column_id` не этого документа — 422 `UNKNOWN_CELL`. **`ChecksAcceptIn`** `{comment}`.
|
||||
|
||||
@ -124,11 +124,15 @@ expected, actual}` (суммы — строками) внутри `detail`: `{"d
|
||||
(`<a href>` и `<img src>` без заголовка не сработают). `Content-Type` — по типу файла (`application/pdf`, `image/png`,
|
||||
`image/jpeg`, для xls/xlsx/xlsb — свои MIME). `Content-Disposition`: `attachment` (по умолчанию) или `inline` (для просмотра в
|
||||
браузере) с ASCII-именем `file.<ext>` и `filename*=UTF-8''…` — исходным именем в percent-кодировке; берите имя из `filename*`. Ответ несёт
|
||||
`X-Content-Type-Options: nosniff` (браузер не угадывает тип по содержимому; тип задаёт `Content-Type`). Работает и для файла `SUPERSEDED`. Объекта нет в хранилище (убран ретеншном) — 404 `FILE_CONTENT_NOT_FOUND` (не путать с `NOT_FOUND`:
|
||||
`X-Content-Type-Options: nosniff` (браузер не угадывает тип по содержимому; тип задаёт `Content-Type`) и `Cache-Control: private,
|
||||
no-store` (не кэшируйте байты); при `disposition=inline` — ещё `Content-Security-Policy: sandbox` (активное содержимое файла в
|
||||
браузере не исполняется; проверьте, что выбранный просмотрщик PDF работает при таком заголовке). Работает и для файла `SUPERSEDED`. Объекта нет в хранилище (убран ретеншном) — 404 `FILE_CONTENT_NOT_FOUND` (не путать с `NOT_FOUND`:
|
||||
файл в БД есть, а байтов нет); хранилище не подключено — 503 `STORAGE_UNAVAILABLE`. Скачивание в журнал не пишется (OQ-153).
|
||||
13. **Комментарии.** Только добавление и чтение; правки и удаления нет. `created_at` — порядок показа (по возрастанию). `text`
|
||||
обрезается по краям, пустой после обрезки или длиннее 4000 символов — 422 в формате pydantic. Автор — пользователь запроса.
|
||||
`comments_count` в карточках — для бейджа; список перечитывается отдельным запросом.
|
||||
Удаление файла (`DELETE /files/{id}`) удаляет и комментарии его документов, и данные форм ББ/ОФР (`financial_statement_*`):
|
||||
после этого `GET …/comments` и `GET …/statement` по документам файла — 404.
|
||||
14. **Имена пользователей.** `GET /users/brief` нужен, чтобы показать «Загрузил» (`uploaded_by`) и авторов правок
|
||||
(`modified_by`, `checks.accepted.by`, `confirmed_by`) без права `USERS_ADMIN`. Отдаёт только найденные id (включая неактивных,
|
||||
`is_active = false`), упорядоченные по `id`, а не в порядке запроса; неизвестные пропускаются без ошибки; пустой `ids` — `[]`; больше 200 id — 422 `TOO_MANY_IDS`; не-UUID —
|
||||
|
||||
@ -492,12 +492,12 @@ curl -s "localhost:8000/api/v1/documents/$DOC/statement" -H "$H" | python3 -m js
|
||||
| F5 | ka_go | `POST …/statement/checks/accept` `{"comment":"округление в форме клиента"}` | 200; `checks.accepted = {by, at, comment}`, `checks.failed` по-прежнему непуст; статус документа пересчитан (`RECOGNIZED`, если других причин `NEEDS_CHECK` нет); `POST …/confirm` теперь проходит (но F11 делайте после возврата значений) |
|
||||
| F6 | ka_go | снова `PATCH` той же ячейки другой суммой | 200; `checks.accepted = null` (акцепт сброшен правкой) |
|
||||
| F7 | ka_go | `PATCH` той же ячейки обратно на исходную сумму (`original_amount` из F3) | 200; `is_modified = false`, `original_amount` остаётся, `checks.failed` пуст; `PATCH` с пустым телом `{}` — 200, новых записей `RECOGNIZED_DATA_EDIT` нет |
|
||||
| F8 | ka_go | `PATCH` `{"header":{"inn":"123"}}`; `{"values":[{"line_id":"<случайный uuid>","column_id":"<случайный uuid>","amount":"1"}]}` | 422 `INVALID_INN`; 422 `UNKNOWN_CELL`; данные не изменились |
|
||||
| F8 | ka_go | `PATCH` `{"header":{"inn":"123"}}` и `{"header":{"inn":null}}`; `{"values":[{"line_id":"<случайный uuid>","column_id":"<случайный uuid>","amount":"1"}]}` | 422 `INVALID_INN` (оба случая); 422 `UNKNOWN_CELL`; данные не изменились |
|
||||
| F9 | ka_go | `PATCH` `{"header":{"report_date":"2025-06-30"}}`, затем вернуть `"2025-09-30"` | период пересчитывается: `period_quarter = Q2` (`reporting_date = 2025-06-30`), `header.report_date.modified = true`, `original_value` не меняется (после возврата на `2025-09-30` — `Q3`, `modified` остаётся `true`); дата не на конец квартала (`2025-09-15`) — 200, `period_quarter = null`, статус `NEEDS_CHECK`, `POST …/confirm` — 409 `PERIOD_REQUIRED` (поправьте дату — подтверждение проходит) |
|
||||
| F10 | ka_go | `POST …/accept` без нарушений | 409 `NOTHING_TO_ACCEPT` |
|
||||
| F11 | ka_go | `POST …/confirm` для чистой формы (после F7/F9 вернуть исходные значения) | 200 `DocumentOut`, `status = CONFIRMED`, `confirmed_by`/`confirmed_at`; повтор — 409 `ALREADY_CONFIRMED`; `PATCH`/`accept` — 409 `DOCUMENT_READONLY`; в БД у шапки `reviewed_by/at` заполнены |
|
||||
| F12 | ka_go | загрузить `1_Форма2_бух_09.xlsx` тому же ЮЛ, дождаться `PROCESSED`; `GET …/statement` | `document_type = INCOME`, `period_quarter = Q3`, 2 колонки, `edition = fsbu4`; у ББ и ОФР поле `checks.cross_form` — либо `null`, либо нарушение `1370 − 1370(начало года) = 2400` (мягкое, не блокирует подтверждение); статус ОФР — `RECOGNIZED` |
|
||||
| F13 | ka_go | `GET /files/{file_id}/content?disposition=inline` и без параметра | 200, тело байт в байт равно загруженному `.xlsx`, `Content-Type` xlsx; `inline` — `Content-Disposition: inline; filename="file.xlsx"; filename*=UTF-8''<имя>` (кириллица в percent-кодировке); `disposition=foo` — 422 |
|
||||
| F13 | ka_go | `GET /files/{file_id}/content?disposition=inline` и без параметра | 200, тело байт в байт равно загруженному `.xlsx`, `Content-Type` xlsx, заголовки `X-Content-Type-Options: nosniff` и `Cache-Control: private, no-store`; `inline` — ещё `Content-Security-Policy: sandbox`, `Content-Disposition: inline; filename="file.xlsx"; filename*=UTF-8''<имя>` (кириллица в percent-кодировке); `disposition=foo` — 422 |
|
||||
| F14 | ka_go | `POST /documents/{id}/comments` `{"text":"нет подписи руководителя"}`; `{"text":" "}`; затем `GET …/comments` и `GET /files/{file_id}` | 201 с `author.full_name`; пустой после обрезки текст — 422; в списке один комментарий; в карточке файла и `GET /documents/{id}` у документа `comments_count = 1`; комментарий к `CONFIRMED` документу разрешён |
|
||||
| F15 | ka_go | `GET /users/brief?ids=<uploaded_by файла>,<случайный uuid>`; с 201 id; с `ids=abc` | 200, один элемент `{id, full_name, is_active}`; 422 `TOO_MANY_IDS`; 422 `INVALID_IDS`; то же под `expert` (без `USERS_ADMIN`) — 200 |
|
||||
| F16 | ka_go | `GET /directories/legal-entities?q=7016006635` после F1 и F12 | `documents_count = 2` (ББ и ОФР) |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user