Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
60 KiB
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—"ЮЛ"/"ИП"(enumEntityType, как в DBML). - Сортировка счетов — только
src.domain.osv_mapping.account_sort_key(одно определение порядка счетов). - Язык комментариев, docstring и коммитов — русский; идентификаторы — английские. Docstring ссылается на спеку/design.
- Каждый коммит заканчивается строками:
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 - Сообщения коммитов —
feat(consolidations): …,test(consolidations): …,docs: …по-русски.
Review Focus
- Пустой список
legal_entity_ids: []— это явный периметр из нуля ЮЛ, а не «поле не передано»: ожидается 422PERIMETER_TOO_SMALL, а не периметр из ГСК. Тест — Task 1,test_create_with_empty_selected_perimeter_is_too_small. - Несуществующий
uuidвlegal_entity_ids— ожидается 422ENTITY_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 добавить:
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 — тесты:
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 в конец файла:
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:
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) добавить:
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 одной строкой:
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 (по ГСК):
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: Коммит
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 <noreply@anthropic.com>
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:
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:
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 добавить:
@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 — конструкторы:
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 …. Добавить тест:
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 ячейки:
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:
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 — в конец:
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: Схема ячейки
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:
@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 → без ошибок.
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 <noreply@anthropic.com>
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. Добавить в конец файла:
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 — в конец:
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:
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. Класс:
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:
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 → без ошибок.
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 <noreply@anthropic.com>
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.mdarm-core («Ошибки API»). -
Produces: PR в RSHB/arm-ka-docs (номер — в описание PR arm-core, Task 5).
-
Step 1: Worktree и issue docs
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/<N>-front-api-gaps-docs
<N> — номер созданной 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 «Общее»; таблица маршрутов; схемы; нюансы). Содержание — по коду, не по памяти:
- Файлы и документы (Срез 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, 409FILE_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).
- Справочники для экранов [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?}; ошибки + 422ENTITY_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).
- 2.1: после «ГК → ГСК → Год → Квартал» — «после выбора ГСК — блок «ЮЛ консолидации»: все неархивные ЮЛ ГК,
отмечены члены выбранной ГСК, отметки меняются; «Выбрано ЮЛ: N. Минимум для консолидации: 2»»; API —
-
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/** не тронут.
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2"
git -C /Users/andrey.epifancev/projects/awp-ca/.wt/front-gaps/docs push -u origin feat/<N>-front-api-gaps-docs
tea pulls create --login home --repo RSHB/arm-ka-docs --head feat/<N>-front-api-gaps-docs --base main --title "Документы к arm-core#42: контракт файлов и справочников, OQ-43, модалка [42]" --description "Closes #<N>. Код — 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 и прогнать всё
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: Коммит описания
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 <noreply@anthropic.com>
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 <commit> / deferred / rejected <причина>;
исправления — отдельными коммитами, повтор make check-full. Решения, принятые без владельца, — дописать в
openspec/changes/front-api-gaps/design.md «Decisions made without discussion» (с № 9).
- Step 5: Push и PR
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#<M> (мержить вместе); вывод
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 — только по команде владельца.