Andrey Epifancev b62cec83ba feat(mapping): автозамена прежних версий файла после маппинга (FILE_REPLACE)
`supersede_previous` вызывается в `map_file` после `PROCESSED`, в той же транзакции (D-10):
файлы того же ЮЛ (не SUPERSEDED, не сам новый) с документом тех же type/счёт/год/квартал →
SUPERSEDED, `superseded_by_file_id`, `updated_at`; новому — `version_no` = max + 1;
одно системное событие `FILE_REPLACE` на заменённый файл (user_id NULL, инициатор загрузки,
перечень документов и `confirmed=true|false` в description). Документы и строки заменённых
файлов сохраняются. Автозамены одного ЮЛ сериализуются advisory-блокировкой, заменяемые
файлы блокируются FOR UPDATE в порядке id, документы перечитываются после блокировки.

Тесты: сценарий спеки «Новая ОСВ 60 за 1 кв. 2025» (CONFIRMED и RECOGNIZED), другой
счёт/год/квартал, другое ЮЛ, идемпотентность после сбоя, цепочка v1→v2→v3, одно событие на файл.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 23:06:37 +04:00

7.6 KiB
Raw Blame History

Tasks: Срез 2c — arm-core

Каждая задача ≤ половины дня, начинается с тестов, заканчивается зелёным make check и интеграционными тестами своей области. Допущения в коде — # ASSUMPTION(OQ-nn). DBML v5.15 — ../docs/model/data-model-v5.15.dbml (ветка feat/16-slice-2c-osv-ingest в .wt/slice-2c/docs).

1. Модель и инфраструктура

  • 1.1 DBML v5.15, миграция 0003. OsvDataType.NU (после BU), миграция 0003_dbml_v5_15 (ALTER TYPE … ADD VALUE 'NU' AFTER 'BU', UPDATE app_user SET email = lower(email); downgrade пересоздаёт enum); DBML_VERSION = "5.15", Makefile, dbml_parity.DBML_FILE_NAME, HEAD в test_migrations, test_enums, test_check_dbml_script (фиктивная 5.16); нормализация email в нижний регистр в seed (_apply_users) и в users_admin.create_user/update_user; тесты seed/admin на регистр. Проверка: make dbml-check пуст; make migration-cycle.
  • 1.2 S3 и фабрики. S3Storage.get_bytes(key) -> bytes (+ закрытие response), ObjectStorage-протокол с get_bytes; FakeStorage.get_bytes; tests/factories.py: make_stored_file(session, *, entity, status, content_hash, job_status), make_document, make_entries_from_result(session, file, result_json); CommittedJobs.cleanup() удаляет osv_entry, document. Настройки UPLOAD_MAX_BYTES (50 МиБ), MAPPING_POLL_SECONDS (5), MAPPING_MAX_ATTEMPTS (3) + .env.example/.env.test.

2. Загрузка и статусы

  • 2.1 Загрузка файлов. domain/file_types.py (расширение → FileType/pipeline, проверка сигнатуры), services/primary_files.py (upload_many, дедуп, S3, stored_file/recognition_job/current_recognition_job_id, аудит FILE_UPLOAD), schemas/files.py (UploadResult{accepted,rejected}, FileOut, JobBrief, DocumentBrief), эндпоинт POST /legal-entities/{id}/files, роутер. Тесты: сценарии спеки primary-files (принята, смесь, сигнатура, чужое ЮЛ, дубль); fixtures-тест: реальный xls принят и publish_pending на FakeNats публикует запрос с ключом uploads/{file_id}/{job_id}/….
  • 2.2 Статусы файла в обмене. recognition_exchange.py: ack → PROCESSING; error → ERROR; вахтёр timeout → ERROR; публикатор dlq → ERROR (все — только при current_recognition_job_id = job); ResultMapper/NoopMapper удалены, _map_result убран, тесты inbox обновлены (RecordingMapper → проверка статуса файла). Тесты: сценарии спеки recognition-job-exchange (ack → PROCESSING, error → ERROR, timeout → ERROR, done → файл остаётся PROCESSING).

3. Маппинг

  • 3.1 Домен маппинга. domain/osv_mapping.py: normalize_account_code, account_root, EntryDraft, build_entries(doc, chart, resolve) (пропуск TOTAL, parent по row_number, account_id), counterparty_check(header, owner), document_status(...); repositories/counterparties.py:CounterpartyIndex + resolve(row) -> uuid | None (ИНН → имя → алиас, >1 → None). Unit-тесты: нормализация (60.1, 76.7.1, 76.АВ, МЦ.04 → root None), дерево и TOTAL на result-osv-minimal.json и golden ОСВ_66 за 2024, резолв (ИНН/имя/алиас/неоднозначно), проверка шапки (ИНН, имя, алиас, пусто), статус документа.
  • 3.2 Цикл маппинга. services/result_mapping.py: pick_candidate (FOR UPDATE SKIP LOCKED), проверка ключа (OQ-70), чтение S3 вне транзакции, map_file (одна транзакция: очистка прежних строк файла, document×N с обработкой повтора счёта, пакетная вставка osv_entry, group_schema, is_cumulative по Q1 (D-9), metrics.mapping_warnings, PROCESSED), повторы mapping_attempts → ERROR, run_mapping_loop в lifespan (ExchangeTasks). Интеграционные тесты: сценарии спеки osv-result-mapping на FakeStorage с golden JSON (ОСВ_66), ключ вне папки, сбой с повтором (monkeypatch), идемпотентность, два листа одного счёта, MISMATCH шапки, нарастающая по Q1, RECOGNIZED.
  • 3.3 Автозамена. В map_file после PROCESSED: поиск прежних файлов по документам, SUPERSEDED, version_no, аудит FILE_REPLACE (системное событие). Тесты: сценарий спеки (CONFIRMED заменяется, confirmed=true в description), два разных счёта не заменяют друг друга, include_superseded.
  • 3.4 Golden-прогон. tests/fixtures/test_mapping_golden.py (fixtures): все ../fixtures/golden/expected_osv_parse/**/*.json → файл + задание done + маппинг через map_file; инварианты спеки (строки без TOTAL, родители, суммы, account_id). Время: ≤ 60 с на все 82.

4. API и удаление

  • 4.1 API чтения и подтверждение. repositories/files.py, services/documents.py, schemas/documents.py, эндпоинты GET /legal-entities/{id}/files, GET /files/{id}, GET /documents/{id}, GET /documents/{id}/entries, POST /documents/{id}/confirm; policy через ЮЛ-владельца. Тесты: сценарии спеки documents-api; test_openapi_routes.
  • 4.2 Удаление файла. DELETE /files/{id}: проверки CONFIRMED/superseded_by, удаление строк/документов/заданий/файла, s3_object.expires_at, аудит FILE_DELETE. Тесты: сценарии спеки.

5. Сквозной прогон и документы

  • 5.1 E2E через compose. tests/e2e/test_osv_ingest_e2e.py (маркеры integration, e2e): при поднятом профиле services (реальный tabular-parser против общего NATS/MinIO) — загрузка rb/2024-FY/ОСВ_66 за 2024.xls через API ядра, ожидание PROCESSED (≤ 60 с), проверка документа и 9 строк (10 строк снимка минус TOTAL); без профиля — skip с причиной. make e2e цель; check-full его включает (skip допустим).
  • 5.2 Документы. README (загрузка: curl-пример, статусы файла и документа, цикл маппинга и настройки, API, удаление и замена, ограничения), CLAUDE.md (DBML v5.15, domain/osv_mapping.py, services/primary_files.py, services/result_mapping.py, три фоновых цикла), qa-guide в docs — сценарии загрузки (в docs-change); make check-full зелёный; tasks.md [x].
  • 5.3 После merge: openspec archive slice-2c-osv-ingest.