19 Commits

Author SHA1 Message Date
Andrey Epifancev
b4522addc0 feat: DBML v5.23 — версия правил обязательна; список пар ВГО версии (ревью #175, AURORA-1536)
- consolidation_version.elimination_rules_version NOT NULL: миграция 0010
  проставляет существующим версиям номер правил, действовавших на дату
  создания (по Москве), иначе последних открытых; без правил — отказ;
  создание версии без действующих правил — 409 ELIMINATION_RULES_MISSING;
  ветки «правила не зафиксированы» убраны (rules, preset, is_version_fixed
  всегда true);
- GET …/elimination-totals: все тройки «правило ББ × упорядоченная пара
  периметра» с ΣA, ΣB, расхождением, суммой элиминации, статусом
  AUTO/DISCREPANCY/ONE_SIDED/MANUAL и osv_entry_ids; сводная ОСВ строится
  один раз; сбор строк сторон вынесен в _collect_rule_sides — общий
  с упаковкой тройки.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:42:10 +04:00
Andrey Epifancev
7dd7d5dd40 test: golden fixtures контракта 2.0 в тестах ядра (#144)
fixtures #26/#27 (19.10.2026) перевели снимки форм и OCR на контракт 2.0,
а загрузчики тестов читали их моделью 1.x: 164 падения на main.

- golden_result/ocr_result: модель по MAJOR schema_version, для 2.0 —
  service.statements_version копии пакета ядра;
- golden_result_v1: снимок 2.0 → 1.2 (строки с кодом, колонки с 0, без
  null в шапке) для модулей приёма 1.x — test_mapping_statements и
  test_api_statements; путь 2.0 покрывают *_v2;
- make_entries_from_result — parse_result; ожидания fixtures_marker под
  эталоны 2.0; удаление файла считает замечания сервиса снимка;
- 76.Н.1 (synthetic s42, tabular-parser#59) — в KNOWN_OUT_OF_PLAN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:34:20 +04:00
Andrey Epifancev
2db0a66a86 feat(models): DBML v5.19 — колонки форм, document_issue, file_ordinal, crm_id ГСК (миграция 0007)
Модели и upgrade ревизии 0007_dbml_v5_19: пять enum'ов, колонки строк/значений/колонок/шапок форм,
document.file_ordinal (уникален в файле, уникальность типа только для не-форм), recognition_job.statements_version,
related_group.crm_id, таблица document_issue; заполнение по умолчанию (row_index, needs_review, header_source и др.).
delete_file_statements удаляет document_issue; make_document берёт следующий file_ordinal; маппер пока нумерует
документы по порядку создания. downgrade — заглушка (задача A2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
2026-10-18 19:04:40 +04:00
Andrey Epifancev
5ae8ff8a81 nats: subject'ы как в старом АРМ — files.*_uploaded, files.tabular_parsed (#77)
Решение владельца 08.10.2026: recognition.tabular.request → files.tabular_uploaded,
recognition.image.request → files.img_uploaded, recognition.result →
files.tabular_parsed (общий subject ack и результатов; files.img_parsed не
используется). Тела сообщений, Nats-Msg-Id, queue group'ы и schema_version не
меняются. Константы api/src/contract/subjects.py, докстринги, тесты, README,
CLAUDE.md; копии схем api/contracts/ из docs/contracts/ побайтно; change
nats-files-subjects с дельтой спеки. Сквозной change — RSHB/arm-ka-docs#74.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-08 23:46:18 +04:00
Andrey Epifancev
e317fe59dd fix(consolidation): ревью Task 4 — общий lock_out, точнее тесты, решения #17–#24 в design.md
- I-1 (T4.1): lock_out — публичный в services/consolidations.py, копия в consolidation_locks удалена
- M-b (T4.3): удалён осиротевший комментарий в tests/factories.py
- M-c (T4.4): точная проверка ip_address/user_agent в аудите смены статуса
- M-d (T4.5): docstring теста Review Focus 4 ссылается на различающий тест порядка D-9
- design.md: решения без обсуждения #17 (R-8), #18 (R-9), #19–#24 (решения Task 4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 04:26:31 +04:00
Andrey Epifancev
b2fa59061d feat(consolidation): политика редактируемости, блокировка с перехватом, ручные статусы Среза 4
- domain/policy: IN_WORK_STATUSES, ensure_version_editable (VERSION_NOT_EDITABLE / LOCK_REQUIRED / LOCKED_BY_OTHER)
- services/consolidation_locks: take/release, перехват с причиной, release_active_lock; FOR UPDATE версии первой (D-9),
  вставка под SAVEPOINT, нарушение uq_consolidation_lock_version_active -> 409 LOCKED_BY_OTHER с владельцем
- services/consolidation_status: MANUAL_TRANSITIONS, transition (единственное место смены статуса: история + аудит,
  CANCELLED снимает блокировку), ConsolidationStatusService.change
- маршруты POST/DELETE …/lock, POST …/status; реестр constraint_conflicts
- тесты: таблица ensure_version_editable, сценарии спеки, гонка двух POST …/lock, перехват во время правки и
  аннулирования (две сессии, Review Focus 4)
- минорные T3.1 (FINISHED_STATUSES из policy в фабриках), T3.2 (удалена CONSOLIDATION_NOT_FOUND)
- план: шаги Task 4; tasks.md 2.2, 2.3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 04:16:23 +04:00
Andrey Epifancev
9411d9d2ae feat(consolidation): создание консолидации, список по ГК, карточка версии
POST /consolidations (версия №1 INITIATED, периметр из текущих членов ГСК, история,
CONSOLIDATION_CREATE с consolidation_version_id), GET /company-groups/{id}/consolidations
и GET /consolidations/versions/{id}. reporting_date по OQ-40; подхват документов — заглушка
bind_confirmed до Task 6, чек-лист в карточке пуст до Task 8. UNIQUE периода -> 409
CONSOLIDATION_EXISTS; RequestAudit.record принимает consolidation_version_id.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 03:57:52 +04:00
Andrey Epifancev
d3c37db197 feat(consolidation): репозиторий, схемы и фабрики консолидаций
Репозиторий консолидаций (get_version/list_periods/lock_versions_for_entity с блокировкой
надмножества версий по R-2, load_osv_rows, entity_files_state), схемы API (D-16),
domain/checklist.EntityFilesState (R-5), фабрики make_consolidation/make_lock/make_confirmed_osv
и очистка таблиц консолидации в CommittedJobs.cleanup().

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 03:46:15 +04:00
Andrey Epifancev
2ac19c6953 fix(files): DELETE ставит expires_at только объектам папок своего файла
M-3: s3_object.expires_at = now ставится только объектам с ролью uploads и ключом
uploads/{file_id}/… или с ролью results и ключом results/{file_id}/…; ссылка
задания на объект другого файла (inbox переиспользует s3_object при конфликте
(bucket, key)) не трогается и ошибкой не считается.

Фабрика make_stored_file кладёт загрузку в папку файла uploads/{file_id}/…, как
upload_key в проде.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 00:35:43 +04:00
Andrey Epifancev
20983fb3d8 feat(files): API файлов, документов и строк ОСВ; подтверждение документа
GET /legal-entities/{id}/files (include_superseded, по убыванию uploaded_at),
GET /files/{id}, GET /documents/{id} (group_schema листа, warnings — R-6),
GET /documents/{id}/entries (limit ≤ 2000, по умолчанию 500),
POST /documents/{id}/confirm (DOCUMENT_EDIT; блокировка файла, затем документа;
409 FILE_SUPERSEDED / ALREADY_CONFIRMED / DOCUMENT_NOT_CONFIRMABLE; аудит
DOCUMENT_CONFIRM). Видимость — через ЮЛ-владельца, чужое — 404 как несуществующее.
Список файлов без N+1: файлы, текущие задания и документы — пакетными запросами.
JobBrief.mapping_error — причина ERROR файла по маппингу.

Тесты: сценарии documents-api, гонка подтверждения с автозаменой (Review Focus 5),
make_entries_from_result (R-2), реальная проверка уникальности документа (T2.2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 23:34:35 +04:00
Andrey Epifancev
3b4d3a45fe fix(exchange): INVALID_REQUEST публикатора переводит файл в ERROR
T4.1: публикатор, переводя задание в финальный error/INVALID_REQUEST, теперь в той же транзакции (под тем же
FOR UPDATE) ставит файлу текущего задания ERROR (spec primary-files «Жизненный цикл статуса файла»); файл
чужого задания не меняется. Тест test_invalid_request_marks_job_error_and_pass_continues[current|non_current];
CommittedJobs.make_file_job принимает original_name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 22:13:58 +04:00
Andrey Epifancev
b7ca7285cc feat(exchange): статусы stored_file по ack/ошибке/таймауту/dlq; inbox без ResultMapper (OQ-67)
- services/file_status.py: set_file_status_for_job(session, job, status, *, now, only_from=None) и пакетный
  set_file_status_for_jobs для вахтёра; переход только при current_recognition_job_id = job.id, PROCESSED и
  SUPERSEDED не перезаписываются (D-5, R-5).
- inbox: ack -> файл UPLOADED->PROCESSING; error -> ERROR; done/needs_check -> UPLOADED->PROCESSING (R-4: ack мог
  потеряться), из PROCESSING файл ждёт цикла маппинга; inbox не читает S3 и не создаёт документы.
- вахтёр: UPDATE recognition_job ... RETURNING id и UPDATE stored_file -> ERROR в той же транзакции;
  публикатор при dlq -> файл ERROR.
- удалены ResultMapper, NoopMapper, _map_result, _MapperError и параметр mapper у handle_message; app.py без mapper.
- тесты: статусы файла (ack/error/done/needs_check/без ack/timeout/dlq), чужое задание файл не трогает (Review
  Focus 1), откат статуса файла вместе с транзакцией сообщения; CommittedJobs.make_file_job/add_job/file.
- tasks.md 2.2, шаги Task 4 в плане.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 22:08:40 +04:00
Andrey Epifancev
2d69a9e867 chore: S3 get_bytes, протокол ObjectStorage, фабрики файлов/документов, настройки загрузки и маппинга
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 21:35:41 +04:00
Andrey Epifancev
8eb8592452 test: фабрики справочников, фикстуры app/auth_client/seeded
Фабрики ГК/ЮЛ/ГСК/членства/пользователя (flush, без commit), фикстуры seeded/app/auth_client
в conftest (override сессии через begin_nested), test_me_and_dev_token переведён на общие
фикстуры (сочетания флагов остались на локальном _build_app), test_factories.py проверяет фабрики.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-04 21:10:41 +04:00
Andrey Epifancev
8d7d014391 fix: nullable jsonb пишет SQL NULL вместо jsonb null (I-1)
Все nullable jsonb-колонки ORM (audit_event.before_value/after_value,
ex_snapshot_item.payload, recognition_job.group_schema/metrics,
osv_entry.original_values) объявлены через nullable_jsonb() =
JSONB(none_as_null=True): Python None сохраняется как SQL NULL, IS NULL
находит строку. DDL не меняется; сверка с DBML и миграции зелёные.

Обходы для не-объекта в metrics (CASE по jsonb_typeof в публикаторе,
merge_metrics в inbox) оставлены: строки до исправления и сырой SQL
могут содержать jsonb null. Тесты: SQL NULL после AuditService.record
без before/after, metrics фабрики None -> SQL NULL, повтор публикации и
слияние метрик для SQL NULL и jsonb null, перечень колонок с флагом.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 01:49:26 +04:00
Andrey Epifancev
9b81fa8e37 feat: inbox recognition.result — ack processing, идемпотентный приём результата, s3_object результата, ResultMapper/NoopMapper
handle_message(msg, *, session_factory, mapper, now): разбор конверта по контракту v1.0, одна короткая транзакция
с SELECT … FOR UPDATE (ждёт блокировку публикатора/вахтёра); ack published → processing; результат — дедуп по
result_msg_id, финальные статусы (R-22) не трогаются, регистрация s3_object (results), слияние метрик с сохранением
republish_count. Подписка app.py получает handler с фабрикой сессий и NoopMapper; шаблонный core/nats_callback.py
удалён (R-3). audit_event не пишется (R-7).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 00:46:48 +04:00
Andrey Epifancev
2e96971475 feat: outbox-публикатор и вахтёр сроков recognition_job, фоновые задачи обмена в lifespan при NATS_ENABLED
- services/recognition_exchange.py: publish_pending — одно задание на короткую транзакцию
  (SELECT … LIMIT 1 FOR UPDATE OF recognition_job SKIP LOCKED → публикация → UPDATE → COMMIT):
  queued → published (request_msg_id = <job_id>:<attempt>, request_subject, published_at, reply_deadline —
  5 мин по умолчанию, requested_at); published без ack дольше ACK_TIMEOUT → повтор с тем же Nats-Msg-Id и
  attempt, metrics.republish_count (jsonb ||, прочие ключи сохраняются); после OUTBOX_MAX_REPUBLISH → dlq/NO_ACK;
  запрос не собирается по контракту → error/INVALID_REQUEST; первая NatsPublishError откатывает задание и
  прерывает проход; нет соединения — проход пропускается. published с прошедшим reply_deadline оставлен вахтёру.
- expire_deadlines — один UPDATE processing/published с reply_deadline < now → timeout/TIMEOUT.
- run_publisher_loop / run_watchdog_loop (30 с), ExchangeTasks: запуск в lifespan вместе с NatsConnection,
  остановка до nats.stop(). audit_event не пишется (R-7).
- Тесты: интеграционные против PostgreSQL и NATS (схема запроса, повторы и dlq на инжектируемых часах, ack после
  первого повтора, вахтёр, ошибка публикации, два конкурентных прохода, циклы), lifespan; фабрика CommittedJobs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 00:20:06 +04:00
Andrey Epifancev
c8c218e4ac test: сужен перехват ошибок PostgreSQL в conftest, самодостаточные тесты изоляции транзакции
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 20:54:30 +04:00
Andrey Epifancev
c3cf307866 test: conftest — БД сессии с миграциями, транзакция с откатом на тест, маркер fixtures, тест чтения Decimal
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 20:46:50 +04:00