# API до сводной ОСВ: пробелы по макету и комментариям фронтенда — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** экран [42] создаёт консолидацию одним запросом с выбранным периметром, экран [34] строится из одной карточки версии (подписанные ячейки чек-листа, тип ЮЛ, счета ОСВ), контракт для фронта описывает всё, что фронт использует. **Architecture:** только расширение существующих схем и сервиса консолидаций arm-core (`ConsolidationCreate`, `ChecklistCellOut`, `MemberOut`, `services/consolidations.py`) и один новый запрос репозитория; миграций и DBML нет. Документы — отдельный bounded-PR в arm-ka-docs. **Tech Stack:** Python 3.11, FastAPI, pydantic v2, SQLAlchemy 2 async, PostgreSQL 16, pytest-asyncio; `make` из корня arm-core. **Spec:** `openspec/changes/front-api-gaps/` (proposal, design D-1…D-4 и «Decisions made without discussion» №1–8, `specs/consolidations-api/spec.md`). Issue — RSHB/arm-core#42. ## Global Constraints - Рабочая копия: `/Users/andrey.epifancev/projects/awp-ca/.wt/front-gaps/arm-core`, ветка `feat/42-front-api-gaps`. Никаких `cd` в основные checkout'ы (`/Users/andrey.epifancev/projects/awp-ca/{arm-core,docs}`): хук guard-main блокирует запись, когда cwd — репозиторий на `main`. - Команды — из корня arm-core: `make check` (ruff + mypy + unit), `make test-integration` (нужен compose: PostgreSQL/NATS уже подняты — `docker ps` показывает `arm-ka-postgres-1`, `arm-ka-nats-1`, `arm-ka-minio-1`). Точечный прогон — `cd api && .venv/bin/pytest <путь>::<тест> -v` (venv создаётся `make venv`, если `api/.venv` нет). - DBML, миграции, контракт распознавания (`api/contracts/`) — **не менять**. - Ответы API только расширяются; запрос `POST /consolidations` без `legal_entity_ids` ведёт себя как прежде. - Значения `entity_type` — `"ЮЛ"` / `"ИП"` (enum `EntityType`, как в DBML). - Сортировка счетов — только `src.domain.osv_mapping.account_sort_key` (одно определение порядка счетов). - Язык комментариев, docstring и коммитов — русский; идентификаторы — английские. Docstring ссылается на спеку/design. - Каждый коммит заканчивается строками: ``` Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 ``` - Сообщения коммитов — `feat(consolidations): …`, `test(consolidations): …`, `docs: …` по-русски. ## Review Focus - **Пустой список `legal_entity_ids: []`** — это явный периметр из нуля ЮЛ, а не «поле не передано»: ожидается 422 `PERIMETER_TOO_SMALL`, а не периметр из ГСК. Тест — Task 1, `test_create_with_empty_selected_perimeter_is_too_small`. - **Несуществующий `uuid` в `legal_entity_ids`** — ожидается 422 `ENTITY_NOT_IN_GROUP` с этим `uuid` в перечне (не 404 и не 500). Тест — Task 1, `test_create_with_unknown_entity_is_not_in_group`. - **Неверная ГСК при явном списке** — проверка ГСК раньше периметра: 422 `RELATED_GROUP_MISMATCH`, даже если список тоже неверный. Тест — Task 1, `test_create_gsk_mismatch_reported_before_perimeter`. - **Один счёт в нескольких кварталах** — в `osv_accounts` один раз. Тест — Task 3, в `test_card_member_osv_accounts_loaded_to_version_period` (ОСВ 60 за Q1 и Q2). - **Версия за Q1** — документы Q2+ того же года в `osv_accounts` не попадают. Тест — Task 3, `test_card_member_osv_accounts_q1_version_ignores_later_quarters`. --- ## File Structure | Файл | Что меняется | |---|---| | `api/src/schemas/consolidations.py` | `ConsolidationCreate.legal_entity_ids`; `ChecklistCellOut.document_type/account`; `MemberOut.entity_type/osv_accounts` | | `api/src/services/consolidations.py` | `_validate_perimeter` (общая для `create` и `set_members`); `create`/`_insert` по списку `uuid`; `_cell_out(rule, cell)`; `_card` — новые поля участника | | `api/src/repositories/consolidations.py` | новая `osv_accounts(...)` | | `api/tests/test_api_consolidations.py` | тесты создания с периметром, карточки (тип ЮЛ, счета ОСВ) | | `api/tests/test_api_consolidation_checklist.py` | ячейки с `document_type`/`account` | | `api/tests/test_schemas_consolidations.py`, `api/tests/test_domain_checklist_csv.py` | конструкторы ячеек, граница `legal_entity_ids` | | `api/tests/test_openapi_routes.py` | новые поля в схеме OpenAPI | | `README.md`, `CLAUDE.md`, `openspec/changes/front-api-gaps/tasks.md` | описание (Task 5) | | docs: `dev/api-contract-files-directories.md` (новый), `dev/api-contract-slice-4.md`, `context/07-open-questions.md`, `context/10-user-journey.md`, `mockup/INDEX.md` | Task 4, отдельный worktree и PR | --- ### Task 1: Создание консолидации с выбранным периметром **Files:** - Modify: `api/src/schemas/consolidations.py` (класс `ConsolidationCreate`, ~стр. 44) - Modify: `api/src/services/consolidations.py` (`create` ~стр. 89–131, `set_members` ~стр. 237–258, `_insert` ~стр. 294–341) - Test: `api/tests/test_api_consolidations.py`, `api/tests/test_schemas_consolidations.py`, `api/tests/test_openapi_routes.py` **Interfaces:** - Consumes: `repo.entities_in_group(session, group_id, entity_ids) -> set[uuid.UUID]` (есть), `directories_repo.current_members(session, related_group_id)` (есть), `MIN_PERIMETER = 2`, `MAX_MEMBERS = 500`. - Produces: `ConsolidationCreate.legal_entity_ids: list[uuid.UUID] | None`; `ConsolidationService._validate_perimeter(self, company_group_id: uuid.UUID, entity_ids: Sequence[uuid.UUID]) -> list[uuid.UUID]`; `ConsolidationService._insert(self, group_id: uuid.UUID, data: ConsolidationCreate, entity_ids: list[uuid.UUID]) -> tuple[Consolidation, ConsolidationVersion]`. Класс сервиса — `ConsolidationService`. - [ ] **Step 1: Тест схемы — граница и значение по умолчанию** В `api/tests/test_schemas_consolidations.py` после `test_consolidation_create_year_bounds` добавить: ```python def test_consolidation_create_selected_perimeter_bounds() -> None: """D-1: `legal_entity_ids` необязателен (`None` — периметр из ГСК), не больше `MAX_MEMBERS`.""" ids = {"company_group_id": uuid.uuid4(), "related_group_id": uuid.uuid4(), "year": 2024, "quarter": "Q3"} assert ConsolidationCreate(**ids).legal_entity_ids is None selected = [uuid.uuid4() for _ in range(MAX_MEMBERS)] assert len(ConsolidationCreate(**ids, legal_entity_ids=selected).legal_entity_ids or []) == MAX_MEMBERS with pytest.raises(ValidationError): ConsolidationCreate(**ids, legal_entity_ids=[*selected, uuid.uuid4()]) ``` - [ ] **Step 2: Интеграционные тесты API** В `api/tests/test_api_consolidations.py`: в импорт `from src.models.directories import …` добавить `RelatedGroupMember`; `from collections.abc import Sequence` — в стандартные импорты. После `_body` добавить помощник и после `test_create_ignores_closed_memberships` — тесты: ```python def _with_ids(body: dict[str, Any], ids: Sequence[uuid.UUID | str]) -> dict[str, Any]: """Тело создания с явным периметром (модалка [42], design D-1).""" return {**body, "legal_entity_ids": [str(entity_id) for entity_id in ids]} async def _gsk_members(session: AsyncSession, gsk: RelatedGroup) -> set[uuid.UUID]: stmt = select(RelatedGroupMember.legal_entity_id).where( RelatedGroupMember.related_group_id == gsk.id, RelatedGroupMember.is_current.is_(True) ) return set((await session.execute(stmt)).scalars()) @pytest.mark.integration async def test_create_with_selected_perimeter_wider_than_gsk( auth_client: AuthClientFactory, seeded: AsyncSession ) -> None: """Выбранный периметр шире ГСК: ровно эти ЮЛ, одно событие создания, состав ГСК не меняется.""" group, gsk, entities = await _scenario(seeded) outsider = await make_legal_entity(seeded, name="ООО Вне ГСК", group=group) selected = [*entities, outsider] client = await auth_client(KA_RF) response = await client.post(CONSOLIDATIONS, json=_with_ids(_body(group, gsk), [e.id for e in selected])) assert response.status_code == HTTP_CREATED, response.text assert {member["id"] for member in response.json()["members"]} == {str(e.id) for e in selected} event = ( await seeded.execute(select(AuditEvent).where(AuditEvent.action == AuditAction.CONSOLIDATION_CREATE)) ).scalar_one() assert event.after_value["members"] == sorted(str(e.id) for e in selected) membership = select(func.count()).select_from(AuditEvent).where(AuditEvent.action == AuditAction.MEMBERSHIP_CHANGE) assert (await seeded.execute(membership)).scalar_one() == 0 assert await _gsk_members(seeded, gsk) == {e.id for e in entities} assert await _count(seeded, ConsolidationLock) == 0 @pytest.mark.integration async def test_create_gsk_of_one_with_second_entity_selected( auth_client: AuthClientFactory, seeded: AsyncSession ) -> None: """ГСК из одного ЮЛ, второе отмечено вручную — периметр из двух (проверка размера — по выбранным).""" group, gsk, (single,) = await _scenario(seeded, members=1) second = await make_legal_entity(seeded, name="ООО Второе", group=group) client = await auth_client(KA_RF) response = await client.post(CONSOLIDATIONS, json=_with_ids(_body(group, gsk), [single.id, second.id])) assert response.status_code == HTTP_CREATED, response.text assert {member["id"] for member in response.json()["members"]} == {str(single.id), str(second.id)} @pytest.mark.integration async def test_create_with_entity_of_other_group_is_rejected( auth_client: AuthClientFactory, seeded: AsyncSession ) -> None: group, gsk, entities = await _scenario(seeded) other_group = await make_company_group(seeded, name="ГК Соседняя", office=await _office(seeded, ROSTOV)) stranger = await make_legal_entity(seeded, name="ООО Чужое", group=other_group) client = await auth_client(KA_RF) response = await client.post(CONSOLIDATIONS, json=_with_ids(_body(group, gsk), [*[e.id for e in entities], stranger.id])) assert response.status_code == HTTP_UNPROCESSABLE, response.text detail = response.json()["detail"] assert detail["code"] == "ENTITY_NOT_IN_GROUP" assert detail["legal_entity_ids"] == [str(stranger.id)] assert await _count(seeded, Consolidation) == 0 @pytest.mark.integration async def test_create_with_unknown_entity_is_not_in_group(auth_client: AuthClientFactory, seeded: AsyncSession) -> None: group, gsk, entities = await _scenario(seeded) ghost = uuid.uuid4() client = await auth_client(KA_RF) response = await client.post(CONSOLIDATIONS, json=_with_ids(_body(group, gsk), [*[e.id for e in entities], ghost])) assert response.status_code == HTTP_UNPROCESSABLE, response.text assert response.json()["detail"]["code"] == "ENTITY_NOT_IN_GROUP" assert response.json()["detail"]["legal_entity_ids"] == [str(ghost)] @pytest.mark.integration async def test_create_with_duplicates_collapsing_to_one_is_too_small( auth_client: AuthClientFactory, seeded: AsyncSession ) -> None: group, gsk, entities = await _scenario(seeded) client = await auth_client(KA_RF) response = await client.post(CONSOLIDATIONS, json=_with_ids(_body(group, gsk), [entities[0].id, entities[0].id])) assert response.status_code == HTTP_UNPROCESSABLE, response.text assert response.json()["detail"]["code"] == "PERIMETER_TOO_SMALL" assert await _count(seeded, Consolidation) == 0 @pytest.mark.integration async def test_create_with_empty_selected_perimeter_is_too_small( auth_client: AuthClientFactory, seeded: AsyncSession ) -> None: """Review Focus: `[]` — явный пустой периметр, а не «поле не передано» (ГСК из двух не подставляется).""" group, gsk, _ = await _scenario(seeded) client = await auth_client(KA_RF) response = await client.post(CONSOLIDATIONS, json=_with_ids(_body(group, gsk), [])) assert response.status_code == HTTP_UNPROCESSABLE, response.text assert response.json()["detail"]["code"] == "PERIMETER_TOO_SMALL" @pytest.mark.integration async def test_create_with_null_selected_perimeter_takes_gsk(auth_client: AuthClientFactory, seeded: AsyncSession) -> None: """Решение №3: `legal_entity_ids: null` равнозначно отсутствию поля.""" group, gsk, entities = await _scenario(seeded) client = await auth_client(KA_RF) response = await client.post(CONSOLIDATIONS, json={**_body(group, gsk), "legal_entity_ids": None}) assert response.status_code == HTTP_CREATED, response.text assert {member["id"] for member in response.json()["members"]} == {str(e.id) for e in entities} @pytest.mark.integration async def test_create_gsk_mismatch_reported_before_perimeter( auth_client: AuthClientFactory, seeded: AsyncSession ) -> None: """Порядок проверок (решение №5): ГСК раньше периметра.""" group, _, _ = await _scenario(seeded) gsz = await make_related_group(seeded, group=group, kind=RelatedGroupKind.gsz, name="ГСЗ") client = await auth_client(KA_RF) response = await client.post(CONSOLIDATIONS, json=_with_ids(_body(group, gsz), [uuid.uuid4()])) assert response.status_code == HTTP_UNPROCESSABLE, response.text assert response.json()["detail"]["code"] == "RELATED_GROUP_MISMATCH" @pytest.mark.integration async def test_create_selected_perimeter_longer_than_limit(auth_client: AuthClientFactory, seeded: AsyncSession) -> None: group, gsk, _ = await _scenario(seeded) client = await auth_client(KA_RF) response = await client.post(CONSOLIDATIONS, json=_with_ids(_body(group, gsk), [uuid.uuid4() for _ in range(501)])) assert response.status_code == HTTP_UNPROCESSABLE, response.text assert response.json()["detail"][0]["loc"] == ["body", "legal_entity_ids"] assert await _count(seeded, Consolidation) == 0 ``` Если `make_related_group` не принимает `kind=` — посмотреть `test_create_gsk_must_belong_to_group_and_be_gsk` (там тот же вызов `make_related_group(seeded, group=group, kind=RelatedGroupKind.gsz, name="ГСЗ")`). Если поле членства ГСК в модели называется не `is_current` — взять фактическое имя из `api/src/models/directories.py` (`class RelatedGroupMember`). Строки длиннее 120 символов разбить, как требует ruff (`make lint` подскажет). - [ ] **Step 3: Тест OpenAPI** В `api/tests/test_openapi_routes.py` в конец файла: ```python def test_consolidation_create_has_optional_selected_perimeter(schema: dict[str, Any]) -> None: """front-api-gaps D-1: `legal_entity_ids` в теле создания — необязательный массив.""" create = schema["components"]["schemas"]["ConsolidationCreate"] assert "legal_entity_ids" in create["properties"] assert "legal_entity_ids" not in create.get("required", []) ``` - [ ] **Step 4: Запустить — тесты падают** Run: `cd api && .venv/bin/pytest tests/test_schemas_consolidations.py::test_consolidation_create_selected_perimeter_bounds tests/test_openapi_routes.py::test_consolidation_create_has_optional_selected_perimeter -v && .venv/bin/pytest -m integration tests/test_api_consolidations.py -k "selected or other_group or unknown_entity or duplicates_collapsing or gsk_of_one or mismatch_reported" -v` Expected: FAIL — `ConsolidationCreate` не знает `legal_entity_ids` (pydantic игнорирует лишнее поле: `legal_entity_ids is None` в теле с 500 id → `AttributeError`/assert; API создаёт периметр из ГСК). - [ ] **Step 5: Схема** `api/src/schemas/consolidations.py`, класс `ConsolidationCreate`: ```python class ConsolidationCreate(BaseModel): """Тело `POST /consolidations`: ГК, ГСК, период и необязательный выбранный периметр (модалка [42], design D-1). `legal_entity_ids` нет или `null` — периметр из текущих членов ГСК; список — ровно эти ЮЛ ГК (дубли схлопывает сервис). """ company_group_id: uuid.UUID related_group_id: uuid.UUID year: int = Field(ge=YEAR_MIN, le=YEAR_MAX) quarter: PeriodQuarter legal_entity_ids: list[uuid.UUID] | None = Field(default=None, max_length=MAX_MEMBERS) ``` `MAX_MEMBERS` объявлен выше в том же модуле (стр. ~31) — проверить, что объявление стоит до класса. - [ ] **Step 6: Сервис — общая проверка периметра** В `api/src/services/consolidations.py`: в импорты `from collections.abc import Sequence` (если нет). В класс сервиса (рядом с `_insert`) добавить: ```python async def _validate_perimeter( self, company_group_id: uuid.UUID, entity_ids: Sequence[uuid.UUID] ) -> list[uuid.UUID]: """Периметр консолидации — одно правило для создания и `PUT …/members` (design D-1): дубли схлопываются в порядке первого вхождения → все ЮЛ существуют и принадлежат ГК (422 `ENTITY_NOT_IN_GROUP`, `legal_entity_ids` — чужие в порядке запроса) → не меньше двух (422 `PERIMETER_TOO_SMALL`).""" requested = list(dict.fromkeys(entity_ids)) valid = await repo.entities_in_group(self._session, company_group_id, requested) foreign = [entity_id for entity_id in requested if entity_id not in valid] if foreign: raise DomainValidationError( "ENTITY_NOT_IN_GROUP", "ЮЛ не принадлежат ГК консолидации", legal_entity_ids=foreign ) if len(requested) < MIN_PERIMETER: raise DomainValidationError( "PERIMETER_TOO_SMALL", f"В периметре должно быть не меньше {MIN_PERIMETER} ЮЛ, сейчас {len(requested)}" ) return requested ``` В `set_members` заменить блок от `requested = list(dict.fromkeys(data.legal_entity_ids))` до конца проверки `PERIMETER_TOO_SMALL` одной строкой: ```python requested = await self._validate_perimeter(consolidation.company_group_id, data.legal_entity_ids) ``` - [ ] **Step 7: Сервис — создание по списку** В `create` заменить строки от `entities = [entity for _, entity in await directories_repo.current_members(...)]` до конца проверки `PERIMETER_TOO_SMALL` (по ГСК): ```python if data.legal_entity_ids is None: entity_ids = [entity.id for _, entity in await directories_repo.current_members(self._session, related.id)] if len(entity_ids) < MIN_PERIMETER: raise DomainValidationError( "PERIMETER_TOO_SMALL", f"В ГСК должно быть не меньше {MIN_PERIMETER} ЮЛ, сейчас {len(entity_ids)}" ) else: entity_ids = await self._validate_perimeter(group.id, data.legal_entity_ids) ``` Далее в `create`: `consolidation, version = await self._insert(group.id, data, entity_ids)` и в аудите `"members": sorted(entity_ids),`. Обновить docstring `create`: «Проверки по порядку: ГК видима (404) → ГСК видима (404), принадлежит ГК и `kind = gsk` (422 `RELATED_GROUP_MISMATCH`) → периметр: без `legal_entity_ids` — члены ГСК, не меньше двух; со списком — `_validate_perimeter` (422 `ENTITY_NOT_IN_GROUP` / `PERIMETER_TOO_SMALL`) → периода ещё нет (409 …)». Модульный docstring: «Периметр версии при создании — текущие члены ГСК или выбранные ЮЛ ГК (`legal_entity_ids`, front-api-gaps D-1), `is_head_entity = false`; …». В `_insert`: параметр `entities: list[LegalEntity]` → `entity_ids: list[uuid.UUID]`; `ConsolidationMember(..., legal_entity_id=entity_id, ...) for entity_id in entity_ids`; `bind_confirmed(..., entity_ids=entity_ids)`. Если `LegalEntity` больше нигде в модуле не используется — убрать импорт (ruff `F401` подскажет; он нужен `_checklist_cells` — тогда оставить). - [ ] **Step 8: Запустить — тесты проходят, прежние не сломаны** Run: `cd api && .venv/bin/pytest tests/test_schemas_consolidations.py tests/test_openapi_routes.py -v && .venv/bin/pytest -m integration tests/test_api_consolidations.py tests/test_api_consolidation_members.py -v` Expected: PASS (все, включая `test_create_with_single_member_is_perimeter_too_small`, `test_foreign_entity_is_reported_before_size`, `test_duplicates_are_collapsed`). - [ ] **Step 9: Линтеры** Run: `make lint` Expected: без ошибок. - [ ] **Step 10: Коммит** ```bash git add api/src/schemas/consolidations.py api/src/services/consolidations.py api/tests/test_api_consolidations.py api/tests/test_schemas_consolidations.py api/tests/test_openapi_routes.py git commit -m "feat(consolidations): создание с выбранным периметром legal_entity_ids, общая проверка периметра (#42) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2" ``` --- ### Task 2: Самоописывающие ячейки чек-листа **Files:** - Modify: `api/src/schemas/consolidations.py` (`ChecklistCellOut`, ~стр. 95) - Modify: `api/src/services/consolidations.py` (`checklist` ~стр. 169, `_cell_out` ~стр. 231, `_card` ~стр. 358) - Test: `api/tests/test_api_consolidation_checklist.py`, `api/tests/test_schemas_consolidations.py`, `api/tests/test_domain_checklist_csv.py`, `api/tests/test_openapi_routes.py` **Interfaces:** - Consumes: `checklist_domain.Rule(document_type, account, is_mandatory)`, `checklist_domain.Cell(status, missing_quarters, document_ids)`, `_checklist_cells(...) -> tuple[list[Rule], list[tuple[LegalEntity, list[Cell]]]]` (есть). - Produces: `ChecklistCellOut(document_type: DocumentType, account: str | None, status, missing_quarters, document_ids)`; `ConsolidationService._cell_out(rule: checklist_domain.Rule, cell: checklist_domain.Cell) -> ChecklistCellOut`. Task 3 меняет `_card` рядом — после Task 2 в `_card` есть переменная `rules`. - [ ] **Step 1: Обновить существующие ожидания и добавить тест карточки** `api/tests/test_api_consolidation_checklist.py`: ```python assert cells[OSV_60] == { "document_type": "OSV", "account": "60", "status": "CONFIRMED", "missing_quarters": [], "document_ids": [str(i) for i in documents], } assert cells[OSV_90] == { "document_type": "OSV", "account": "90", "status": "MISSING", "missing_quarters": [], "document_ids": [], } ``` (в `test_checklist_osv_60_confirmed_osv_90_missing`), и в `test_checklist_gap_needs_check_and_processing`: ```python assert first_cells[OSV_60] == { "document_type": "OSV", "account": "60", "status": "MISSING", "missing_quarters": ["Q2"], "document_ids": [str(first_q1), str(third_q3)], } ``` После `test_card_members_have_same_cells_as_checklist` добавить: ```python @pytest.mark.integration async def test_card_cells_are_self_describing(auth_client: AuthClientFactory, seeded: AsyncSession) -> None: """front-api-gaps D-2: ячейка несёт правило — фронт не сопоставляет ячейки с `rules` по индексу.""" version, _ = await _bound_world(seeded) client = await auth_client(KA_RF) checklist = (await client.get(f"/consolidations/versions/{version.id}/checklist")).json() card = (await client.get(f"/consolidations/versions/{version.id}")).json() expected = [(rule["document_type"], rule["account"]) for rule in checklist["rules"]] assert expected == EXPECTED_RULES for member in card["members"]: assert [(cell["document_type"], cell["account"]) for cell in member["checklist"]] == expected for row in checklist["rows"]: assert [(cell["document_type"], cell["account"]) for cell in row["cells"]] == expected balance = next(cell for cell in card["members"][0]["checklist"] if cell["document_type"] == "BALANCE") assert balance["account"] is None ``` `api/tests/test_schemas_consolidations.py`, `test_checklist_cell_status_is_the_domain_enum` — конструкторы: ```python cell = ChecklistCellOut( document_type=DocumentType.OSV, account="60", status=status.value, missing_quarters=[], document_ids=[] # type: ignore[arg-type] ) ... ChecklistCellOut( document_type=DocumentType.OSV, account="60", status="UNKNOWN", missing_quarters=[], document_ids=[] # type: ignore[arg-type] ) ``` и импорт `DocumentType` в `from src.models.enums import …`. Добавить тест: ```python def test_checklist_cell_requires_its_rule() -> None: """D-2: `document_type` обязателен, `account` — обязателен, но может быть `None` (ББ/ОФР).""" with pytest.raises(ValidationError): ChecklistCellOut(status=CellStatus.MISSING, missing_quarters=[], document_ids=[]) # type: ignore[call-arg] cell = ChecklistCellOut( document_type=DocumentType.BALANCE, account=None, status=CellStatus.MISSING, missing_quarters=[], document_ids=[] ) assert cell.account is None ``` `api/tests/test_domain_checklist_csv.py` — в `test_render` ячейки: ```python ChecklistCellOut( document_type=DocumentType.BALANCE, account=None, status=CellStatus.CONFIRMED, missing_quarters=[], document_ids=[], ), ChecklistCellOut( document_type=DocumentType.OSV, account="60", status=CellStatus.MISSING, missing_quarters=[PeriodQuarter.Q2], document_ids=[], ), ``` и помощник `_cell`: ```python def _cell(status: CellStatus, *quarters: PeriodQuarter) -> ChecklistCellOut: return ChecklistCellOut( document_type=DocumentType.OSV, account="60", status=status, missing_quarters=list(quarters), document_ids=[] ) ``` `api/tests/test_openapi_routes.py` — в конец: ```python def test_checklist_cell_declares_its_rule(schema: dict[str, Any]) -> None: """front-api-gaps D-2: ячейка чек-листа самоописывающая.""" cell = schema["components"]["schemas"]["ChecklistCellOut"] assert {"document_type", "account", "status", "missing_quarters", "document_ids"} <= set(cell["required"]) ``` - [ ] **Step 2: Запустить — падают** Run: `cd api && .venv/bin/pytest tests/test_schemas_consolidations.py tests/test_domain_checklist_csv.py tests/test_openapi_routes.py -v; .venv/bin/pytest -m integration tests/test_api_consolidation_checklist.py -v` Expected: FAIL — `ChecklistCellOut` не принимает/не отдаёт `document_type`, `account`. - [ ] **Step 3: Схема ячейки** ```python class ChecklistCellOut(BaseModel): """Ячейка чек-листа — самоописывающая (front-api-gaps D-2): правило ячейки (`document_type`, `account` — счёт ОСВ, `None` у ББ/ОФР), статус пары (ЮЛ, правило), недостающие кварталы ОСВ и привязанные документы.""" document_type: DocumentType account: str | None status: CellStatus # единственное определение — `domain.checklist.CellStatus` (T2.8) missing_quarters: list[PeriodQuarter] document_ids: list[uuid.UUID] ``` - [ ] **Step 4: Сервис** `_cell_out`: ```python @staticmethod def _cell_out(rule: checklist_domain.Rule, cell: checklist_domain.Cell) -> ChecklistCellOut: """Ячейка ответа с правилом (D-2); `rule` — правило того же столбца, что и `cell`.""" return ChecklistCellOut( document_type=rule.document_type, account=rule.account, status=cell.status, missing_quarters=list(cell.missing_quarters), document_ids=list(cell.document_ids), ) ``` В `checklist`: `cells=[self._cell_out(rule, cell) for rule, cell in zip(rules, cells, strict=True)],`. В `_card`: `rules, cells = await self._checklist_cells(...)` (вместо `_, cells = …`) и в `MemberOut(...)`: `checklist=[self._cell_out(rule, cell) for rule, cell in zip(rules, cells[position][1], strict=True)],`. - [ ] **Step 5: Запустить — проходят** Run: `cd api && .venv/bin/pytest tests/test_schemas_consolidations.py tests/test_domain_checklist_csv.py tests/test_openapi_routes.py -v && .venv/bin/pytest -m integration tests/test_api_consolidation_checklist.py tests/test_api_consolidations.py tests/test_api_consolidation_members.py -v` Expected: PASS; CSV-тесты (`test_checklist_csv_*`) проходят без изменения ожидаемого текста. - [ ] **Step 6: Линтеры и коммит** Run: `make lint` → без ошибок. ```bash git add api/src/schemas/consolidations.py api/src/services/consolidations.py api/tests/test_api_consolidation_checklist.py api/tests/test_schemas_consolidations.py api/tests/test_domain_checklist_csv.py api/tests/test_openapi_routes.py git commit -m "feat(consolidations): ячейки чек-листа с document_type и account своего правила (#42) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2" ``` --- ### Task 3: Участник периметра — тип ЮЛ и счета ОСВ **Files:** - Modify: `api/src/repositories/consolidations.py` (новая функция после `unconfirmed_documents`, ~стр. 351) - Modify: `api/src/schemas/consolidations.py` (`MemberOut`, импорт `EntityType`) - Modify: `api/src/services/consolidations.py` (`_card`; импорт `quarters_through`) - Test: `api/tests/test_api_consolidations.py`, `api/tests/test_openapi_routes.py` **Interfaces:** - Consumes: `account_sort_key(code: str)` из `src.domain.osv_mapping`; `quarters_through(quarter: PeriodQuarter) -> tuple[PeriodQuarter, ...]` из `src.domain.consolidation_periods`; после Task 2 в `_card` есть `rules, cells`. - Produces: `repo.osv_accounts(session: AsyncSession, entity_ids: Collection[uuid.UUID], year: int, quarters: Collection[PeriodQuarter]) -> dict[uuid.UUID, list[str]]`; `MemberOut.entity_type: EntityType`, `MemberOut.osv_accounts: list[str]`. - [ ] **Step 1: Интеграционные тесты карточки** В `api/tests/test_api_consolidations.py` в импорты фабрик добавить `make_document`, `make_stored_file`; в импорт enums — `DocumentStatus`, `EntityType`, `FileStatus`, `RecognitionStatus`. Добавить в конец файла: ```python async def _osv( # noqa: PLR0913 - параметры документа сценария session: AsyncSession, entity: LegalEntity, account: str, year: int, quarter: PeriodQuarter, *, status: DocumentStatus = DocumentStatus.RECOGNIZED, file_status: FileStatus = FileStatus.PROCESSED, ) -> None: """ОСВ-документ ЮЛ в отдельном файле (на файл — одна ОСВ по счёту).""" file, job = await make_stored_file(session, entity=entity, status=file_status, job_status=RecognitionStatus.done) await make_document(session, file=file, job=job, account=account, year=year, quarter=quarter, status=status) async def _card_world( session: AsyncSession, quarter: PeriodQuarter = PeriodQuarter.Q3 ) -> tuple[ConsolidationVersion, LegalEntity, LegalEntity]: """Версия 2024 `quarter` ГК Ростовского филиала: ЮЛ «ООО А» и ИП «ИП Б».""" group = await make_company_group(session, name="ГК Карточка", office=await _office(session, ROSTOV)) company = await make_legal_entity(session, name="ООО А", group=group) person = await make_legal_entity( session, name="ИП Б", group=group, entity_type=EntityType.ИП, inn="770100000012" ) _, version = await make_consolidation( session, group=group, year=2024, quarter=quarter, members=[company, person], initiator=await _user(session, KA_RF) ) return version, company, person @pytest.mark.integration async def test_card_member_entity_type(auth_client: AuthClientFactory, seeded: AsyncSession) -> None: version, company, person = await _card_world(seeded) client = await auth_client(KA_RF) card = (await client.get(f"{VERSIONS}/{version.id}")).json() types = {member["id"]: member["entity_type"] for member in card["members"]} assert types == {str(company.id): "ЮЛ", str(person.id): "ИП"} @pytest.mark.integration async def test_card_member_osv_accounts_loaded_to_version_period( auth_client: AuthClientFactory, seeded: AsyncSession ) -> None: """D-3 (вариант B): счета действующих ОСВ за год версии с кварталом ≤ версии, в любом статусе, кроме ERROR.""" version, company, person = await _card_world(seeded) q1, q2, q3, q4 = PeriodQuarter.Q1, PeriodQuarter.Q2, PeriodQuarter.Q3, PeriodQuarter.Q4 await _osv(seeded, company, "60", 2024, q1) # RECOGNIZED await _osv(seeded, company, "60", 2024, q2) # тот же счёт ещё раз — один раз в списке await _osv(seeded, company, "51", 2024, q3, status=DocumentStatus.CONFIRMED) await _osv(seeded, company, "58.03", 2024, q3, status=DocumentStatus.NEEDS_CHECK) await _osv(seeded, company, "62", 2024, q4) # позже квартала версии await _osv(seeded, company, "01", 2023, q3) # прошлый год await _osv(seeded, company, "76", 2024, q3, file_status=FileStatus.SUPERSEDED) # заменённый файл await _osv(seeded, company, "90", 2024, q3, status=DocumentStatus.ERROR) # документ в ошибке await _osv(seeded, company, "10", 2024, q3, file_status=FileStatus.ERROR) # файл в ошибке client = await auth_client(KA_RF) card = (await client.get(f"{VERSIONS}/{version.id}")).json() accounts = {member["id"]: member["osv_accounts"] for member in card["members"]} assert accounts == {str(company.id): ["51", "58.03", "60"], str(person.id): []} @pytest.mark.integration async def test_card_member_osv_accounts_q1_version_ignores_later_quarters( auth_client: AuthClientFactory, seeded: AsyncSession ) -> None: """Review Focus: версия за Q1 — документы Q2 того же года не входят.""" version, company, _ = await _card_world(seeded, PeriodQuarter.Q1) await _osv(seeded, company, "60", 2024, PeriodQuarter.Q1) await _osv(seeded, company, "62", 2024, PeriodQuarter.Q2) client = await auth_client(KA_RF) card = (await client.get(f"{VERSIONS}/{version.id}")).json() member = next(member for member in card["members"] if member["id"] == str(company.id)) assert member["osv_accounts"] == ["60"] ``` Импорт `make_consolidation` в тестовом модуле уже есть. Если `make_stored_file` с `status=FileStatus.ERROR` требует другой `job_status` — передать `job_status=RecognitionStatus.error` (значение enum проверить в `api/src/models/enums.py`, класс `RecognitionStatus`). `api/tests/test_openapi_routes.py` — в конец: ```python def test_version_member_declares_type_and_osv_accounts(schema: dict[str, Any]) -> None: """front-api-gaps D-3: участник периметра — тип ЮЛ и счета ОСВ к периоду версии.""" member = schema["components"]["schemas"]["MemberOut"] assert {"entity_type", "osv_accounts", "checklist"} <= set(member["required"]) ``` - [ ] **Step 2: Запустить — падают** Run: `cd api && .venv/bin/pytest tests/test_openapi_routes.py -v; .venv/bin/pytest -m integration tests/test_api_consolidations.py -k "card_member" -v` Expected: FAIL — `KeyError: 'entity_type'` / `'osv_accounts'`. - [ ] **Step 3: Репозиторий** `api/src/repositories/consolidations.py`: импорт `from src.domain.osv_mapping import account_sort_key`; в импорт enums добавить `PeriodQuarter` (если нет — он уже есть в списке; `DocumentStatus`, `DocumentType`, `FileStatus` — есть). После `unconfirmed_documents`: ```python async def osv_accounts( session: AsyncSession, entity_ids: Collection[uuid.UUID], year: int, quarters: Collection[PeriodQuarter] ) -> dict[uuid.UUID, list[str]]: """Счета ОСВ, загруженные к периоду версии (колонка «ОСВ» панели [34]; front-api-gaps D-3): уникальные `osv_account_code` документов `OSV` ЮЛ за год `year` и кварталы `quarters`; документ не `ERROR`, файл не `SUPERSEDED`/`ERROR`, квартал и код счёта заданы; статус подтверждения и привязка к версии не важны. Порядок — натуральный (`account_sort_key`); ЮЛ без документов в словаре нет.""" if not entity_ids: return {} stmt = ( select(Document.legal_entity_id, Document.osv_account_code) .join(StoredFile, StoredFile.id == Document.stored_file_id) .where( Document.legal_entity_id.in_(list(entity_ids)), Document.document_type == DocumentType.OSV, Document.period_year == year, Document.period_quarter.in_(list(quarters)), Document.status != DocumentStatus.ERROR, StoredFile.status.not_in([FileStatus.SUPERSEDED, FileStatus.ERROR]), Document.osv_account_code.is_not(None), Document.osv_account_code != "", ) .distinct() ) accounts: dict[uuid.UUID, list[str]] = defaultdict(list) for entity_id, code in await session.execute(stmt): accounts[entity_id].append(code) return {entity_id: sorted(codes, key=account_sort_key) for entity_id, codes in accounts.items()} ``` (`period_quarter IN (...)` отбрасывает `NULL` сам — документы без квартала не попадают.) - [ ] **Step 4: Схема участника** `api/src/schemas/consolidations.py`: в импорт enums добавить `EntityType`. Класс: ```python class MemberOut(BaseModel): """ЮЛ периметра версии (front-api-gaps D-2, D-3): тип ЮЛ, самоописывающие ячейки чек-листа в порядке правил подхода и счета ОСВ, загруженные к периоду версии (`osv_accounts` — «что загружено», готовность — в ячейках).""" id: uuid.UUID name: str inn: str entity_type: EntityType is_head_entity: bool checklist: list[ChecklistCellOut] osv_accounts: list[str] ``` - [ ] **Step 5: Сервис** `api/src/services/consolidations.py`: `from src.domain.consolidation_periods import quarters_through, reporting_date`. В `_card` после получения `rules, cells`: ```python accounts = await repo.osv_accounts( self._session, [entity.id for _, entity in members], consolidation.year, quarters_through(consolidation.quarter), ) ``` и в `MemberOut(...)` добавить `entity_type=entity.entity_type,` и `osv_accounts=accounts.get(entity.id, []),`. Docstring `_card`: дописать «у членов — тип ЮЛ и счета ОСВ к периоду версии (D-3)». - [ ] **Step 6: Запустить — проходят** Run: `cd api && .venv/bin/pytest tests/test_openapi_routes.py tests/test_schemas_consolidations.py -v && .venv/bin/pytest -m integration tests/test_api_consolidations.py tests/test_api_consolidation_checklist.py tests/test_api_consolidation_members.py tests/test_consolidation_fixtures.py -v` Expected: PASS. - [ ] **Step 7: Линтеры и коммит** Run: `make lint` → без ошибок. ```bash git add api/src/repositories/consolidations.py api/src/schemas/consolidations.py api/src/services/consolidations.py api/tests/test_api_consolidations.py api/tests/test_openapi_routes.py git commit -m "feat(consolidations): тип ЮЛ и счета ОСВ к периоду версии у участника периметра (#42) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2" ``` --- ### Task 4: Документы workspace (bounded-PR в arm-ka-docs) **Files (репозиторий docs, отдельный worktree):** - Create: `dev/api-contract-files-directories.md` - Modify: `dev/api-contract-slice-4.md`, `dev/api-contract-slice-3b.md` (ссылка), `context/07-open-questions.md` (OQ-43), `context/10-user-journey.md` (шаги 2.1, 2.2, 2.6, 2.5), `mockup/INDEX.md` ([42]) **Interfaces:** - Consumes: итоговые схемы Tasks 1–3 (`ConsolidationCreate`, `ChecklistCellOut`, `MemberOut`); существующие схемы `api/src/schemas/files.py` (`FileOut`, `JobBrief`, `DocumentBrief`), `api/src/schemas/documents.py` (`DocumentOut`, `EntryOut` — имя модуля проверить), `api/src/schemas/directories.py` (`CompanyGroupOut`, `LegalEntityOut`, `RelatedGroupOut`, `RelatedGroupDetail`, `RegionalOfficeOut`, фильтры `CompanyGroupFilter`, фильтр списка ЮЛ), `api/src/schemas/common.py` (`Page`, лимиты), `api/src/api/endpoints/files/*.py` (коды ошибок), `README.md` arm-core («Ошибки API»). - Produces: PR в RSHB/arm-ka-docs (номер — в описание PR arm-core, Task 5). - [ ] **Step 1: Worktree и issue docs** ```bash cd /Users/andrey.epifancev/projects/awp-ca/.wt/front-gaps rm docs # это симлинк на основной checkout docs git -C /Users/andrey.epifancev/projects/awp-ca/docs fetch -q git -C /Users/andrey.epifancev/projects/awp-ca/docs worktree add -b tmp/front-gaps-docs /Users/andrey.epifancev/projects/awp-ca/.wt/front-gaps/docs origin/main touch /Users/andrey.epifancev/projects/awp-ca/.wt/front-gaps/docs/.bounded tea issues create --login home --repo RSHB/arm-ka-docs --title "Документы к arm-core#42: контракт файлов и справочников, OQ-43, модалка [42]" --description "Bounded: контракт для фронта (файлы/документы, справочники, новые поля консолидаций), OQ-43 (решение владельца 07.10.2026, П-3), баннер «Осталось: N» = все несопоставленные (П-1), шаг 2.1 пути и [42] в mockup/INDEX.md (блок «ЮЛ консолидации»), устаревшее «CSV в Срезе 4 нет». Код — RSHB/arm-core#42." git -C /Users/andrey.epifancev/projects/awp-ca/.wt/front-gaps/docs branch -m tmp/front-gaps-docs feat/-front-api-gaps-docs ``` `` — номер созданной issue (из вывода `tea`). `.bounded` в `.gitignore` docs — проверить `git -C … status`: файла в списке быть не должно; если он виден — не добавлять его в коммит. - [ ] **Step 2: Новый контракт `dev/api-contract-files-directories.md`** Структура — как у `dev/api-contract-slice-3b.md` (шапка: назначение, общие правила со ссылкой на `api-contract-slice-4.md` «Общее»; таблица маршрутов; схемы; нюансы). Содержание — **по коду**, не по памяти: 1. Файлы и документы (Срез 2c, используются панелью [39]/[40] и экраном [41]): - `GET /legal-entities/{legal_entity_id}/files` — параметры (`include_superseded`, `limit` (по умолчанию / максимум — из `schemas/common.py`), `offset`), порядок (по убыванию `uploaded_at` — проверить в репозитории), ответ `Page[FileOut]`; опрос во время распознавания — этим же списком или `GET /files/{id}`. - `GET /files/{file_id}` — `FileOut`; `DELETE /files/{file_id}` — 204, 409 `FILE_HAS_CONFIRMED_DOCUMENTS` / `FILE_SUPERSEDES_OTHERS` (коды и тексты — из `endpoints/files/read.py` и README «Ошибки API»). - `POST /legal-entities/{id}/files` — коды `rejected[].code` (`UNSUPPORTED_FILE`, `FILE_TOO_LARGE`, `DUPLICATE_FILE`, `STORAGE_UNAVAILABLE` — сверить с `services/primary_files.py`), 201 vs 422 `{accepted, rejected}` и **второе** тело 422 — доменная ошибка `ENTITY_NOT_IN_PERIMETER` (`{"detail": {"code", "message"}}`): клиент различает по форме. - `GET /documents/{document_id}` (`DocumentOut`), `GET /documents/{document_id}/entries` (`EntryOut`, пагинация). - Схемы `FileOut`, `JobBrief`, `DocumentBrief` — все поля с типами и nullable (из `schemas/files.py`); как показать «Период» в [39] (`period_year` + `period_quarter` + `is_cumulative`: Q3 нарастающий — «2024 · 9 мес.»), «Проверка» (`counterparty_check`; `period_check` всегда `UNKNOWN` — показывать «—», OQ-120), статус строки (файл `UPLOADED`/ `PROCESSING` — «Обработка», `ERROR` — «Ошибка»; документ `RECOGNIZED`/`CONFIRMED` — «Загружен», `NEEDS_CHECK` — «Требуется проверка»; подписи — `10-user-journey.md` «Словарь UI»). - Фильтра по версии консолидации нет: drawer показывает все документы ЮЛ (решение владельца 07.10.2026). 2. Справочники для экранов [03], [36], [42], [60]: `GET /directories/company-groups` (`CompanyGroupOut`, `q` ищет по name/crm_id/code, `legal_entities_count`; имя РФ — через `GET /directories/regional-offices`), `GET /directories/company-groups/{id}/related-groups` (ГСК/ГСЗ ГК, `kind`), `GET /directories/related-groups/{id}` (`RelatedGroupDetail.members`), `GET /directories/legal-entities` (фильтры `company_group_id`, `outside_group`, `q`; `entity_type`, `documents_count`); как собрать модалку [42]: ЮЛ ГК — `legal-entities?company_group_id=`, отметки — члены выбранной ГСК из `related-groups/{id}`; права — `DIRECTORY_VIEW`. - [ ] **Step 3: `dev/api-contract-slice-4.md`** - Таблица маршрутов, `POST /consolidations`: тело `{company_group_id, related_group_id, year, quarter, legal_entity_ids?}`; ошибки + 422 `ENTITY_NOT_IN_GROUP` (`legal_entity_ids`), `PERIMETER_TOO_SMALL` по выбранным; нюанс: без поля / `null` — периметр из ГСК, `[]` — 422; ГСК обязательна, в версии не хранится (П-2), колонка «ГСК» [34] до верификации пустая. - Схемы: `members[]` — `{id, name, inn, entity_type ("ЮЛ"|"ИП"), is_head_entity, checklist[], osv_accounts[]}`; `ChecklistCellOut` — `{document_type, account (string|null), status, missing_quarters[], document_ids[]}`: ячейка самоописывающая, искать по `document_type`/`account`, а не по индексу; `osv_accounts` — счета действующих ОСВ за год версии до её квартала в любом статусе, кроме ошибки; готовность — по ячейкам. - «Ответственный» на [34]: `lock.locked_by_name`, если есть активная блокировка, иначе `initiator.name`. - П-1: «Осталось: N» и «Показать ЮЛ без ИНН» — `summary.unmapped_total` и `GET …/osv?unmapped=true`: все строки свода без сопоставленного ЮЛ, в том числе с ИНН, которого нет в справочнике (решение владельца 07.10.2026). - П-3: «К отчетности» доступна во всех статусах (OQ-43 пересмотрен). - Строка `| [37] Чек-лист | … (CSV в Срезе 4 нет) |` → «… `?format=csv` — CSV (Срез 3b)». - Nullable-поля `ConsolidatedRowOut` (`account_code`, `account_root`, `row_number`, `counterparty_*`, `match_kind`) и `VersionBrief.created_at` — пометить `?`/`| null` (сверить с `schemas/consolidations.py`). - В «Общее» уточнить: лимиты страниц у разных списков разные (сводная ОСВ — 500/2000, файлы и справочники — из `schemas/common.py`); ссылка на новый `api-contract-files-directories.md`. - [ ] **Step 4: OQ-43, путь пользователя, INDEX** - `context/07-open-questions.md`, строка OQ-43: в колонку статуса — «**решено владельцем 07.10.2026 (arm-core#42, П-3):** «К отчетности» доступна во всех статусах начиная с `INITIATED` для ролей с `CONSOLIDATION_VIEW`; правка свода (сопоставление) — только «в работе» со своей блокировкой». Прежнее допущение не удалять — зачеркнуть или оставить с пометкой «было». - `context/10-user-journey.md`: - 2.1: после «ГК → ГСК → Год → Квартал» — «после выбора ГСК — блок **«ЮЛ консолидации»**: все неархивные ЮЛ ГК, отмечены члены выбранной ГСК, отметки меняются; «Выбрано ЮЛ: N. Минимум для консолидации: 2»»; API — `POST /consolidations` с `legal_entity_ids`; периметр — отмеченные ЮЛ. - 2.2: «Ответственный» — правило из Step 3; колонки ОФР / Бух. баланс / ОСВ — `members[].checklist` и `osv_accounts`, «Тип» — `entity_type`. - 2.3: «К отчетности» — во всех статусах (OQ-43). - 2.6: «CSV в Срезе 4 нет» → «CSV — `?format=csv` (Срез 3b)». - 3.1: «Показать ЮЛ без ИНН» = все несопоставленные (П-1). - `mockup/INDEX.md`, строка `42`: в описание добавить «после ГК и ГСК — блок «ЮЛ консолидации» (все неархивные ЮЛ ГК, отмечены члены ГСК, минимум 2; снимок снят до выбора ГК — блока на нём нет)». - [ ] **Step 5: Проверка и коммит docs** Run: `git -C /Users/andrey.epifancev/projects/awp-ca/.wt/front-gaps/docs diff --stat` — изменены только перечисленные файлы; `docs/source/**` не тронут. ```bash git -C /Users/andrey.epifancev/projects/awp-ca/.wt/front-gaps/docs add dev/api-contract-files-directories.md dev/api-contract-slice-4.md dev/api-contract-slice-3b.md context/07-open-questions.md context/10-user-journey.md mockup/INDEX.md git -C /Users/andrey.epifancev/projects/awp-ca/.wt/front-gaps/docs commit -m "docs: контракт файлов и справочников, поля консолидаций #42, OQ-43, модалка [42] Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2" git -C /Users/andrey.epifancev/projects/awp-ca/.wt/front-gaps/docs push -u origin feat/-front-api-gaps-docs tea pulls create --login home --repo RSHB/arm-ka-docs --head feat/-front-api-gaps-docs --base main --title "Документы к arm-core#42: контракт файлов и справочников, OQ-43, модалка [42]" --description "Closes #. Код — RSHB/arm-core#42 (PR ссылается сюда). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2" ``` --- ### Task 5: Завершение — README, CLAUDE.md, проверки, ревью, PR arm-core **Files:** - Modify: `README.md` (раздел «Консолидация (Срез 4)», таблица эндпоинтов ~стр. 636–645; «Ошибки API» ~стр. 843–845) - Modify: `CLAUDE.md` (абзац «Консолидации (Срез 4)») - Modify: `openspec/changes/front-api-gaps/tasks.md` **Interfaces:** - Consumes: Tasks 1–4 (номер docs-PR из Task 4). - Produces: PR в RSHB/arm-core (`Closes #42`). - [ ] **Step 1: README и CLAUDE.md** README, таблица эндпоинтов: - `POST /consolidations` → «создать консолидацию ГК за период и версию №1 (`INITIATED`); периметр — текущие члены ГСК (≥ 2) или выбранные ЮЛ ГК `legal_entity_ids` (≥ 2, front-api-gaps)». - `GET /consolidations/versions/{id}` → «карточка версии: периметр (тип ЮЛ, счета ОСВ к периоду версии), чек-лист по ЮЛ (самоописывающие ячейки), история статусов, блокировка». «Ошибки API»: `PERIMETER_TOO_SMALL` — «в ГСК (при создании без списка), в выбранных ЮЛ (`legal_entity_ids`) или в новом периметре (`PUT …/members`) меньше двух ЮЛ …»; `ENTITY_NOT_IN_GROUP` — «`POST /consolidations` с `legal_entity_ids` и `PUT …/members`: …». После таблицы эндпоинтов абзац: «Доработка #42 (`openspec/changes/front-api-gaps/`): создание с выбранным периметром, ячейки чек-листа с `document_type`/`account`, `members[].entity_type`/`osv_accounts`; контракт — `../docs/dev/api-contract-slice-4.md`, файлы и справочники — `../docs/dev/api-contract-files-directories.md`». CLAUDE.md, абзац «Консолидации (Срез 4)», после перечня сервисов: «Создание и `PUT …/members` проверяют периметр одной функцией `_validate_perimeter` (#42); ячейки чек-листа самоописывающие (`document_type`, `account`); `members[].osv_accounts` — `repositories/consolidations.osv_accounts` (не подходящесть §3, а «что загружено к периоду»)». `openspec/changes/front-api-gaps/tasks.md` — отметить `[x]` выполненные пункты 1–5 (пункт 4 этого tasks.md — README/ CLAUDE.md/OpenAPI — выполнен Tasks 1–3 и этим шагом; пункт 5 — Task 4 с номером docs-PR). - [ ] **Step 2: Слить main и прогнать всё** ```bash git fetch -q origin && git merge --no-edit origin/main make check-full 2>&1 | tail -40 openspec validate front-api-gaps --strict ``` Expected: `check-full` EXIT 0 (unit, integration, `dbml-check`, docker-build, web-check; e2e — skip без профиля `services`); `Change 'front-api-gaps' is valid`. Итоговые числа passed — в описание PR. - [ ] **Step 3: Коммит описания** ```bash git add README.md CLAUDE.md openspec/changes/front-api-gaps/tasks.md git commit -m "docs: README и CLAUDE.md — периметр при создании, ячейки и участник периметра (#42) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2" ``` - [ ] **Step 4: Финальное ревью ветки** Ревью всей ветки (`git diff origin/main...HEAD`) на модели opus против спеки `openspec/changes/front-api-gaps/` и раздела Review Focus. Находки — маркеры `C-n`/`I-n`/`M-n` со статусом `fixed ` / `deferred` / `rejected <причина>`; исправления — отдельными коммитами, повтор `make check-full`. Решения, принятые без владельца, — дописать в `openspec/changes/front-api-gaps/design.md` «Decisions made without discussion» (с № 9). - [ ] **Step 5: Push и PR** ```bash git push -u origin feat/42-front-api-gaps tea pulls create --login home --repo RSHB/arm-core --head feat/42-front-api-gaps --base main \ --title "API до сводной ОСВ: периметр при создании, ячейки чек-листа, участник периметра (#42)" \ --description "<описание>" ``` Описание PR: «Closes #42»; что сделано (3 пункта); docs-PR `RSHB/arm-ka-docs#` (мержить вместе); вывод `make check-full` (числа); таблица находок ревью с маркерами и статусами; «Rulings I made» (решения №1–8 design.md + новые); deferred — отдельным списком; в конце: ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 ``` Merge — только по команде владельца.