28 Commits

Author SHA1 Message Date
Andrey Epifancev
58f039a91d feat(users): GET /users/brief — имена пользователей для всех аутентифицированных (D-12)
Добавлен эндпоинт GET /api/v1/users/brief для получения краткого справочника
пользователей (id, ФИО, статус активности) по списку id. Доступен любому
аутентифицированному пользователю без отдельного права. Максимум 200 id,
иначе 422 TOO_MANY_IDS; некорректный UUID — 422 INVALID_IDS.

- Создан api/src/api/endpoints/users.py с маршрутом /brief
- Добавлен метод users_brief в repositories/users.py
- Добавлена схема UserBriefOut в schemas/admin.py
- Подключён маршрут users в build_api_router ДО admin.router
- Обновлены тесты OpenAPI: добавлены маршрут и префикс /users/brief
- Написаны интеграционные тесты в test_api_users_brief.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-07 13:08:29 +04:00
Andrey Epifancev
c9bd620a18 feat(documents): комментарии к документу и comments_count в карточке (D-11)
POST/GET /documents/{id}/comments, счётчик комментариев в карточке файла
и документа одним запросом на пачку документов.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-07 12:46:43 +04:00
Andrey Epifancev
6c00a8705f fix(statements): шапка формы перечитывается под блокировкой; тесты аудита и сброса акцепта (I-1, T6.1)
I-1: `_editable` брал шапку без перечитывания, и после ожидания блокировки видел устаревший акцепт и значения
шапки. `get_meta(..., refresh=True)` читает с populate_existing после lock_file и lock_document.

T6.1: тесты — содержимое аудита правки значения и акцепта, исходное значение даты отчёта, дата не на конце квартала,
сброс акцепта правкой только значения, неизменность формы после INVALID_INN, свежесть шапки под блокировкой.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-07 12:23:03 +04:00
Andrey Epifancev
38a6a1b0c2 feat(statements): правка формы (значения, ИНН, дата, единицы) и акцепт проверок — аудит, пересчёт статуса (D-7, D-8)
PATCH /documents/{id}/statement и POST /documents/{id}/statement/checks/accept: блокировки «файл → документ»,
409 FILE_SUPERSEDED/DOCUMENT_READONLY/DOCUMENT_NOT_STATEMENT, 422 INVALID_INN/UNKNOWN_CELL/DUPLICATE_CELL,
один audit_event на запрос, сброс акцепта правкой, пересчёт статуса D-6.

Замечания ревью Task 5: пара формы ищется только среди документов с шапкой формы (T5.1), строки формы упорядочены
по (line_code, id) (T5.3), слабый тест межформенной проверки удалён (T5.4), локальный импорт в тестах убран (T5.6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-07 12:09:43 +04:00
Andrey Epifancev
2b5e70f7c3 feat(statements): GET /documents/{id}/statement — шапка, колонки, строки, проверки §7, редакция (D-5, D-14)
Схемы формы, StatementService (видимость, 409 DOCUMENT_NOT_STATEMENT, проверки §7 и межформенная),
репозиторий (get_meta, load_form, pair_amounts), маршрут GET с DOCUMENT_VIEW, тесты API и детерминированный
тест межформенной проверки.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-07 11:54:38 +04:00
Andrey Epifancev
1d821b4d3e feat(statements): маппинг форм ББ/ОФР в map_file — meta, колонки, строки, значения, статус (D-1…D-3, D-6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-07 11:40:59 +04:00
Andrey Epifancev
74be7d357a fix(aliases): удаление алиаса под advisory-блокировкой (T11.3, M-4)
aliases.remove берёт lock_alias_key по normalized_alias и перечитывает алиас под блокировкой:
при гонке с move_alias ручного сопоставления отвечает 404 без фантомного события об удалении.
Тесты: удаление ∥ перенос алиаса; ручное сопоставление ждёт блокировку алиаса (design №66).

Refs #21

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Claude-Session: cedd7915-2efe-41e5-988c-a9117033598b
2026-10-06 13:06:11 +04:00
Andrey Epifancev
42c253146c test(consolidation): подходящесть неподтверждённых кандидатов чек-листа, правки spec и design по финальному ревью
I-1 (T8.2): тесты, что неподтверждённый документ даёт NEEDS_CHECK/RECOGNIZED только если подходит версии (fits):
нарастающий Q2 и квартальный Q4 при версии Q3, другой год и ЮЛ вне периметра не влияют; положительный контроль.
M-2: 404 `NOT_FOUND` с сообщением `…_NOT_FOUND`, ключ привязки с признаком нарастающего (R-1, R-14), порядок и
режимы блокировок подтверждения (R-3) в spec. M-3: в design #39 и #43 — окно коммита перехода после загрузки.
M-5: docstring lock_versions_for_entity про ЮЛ без ГК в периметре.

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 08:28:24 +04:00
Andrey Epifancev
e246531c70 feat(consolidation): кандидаты и ручное сопоставление контрагента с алиасом и автоприменением
GET …/counterparty-candidates (scope group|all, QUERY_TOO_SHORT, recommended по
нормализованному имени строки) и POST …/osv/entries/{entry_id}/counterparty:
проверка строки свода версии, ЮЛ без policy, ALIAS_TAKEN с owners / перенос алиаса,
MANUAL строке и PROPAGATED строкам с тем же strip()-именем без противоречия ИНН,
один OSV_COUNTERPARTY_MAP и LEGAL_ENTITY_CHANGE на каждый алиас. Строки общие для
версий блокируются FOR NO KEY UPDATE ORDER BY id; advisory-блокировка алиаса общая
со справочником. Решения #62–#70 в design.md; 4.3 отмечена.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 06:46:06 +04:00
Andrey Epifancev
f46cf2fec5 feat(consolidation): чек-лист комплекта документов по правилам подхода
Домен `checklist` (Rule, CellStatus, Cell, order_rules, build) — чистые функции без ORM; ячейка (ЮЛ, правило):
CONFIRMED по привязанным документам (ОСВ: нарастающий за квартал версии либо квартальные Q1..Qn, пробел →
missing_quarters; ББ/ОФР: документ за квартал версии), NEEDS_CHECK/RECOGNIZED по неподтверждённому подходящему,
PROCESSING, ERROR, иначе MISSING; правило ОСВ закрывает только точный код счёта. Репозиторий: rules_for,
unconfirmed_documents. Сервис: checklist(), ячейки в карточке версии. Маршрут GET /consolidations/versions/{id}/checklist.
Схема использует CellStatus домена (T2.8). Решения №44-49 в design.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 05:50:48 +04:00
Andrey Epifancev
0e1fbd8036 fix(consolidation): ревью Task 6 — гонки автозамены с подхватом, выбор по confirmed_at, одна выборка ГК
- T6.0: тесты на двух сессиях «автозамена ∥ подхват при правке периметра» и «автозамена ∥ создание версии»:
  автозамена ждёт, снимает незафиксированную ранее привязку, unbound_version_ids — эта версия.
- T6.2: тесты выбора самого позднего confirmed_at строятся так, чтобы «наибольший id» проигрывал;
  тест равенства confirmed_at (больший id, решение №35).
- T6.3: реальный подхват через PUT …/members — версия в INITIATED, статус не меняется.
- T6.4: lock_versions_for_entity не читает ГК ЮЛ второй раз (подзапрос заменён значением).
- T6.5: design №30 — цена: подтверждения документов ЮЛ одной ГК сериализуются на advisory даже без версий в работе.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 05:11:03 +04:00
Andrey Epifancev
36bc966bf6 feat(consolidation): привязка документов к версиям при подтверждении, подхват и отвязка при автозамене
- consolidation_documents: чистая fits (D-5), bind_confirmed (подхват, самый поздний confirmed_at на ключ R-1),
  unbind_entity, on_document_confirmed (привязка ко всем подходящим версиям «в работе», переход в DATA_CORRECTION
  через transition), on_documents_superseded (отвязка документов заменённых файлов).
- DocumentService.confirm вызывает хук привязки; supersede_previous снимает привязки и пишет
  FILE_REPLACE.after_value.unbound_version_ids; create и PUT …/members подхватывают по-настоящему.
- R-2: advisory-блокировка привязок ГК (lock_group_bindings) в create и lock_versions_for_entity закрывает
  гонку «создание версии ∥ подтверждение/автозамена».
- R-3: lock_document и lock_file_documents — FOR NO KEY UPDATE (совместимо с FOR KEY SHARE вставки привязки).
- Тесты: таблица fits, сценарии documents-api и primary-files, две версии одного документа, гонки на двух
  сессиях (confirm ∥ members в обе стороны, create ∥ confirm в обе стороны, цикл блокировок документа и версии).
- design.md: решения №30–№36; plan и tasks.md (3.1) отмечены.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 04:57:15 +04:00
Andrey Epifancev
90b7bd5c16 feat(consolidation): редактирование периметра версии с подхватом и отвязкой документов
PUT /consolidations/versions/{id}/members: замена состава ЮЛ версии, проверка принадлежности ГК
(ENTITY_NOT_IN_GROUP), минимум два ЮЛ, diff с отвязкой документов ушедших (unbind_entity) и подхватом
добавленных (bind_confirmed, заглушка до Task 6), аудит MEMBERSHIP_CHANGE. Общий пролог
«lock → видимость → редактируемость» вынесен в services/consolidation_guard.py (T4.2). Решения #25–#29.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 04:34:52 +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
eaee3ec5f9 feat(model): DBML v5.16 — counterparty_match_kind, миграция 0004, шаг резолва в маппинге
Enum counterparty_match_kind (INN, NAME, ALIAS, MANUAL, PROPAGATED) и nullable-колонка osv_entry.counterparty_match_kind
(NULL <=> counterparty_id NULL). Миграция 0004_dbml_v5_16: enum, колонка, backfill тремя UPDATE по уже смапленным
строкам (ИНН без пробелов -> INN, trim(имя) -> NAME, остальные -> ALIAS), откат удаляет колонку и тип.
CounterpartyIndex.resolve возвращает Resolution(entity_id, kind) (определение в domain/osv_mapping, R-4);
маппинг пишет kind; EntryOut.match_kind. DBML_VERSION 5.16, Makefile/dbml_parity/CLAUDE/README/config.yaml.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-06 03:23:02 +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
15021735e9 feat(files): удаление файла с ограничениями, expires_at у объекта S3, FILE_DELETE
DELETE /files/{file_id} (DOCUMENT_EDIT, policy): блокировки «задание → файл → документы», проверки после
блокировок (409 FILE_HAS_CONFIRMED_DOCUMENTS, FILE_SUPERSEDES_OTHERS), удаление osv_entry/document/recognition_job/
stored_file (разрыв FK-цикла), expires_at у объектов S3 файла и результатов, аудит FILE_DELETE с before_value.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 23:43:54 +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
9658f809e7 feat(mapping): индекс и резолв контрагентов по ИНН/имени/алиасу (§3 п. 4)
CounterpartyIndex.load — два запроса по всему справочнику без policy (включая закрытые ЮЛ);
resolve — ИНН, затем точное имя, затем normalize_alias по именам и алиасам, >1 кандидата → None;
неизвестный ИНН не блокирует поиск по имени. owner_names — для counterparty_check.
Отмечены шаги Task 6 и пункт 3.1 tasks.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 22:27:42 +04:00
Andrey Epifancev
800f3a49c4 feat(admin): пользователи, роли, матрица прав — API с аудитом и защитой LAST_ADMIN
/api/v1/admin/* под правом USERS_ADMIN (403 на всё без него): список пользователей
(поиск по email/ФИО, is_active, Page), создание (email уникален без учёта регистра →
409 EMAIL_EXISTS, роли — коды активных ролей → 422 UNKNOWN_ROLE), PATCH полей и
деактивация, PUT ролей (замена набора с diff), GET ролей с правами, GET справочника
прав (16 кодов), PUT прав роли (422 UNKNOWN_PERMISSION, granted_by/granted_at).
Аудит USER_CHANGE / USER_ROLE_CHANGE (USERS) и ROLE_PERMISSION_CHANGE (ACCESS);
повтор того же состояния аудит не пишет.

LAST_ADMIN: деактивация (в том числе себя), замена ролей и снятие USERS_ADMIN с роли
запрещены, если у активных пользователей через активные роли не останется USERS_ADMIN.
Гонки: строка пользователя/роли и строка права USERS_ADMIN блокируются FOR NO KEY
UPDATE (FOR UPDATE давал deadlock на FK-проверке вставки audit_event); тест двух
параллельных транзакций с барьером.

Задача плана Task 12, tasks.md 4.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 00:07:07 +04:00
Andrey Epifancev
c9c3b69076 feat(directories): импорт выгрузки ЕЦП — сервис, эндпоинт, аудит, файл в S3
POST /api/v1/directories/import (DIRECTORY_ADMIN, multipart: file, dry_run=true, on_existing=skip):
снимок справочника тремя запросами (load_snapshot), план ядра §13.2, применение пакетами по типу
операции; UpdateGroup/UpdateEntity — apply_group_update/apply_entity_update (R-14) с description
«импорт ЕЦП». dry_run — явный session.rollback(), без аудита и S3. Боевой прогон —
audit_event(DIRECTORY_IMPORT, after_value = отчёт) и файл uploads/imports/{event_id}/{имя} +
s3_object(uploads). IMPORT_MAX_BYTES (20 МБ) → 413; ошибки файла → 422 MISSING_COLUMNS/UNSUPPORTED_FILE.
Тест dry_run — на отдельной БД с сессией запроса как в проде (SAVEPOINT-обёртка db_session
при rollback снимает и seed теста).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-04 23:15:26 +04:00
Andrey Epifancev
88a4b6d7a2 feat(directories): ГСК/ГСЗ и состав (SCD2, одна группа каждого вида на ЮЛ)
Создание с кодом GSK-/GSZ-NNNNNN, изменение name/created_date, удаление только пустой группы
(ASSUMPTION(OQ-34), закрытые строки состава удаляются вместе с группой), добавление члена
(та же ГК, kind группы, MEMBER_OF_OTHER_GROUP / MEMBER_EXISTS), исключение SCD2. Видимость
группы — через явный JOIN ГК и company_groups_visible (T4.1). Аудит MEMBERSHIP_CHANGE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-04 22:08:04 +04:00
Andrey Epifancev
7007c51f63 feat(directories): алиасы ЮЛ с нормализацией §3
normalize_alias (lower, ё→е, без пунктуации и ОПФ), POST/DELETE
/directories/legal-entities/{id}/aliases, конфликты ALIAS_EXISTS /
ALIAS_TAKEN (с legal_entity_id владельца), EMPTY_ALIAS, аудит
LEGAL_ENTITY_CHANGE по legal_entity_alias.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-04 21:52:47 +04:00
Andrey Epifancev
347b46e32c feat(directories): ЮЛ/ИП — CRUD, перевод, вывод из состава, история, аудит
- domain/inn.py: нормализация и проверка ИНН (ЮЛ — 10, ИП — 12 цифр 0–9), ASSUMPTION(OQ-88)
- репозиторий: список ЮЛ с видимостью и фильтрами (company_group_id, outside_group, inn точно, q по name/inn),
  карточка, история (по убыванию valid_from), текущие членства, close_memberships (SCD2), алиасы
- LegalEntityService: create (INN_EXISTS, CRM_ID_EXISTS, ГК через scope → 404, филиал по умолчанию — филиал ГК),
  update (no-op без истории; снимок UPDATE), move (MOVE_TO_GROUP, закрытие членств, MEMBERSHIP_CHANGE),
  withdraw (CLOSE / REMOVE_FROM_GROUP), effective_date ≥ сегодня — ASSUMPTION(OQ-21)
- эндпоинты /directories/legal-entities: GET/POST, GET/PATCH /{id}, /{id}/move, /{id}/withdraw, /{id}/history
- тесты: unit ИНН, интеграционные сценарии спеки, Review Focus 1–3, откат мутации

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-04 21:41:17 +04:00
Andrey Epifancev
80e8905e68 feat(directories): филиалы и ГК — API, история, аудит, видимость
Репозиторий, сервис и эндпоинты /api/v1/directories/{regional-offices,company-groups}:
код GK-NNNNNN из последовательности, снимки в company_group_history, no-op PATCH
без истории и аудита, поиск q по name/crm_id/code с экранированием LIKE, видимость
по филиалу (чужая ГК = 404 как несуществующая), аудит GROUP_CHANGE/ASSIGNMENT_CHANGE.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-04 21:28:06 +04:00
Andrey Epifancev
11941caa20 feat: зависимости current_user и require(code), репозиторий пользователей, схема me
Пользователь, филиал, роли и права читаются одним запросом; роль is_active=false прав не даёт;
email без учёта регистра, неоднозначность — 403 (R-17).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 22:14:10 +04:00
Andrey Epifancev
ba922225ab chore: каркас из шаблона команды (DKB_ARMKA@ba172f8, доменный код удалён) 2026-10-02 23:15:37 +04:00