test(consolidation): подходящесть неподтверждённых кандидатов чек-листа, правки spec и design по финальному ревью
I-1 (T8.2): тесты, что неподтверждённый документ даёт NEEDS_CHECK/RECOGNIZED только если подходит версии (fits): нарастающий Q2 и квартальный Q4 при версии Q3, другой год и ЮЛ вне периметра не влияют; положительный контроль. M-2: 404 `NOT_FOUND` с сообщением `…_NOT_FOUND`, ключ привязки с признаком нарастающего (R-1, R-14), порядок и режимы блокировок подтверждения (R-3) в spec. M-3: в design #39 и #43 — окно коммита перехода после загрузки. M-5: docstring lock_versions_for_entity про ЮЛ без ГК в периметре. Co-Authored-By: Claude Sonnet <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
98922cc75c
commit
42c253146c
@ -128,8 +128,10 @@ async def lock_versions_for_entity(
|
|||||||
) -> list[ConsolidationVersion]:
|
) -> list[ConsolidationVersion]:
|
||||||
"""Версии со статусом из `statuses`, где ЮЛ — член периметра; блокируются `FOR UPDATE ORDER BY id` (R-2).
|
"""Версии со статусом из `statuses`, где ЮЛ — член периметра; блокируются `FOR UPDATE ORDER BY id` (R-2).
|
||||||
|
|
||||||
Первой берётся advisory-блокировка привязок ГК ЮЛ (`lock_group_bindings`; ЮЛ без ГК — без неё: членом версии оно
|
Первой берётся advisory-блокировка привязок ГК ЮЛ (`lock_group_bindings`; у ЮЛ без ГК её нет): иначе версия,
|
||||||
быть не может): иначе версия, создаваемая параллельно, не видна ни здесь, ни подтверждённый нами документ — там.
|
создаваемая параллельно, не видна ни здесь, ни подтверждённый нами документ — там. ЮЛ без ГК (после выхода из
|
||||||
|
группы или переноса) может оставаться членом версий «в работе» — его ловит ветка `is_member`; такое ЮЛ в периметре
|
||||||
|
заставляет любой `PUT …/members` отвечать `ENTITY_NOT_IN_GROUP`, пока его не уберут из периметра.
|
||||||
|
|
||||||
Блокируется НАДМНОЖЕСТВО: версии этих статусов, где ЮЛ — член ИЛИ `consolidation.company_group_id` равен ГК ЮЛ
|
Блокируется НАДМНОЖЕСТВО: версии этих статусов, где ЮЛ — член ИЛИ `consolidation.company_group_id` равен ГК ЮЛ
|
||||||
(`legal_entity.company_group_id`). Иначе подтверждение документа ∥ `PUT …/members` теряет привязку: подтверждение
|
(`legal_entity.company_group_id`). Иначе подтверждение документа ∥ `PUT …/members` теряет привязку: подтверждение
|
||||||
|
|||||||
@ -151,3 +151,93 @@ async def test_checklist_visible_to_go_curator_hidden_from_foreign_office(
|
|||||||
assert hidden.status_code == unknown.status_code == HTTP_NOT_FOUND
|
assert hidden.status_code == unknown.status_code == HTTP_NOT_FOUND
|
||||||
assert hidden.json() == unknown.json()
|
assert hidden.json() == unknown.json()
|
||||||
assert (await admin.get(url)).status_code == HTTP_FORBIDDEN
|
assert (await admin.get(url)).status_code == HTTP_FORBIDDEN
|
||||||
|
|
||||||
|
|
||||||
|
async def _unconfirmed( # noqa: PLR0913 - тестовая фабрика: ЮЛ, счёт, период, признак и статус документа
|
||||||
|
session: AsyncSession,
|
||||||
|
entity: LegalEntity,
|
||||||
|
*,
|
||||||
|
account: str,
|
||||||
|
year: int = YEAR,
|
||||||
|
quarter: PeriodQuarter = Q3,
|
||||||
|
cumulative: bool = False,
|
||||||
|
status: DocumentStatus = DocumentStatus.NEEDS_CHECK,
|
||||||
|
) -> uuid.UUID:
|
||||||
|
file, job = await make_stored_file(
|
||||||
|
session, entity=entity, status=FileStatus.PROCESSED, job_status=RecognitionStatus.done
|
||||||
|
)
|
||||||
|
document = await make_document(
|
||||||
|
session,
|
||||||
|
file=file,
|
||||||
|
job=job,
|
||||||
|
account=account,
|
||||||
|
year=year,
|
||||||
|
quarter=quarter,
|
||||||
|
status=status,
|
||||||
|
is_cumulative=cumulative,
|
||||||
|
)
|
||||||
|
return document.id
|
||||||
|
|
||||||
|
|
||||||
|
def _column(account: str) -> int:
|
||||||
|
return [account for _, account in EXPECTED_RULES].index(account)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("account", "year", "quarter", "cumulative", "status", "expected"),
|
||||||
|
[
|
||||||
|
# положительный контроль: квартальный Q3 ОСВ версии Q3 подходит -> кандидат виден
|
||||||
|
("76", YEAR, Q3, False, DocumentStatus.NEEDS_CHECK, "NEEDS_CHECK"),
|
||||||
|
# квартальный Q1 не позже квартала версии (Q3) подходит; RECOGNIZED
|
||||||
|
("90", YEAR, Q1, False, DocumentStatus.RECOGNIZED, "RECOGNIZED"),
|
||||||
|
# нарастающий за Q2 при версии Q3 не подходит (нужен нарастающий за квартал версии)
|
||||||
|
("62", YEAR, Q2, True, DocumentStatus.NEEDS_CHECK, "MISSING"),
|
||||||
|
("62", YEAR, Q2, True, DocumentStatus.RECOGNIZED, "MISSING"),
|
||||||
|
# квартальный позже квартала версии не подходит
|
||||||
|
("66", YEAR, PeriodQuarter.Q4, False, DocumentStatus.NEEDS_CHECK, "MISSING"),
|
||||||
|
# другой год не подходит
|
||||||
|
("67", YEAR - 1, Q3, False, DocumentStatus.NEEDS_CHECK, "MISSING"),
|
||||||
|
],
|
||||||
|
ids=["fits-quarterly-q3", "fits-quarterly-q1", "cumulative-q2", "cumulative-q2-recognized", "quarterly-q4", "year"],
|
||||||
|
)
|
||||||
|
async def test_checklist_unconfirmed_candidate_must_fit_version( # noqa: PLR0913, PLR0917 - параметры сценария
|
||||||
|
auth_client: AuthClientFactory,
|
||||||
|
seeded: AsyncSession,
|
||||||
|
account: str,
|
||||||
|
year: int,
|
||||||
|
quarter: PeriodQuarter,
|
||||||
|
cumulative: bool,
|
||||||
|
status: DocumentStatus,
|
||||||
|
expected: str,
|
||||||
|
) -> None:
|
||||||
|
"""Неподтверждённый документ даёт `NEEDS_CHECK`/`RECOGNIZED` только если подходит версии (`fits`; I-1, T8.2)."""
|
||||||
|
version, (first, _) = await _world(seeded)
|
||||||
|
await _unconfirmed(seeded, first, account=account, year=year, quarter=quarter, cumulative=cumulative, status=status)
|
||||||
|
client = await auth_client(KA_RF)
|
||||||
|
|
||||||
|
body = (await client.get(f"/consolidations/versions/{version.id}/checklist")).json()
|
||||||
|
|
||||||
|
cells = _cells(body, "ООО А")
|
||||||
|
assert cells[_column(account)]["status"] == expected
|
||||||
|
others = [cell["status"] for position, cell in enumerate(cells) if position != _column(account)]
|
||||||
|
assert set(others) == {"MISSING"} # прочие столбцы не затронуты
|
||||||
|
assert {cell["status"] for cell in _cells(body, "ООО Б")} == {"MISSING"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_checklist_unconfirmed_of_entity_outside_perimeter_has_no_effect(
|
||||||
|
auth_client: AuthClientFactory, seeded: AsyncSession
|
||||||
|
) -> None:
|
||||||
|
"""Неподтверждённый документ ЮЛ вне периметра (той же ГК) не влияет на ячейки и не добавляет строк (I-1, T8.2)."""
|
||||||
|
version, _ = await _world(seeded)
|
||||||
|
group = (await seeded.execute(select(CompanyGroup).where(CompanyGroup.name == "ГК Русский бисквит"))).scalar_one()
|
||||||
|
outsider = await make_legal_entity(seeded, name="ООО В вне периметра", group=group)
|
||||||
|
await _unconfirmed(seeded, outsider, account="62")
|
||||||
|
await _unconfirmed(seeded, outsider, account="60", status=DocumentStatus.RECOGNIZED)
|
||||||
|
client = await auth_client(KA_RF)
|
||||||
|
|
||||||
|
body = (await client.get(f"/consolidations/versions/{version.id}/checklist")).json()
|
||||||
|
|
||||||
|
assert [row["legal_entity_name"] for row in body["rows"]] == ["ООО А", "ООО Б"]
|
||||||
|
assert all({cell["status"] for cell in row["cells"]} == {"MISSING"} for row in body["rows"])
|
||||||
|
|||||||
@ -223,11 +223,11 @@ legal_entity_name, account_code, missing_quarters[]}}`. Фильтры `GET …/
|
|||||||
| 36 | 2026-10-06 | Переход версии при подтверждении пишется `transition(...)` с `user_id` подтвердившего, но без IP/user-agent (сигнатура хука `on_document_confirmed(session, audit: AuditService, *, document, user_id, now)` фиксирована планом); `transition` импортируется внутри хука (цикл `consolidation_status → consolidations → consolidation_documents`) | audit-trail требует для автоматического перехода только `user_id` инициатора | в `CONSOLIDATION_STATUS_CHANGE` автоматического перехода нет IP/UA — они есть в соседнем `DOCUMENT_CONFIRM` той же транзакции |
|
| 36 | 2026-10-06 | Переход версии при подтверждении пишется `transition(...)` с `user_id` подтвердившего, но без IP/user-agent (сигнатура хука `on_document_confirmed(session, audit: AuditService, *, document, user_id, now)` фиксирована планом); `transition` импортируется внутри хука (цикл `consolidation_status → consolidations → consolidation_documents`) | audit-trail требует для автоматического перехода только `user_id` инициатора | в `CONSOLIDATION_STATUS_CHANGE` автоматического перехода нет IP/UA — они есть в соседнем `DOCUMENT_CONFIRM` той же транзакции |
|
||||||
| 37 | 2026-10-06 | Отдельная транзакция перехода после загрузки (`after_upload_to_version`) берётся из `app.state.session_audit.transaction` (та же фабрика `session_transaction`, что у аудита входа; без неё — `src.db.db.session_transaction`); `get_primary_files_service` передаёт её в `PrimaryFilesService(…, transaction)` | отдельной зависимости «фабрика транзакций» в приложении нет, а фикстуры тестов уже подставляют фабрику в `SessionAudit`; не меняем `app.py` и `conftest` | если `SessionAudit` когда-либо уйдёт из `app.state`, сервис молча возьмёт боевой `session_transaction` (в тестах это была бы чужая БД) |
|
| 37 | 2026-10-06 | Отдельная транзакция перехода после загрузки (`after_upload_to_version`) берётся из `app.state.session_audit.transaction` (та же фабрика `session_transaction`, что у аудита входа; без неё — `src.db.db.session_transaction`); `get_primary_files_service` передаёт её в `PrimaryFilesService(…, transaction)` | отдельной зависимости «фабрика транзакций» в приложении нет, а фикстуры тестов уже подставляют фабрику в `SessionAudit`; не меняем `app.py` и `conftest` | если `SessionAudit` когда-либо уйдёт из `app.state`, сервис молча возьмёт боевой `session_transaction` (в тестах это была бы чужая БД) |
|
||||||
| 38 | 2026-10-06 | Переход после загрузки пишет `CONSOLIDATION_STATUS_CHANGE` с IP и user-agent запроса (в отличие от хука подтверждения, №36); `transition` вызывается из сервиса загрузки с `RequestAudit`-контекстом, но в своей транзакции и своим `AuditService(session)` | сервис знает IP/UA запроса, а `transition` их принимает; потери информации нет | нет |
|
| 38 | 2026-10-06 | Переход после загрузки пишет `CONSOLIDATION_STATUS_CHANGE` с IP и user-agent запроса (в отличие от хука подтверждения, №36); `transition` вызывается из сервиса загрузки с `RequestAudit`-контекстом, но в своей транзакции и своим `AuditService(session)` | сервис знает IP/UA запроса, а `transition` их принимает; потери информации нет | нет |
|
||||||
| 39 | 2026-10-06 | Версия в статусе `DOCUMENTS_PROCESSING`/`DATA_CORRECTION` при загрузке не меняется, `after_upload_to_version` — no-op; если между проверкой и переходом версию аннулировали/перевели, переход не выполняется (статус перечитывается под `FOR UPDATE`), загрузка при этом остаётся принятой (201) | проверка до приёма и переход после — две разные транзакции, окно между ними неустранимо без блокировки версии, которую спека запрещает | версия, аннулированная в окне, остаётся аннулированной; файл принят и виден в ЮЛ, но к версии не привязан (привязка — только подтверждением). Окно покрывает и удаление ЮЛ из периметра между проверкой и переходом: версия всё равно перейдёт в `DOCUMENTS_PROCESSING` — допустимо. Отдельная транзакция перехода открывает второе подключение к БД, пока запрос держит своё: при множестве параллельных загрузок в контексте версии это давит на пул (по умолчанию 5+10) — пока теоретически |
|
| 39 | 2026-10-06 | Версия в статусе `DOCUMENTS_PROCESSING`/`DATA_CORRECTION` при загрузке не меняется, `after_upload_to_version` — no-op; если между проверкой и переходом версию аннулировали/перевели, переход не выполняется (статус перечитывается под `FOR UPDATE`), загрузка при этом остаётся принятой (201) | проверка до приёма и переход после — две разные транзакции, окно между ними неустранимо без блокировки версии, которую спека запрещает | версия, аннулированная в окне, остаётся аннулированной; файл принят и виден в ЮЛ, но к версии не привязан (привязка — только подтверждением). Окно покрывает и удаление ЮЛ из периметра между проверкой и переходом: версия всё равно перейдёт в `DOCUMENTS_PROCESSING` — допустимо. Отдельная транзакция перехода открывает второе подключение к БД, пока запрос держит своё: при множестве параллельных загрузок в контексте версии это давит на пул (по умолчанию 5+10) — пока теоретически Переход после загрузки фиксируется отдельной транзакцией ДО коммита транзакции запроса с файлами: если коммит запроса затем упадёт, версия может остаться в `DOCUMENTS_PROCESSING` без файлов (исправимо: вручную вернуть в `INITIATED`); возможное последующее исправление — переход после коммита запроса. |
|
||||||
| 40 | 2026-10-06 | `on_documents_mapped` блокирует через `lock_versions_for_entity` надмножество по `IN_WORK_STATUSES` и фильтрует `DOCUMENTS_PROCESSING` ПОСЛЕ блокировки, а не передаёт `statuses={DOCUMENTS_PROCESSING}`; вызывается в `_map_candidate` ПОСЛЕ `map_file`, то есть после автозамены | при фильтре статуса в блокирующем запросе версия, которую параллельно переводят `INITIATED → DOCUMENTS_PROCESSING`, не ждалась бы; порядок «файл → документы → advisory ГК → версии» сохраняется, автозамена внутри `map_file` берёт advisory ЮЛ → старые файлы → advisory ГК → версии раньше хука; вызов до автозамены держал бы версии и затем ждал старые файлы (встречная блокировка с подтверждением) | блокируется чуть больше версий ГК (все «в работе»), на время короткой транзакции маппинга |
|
| 40 | 2026-10-06 | `on_documents_mapped` блокирует через `lock_versions_for_entity` надмножество по `IN_WORK_STATUSES` и фильтрует `DOCUMENTS_PROCESSING` ПОСЛЕ блокировки, а не передаёт `statuses={DOCUMENTS_PROCESSING}`; вызывается в `_map_candidate` ПОСЛЕ `map_file`, то есть после автозамены | при фильтре статуса в блокирующем запросе версия, которую параллельно переводят `INITIATED → DOCUMENTS_PROCESSING`, не ждалась бы; порядок «файл → документы → advisory ГК → версии» сохраняется, автозамена внутри `map_file` берёт advisory ЮЛ → старые файлы → advisory ГК → версии раньше хука; вызов до автозамены держал бы версии и затем ждал старые файлы (встречная блокировка с подтверждением) | блокируется чуть больше версий ГК (все «в работе»), на время короткой транзакции маппинга |
|
||||||
| 41 | 2026-10-06 | Хук получает документы файла отдельным `SELECT` по `stored_file_id` после `map_file` (возвращаемое значение `map_file` — предупреждения — не менялось) | сигнатура `map_file` и её тесты не трогаются; документы только что записаны в этой же транзакции | один лишний запрос на файл |
|
| 41 | 2026-10-06 | Хук получает документы файла отдельным `SELECT` по `stored_file_id` после `map_file` (возвращаемое значение `map_file` — предупреждения — не менялось) | сигнатура `map_file` и её тесты не трогаются; документы только что записаны в этой же транзакции | один лишний запрос на файл |
|
||||||
| 42 | 2026-10-06 | Переход по маппингу — только если среди документов файла есть `NEEDS_CHECK`, подходящий версии по `fits` (`fits` — D-5: период, год, член); статус файла/документа в других статусах (`RECOGNIZED`) переход не вызывает, привязки не создаются | спека osv-result-mapping | версия с неподходящим по периоду `NEEDS_CHECK` документом остаётся в `DOCUMENTS_PROCESSING` до подтверждения подходящего |
|
| 42 | 2026-10-06 | Переход по маппингу — только если среди документов файла есть `NEEDS_CHECK`, подходящий версии по `fits` (`fits` — D-5: период, год, член); статус файла/документа в других статусах (`RECOGNIZED`) переход не вызывает, привязки не создаются | спека osv-result-mapping | версия с неподходящим по периоду `NEEDS_CHECK` документом остаётся в `DOCUMENTS_PROCESSING` до подтверждения подходящего |
|
||||||
| 43 | 2026-10-06 | R-10: ошибка (`Exception`) отдельной транзакции перехода после загрузки (`after_upload_to_version`) логируется (`logger.exception`, `error_category=version_transition_error`) и не меняет ответ: 201 с принятыми файлами; `BaseException` (`asyncio.CancelledError`) не перехватывается | приём файлов важнее побочного статуса; иначе 500 откатил бы транзакцию запроса, а принятые объекты S3 остались бы сиротами (как в `SessionAudit`, ошибка записи побочного события ответ не меняет) | версия может остаться `INITIATED` после загрузки до следующей загрузки или подтверждения документа; сбой виден только в логе |
|
| 43 | 2026-10-06 | R-10: ошибка (`Exception`) отдельной транзакции перехода после загрузки (`after_upload_to_version`) логируется (`logger.exception`, `error_category=version_transition_error`) и не меняет ответ: 201 с принятыми файлами; `BaseException` (`asyncio.CancelledError`) не перехватывается | приём файлов важнее побочного статуса; иначе 500 откатил бы транзакцию запроса, а принятые объекты S3 остались бы сиротами (как в `SessionAudit`, ошибка записи побочного события ответ не меняет) | версия может остаться `INITIATED` после загрузки до следующей загрузки или подтверждения документа; сбой виден только в логе Переход после загрузки фиксируется отдельной транзакцией ДО коммита транзакции запроса с файлами: если коммит запроса затем упадёт, версия может остаться в `DOCUMENTS_PROCESSING` без файлов (исправимо: вручную вернуть в `INITIATED`); возможное последующее исправление — переход после коммита запроса. |
|
||||||
| 44 | 2026-10-06 | Статус ячейки — одно определение: `domain.checklist.CellStatus` (StrEnum); `ChecklistCellOut.status` в схеме API использует его, `Literal` `ChecklistCellStatus` удалён (T2.8) | два расходящихся определения — источник тихого рассинхрона домена и контракта | нулевая: OpenAPI-enum тот же набор значений |
|
| 44 | 2026-10-06 | Статус ячейки — одно определение: `domain.checklist.CellStatus` (StrEnum); `ChecklistCellOut.status` в схеме API использует его, `Literal` `ChecklistCellStatus` удалён (T2.8) | два расходящихся определения — источник тихого рассинхрона домена и контракта | нулевая: OpenAPI-enum тот же набор значений |
|
||||||
| 45 | 2026-10-06 | Проверка «подходящести» неподтверждённых документов (`fits`: член периметра, год, квартал) выполняется в сервисе (`_checklist_cells`), в домен `build` приходят уже подходящие; домен сам сверяет только ЮЛ, тип, счёт и статус (`RECOGNIZED`/`NEEDS_CHECK`) | `fits` живёт в `services.consolidation_documents` (домен не импортирует сервисы и ORM); дублировать предикат в домене — риск расхождения с привязкой | если `fits` изменится — меняется и чек-лист, что желательно; вне `fits` домен кандидатов не отсеивает |
|
| 45 | 2026-10-06 | Проверка «подходящести» неподтверждённых документов (`fits`: член периметра, год, квартал) выполняется в сервисе (`_checklist_cells`), в домен `build` приходят уже подходящие; домен сам сверяет только ЮЛ, тип, счёт и статус (`RECOGNIZED`/`NEEDS_CHECK`) | `fits` живёт в `services.consolidation_documents` (домен не импортирует сервисы и ORM); дублировать предикат в домене — риск расхождения с привязкой | если `fits` изменится — меняется и чек-лист, что желательно; вне `fits` домен кандидатов не отсеивает |
|
||||||
| 46 | 2026-10-06 | Выбор источников ОСВ в чек-листе — локальная функция `_osv_coverage` (нарастающий за квартал версии, иначе квартальные Q1..Qn все; нарастающий приоритетнее; пробел → `missing_quarters` + документы имеющихся кварталов), помечена `# Task 9 заменит на consolidation_periods.select_sources`; при полном отсутствии документов `missing_quarters = []` (перечисляются только при частичном покрытии, как в спеке) | `select_sources` появится в Task 9; план прямо разрешает локальную проверку до него | до Task 9 два места с правилом выбора; Task 9 удаляет локальное |
|
| 46 | 2026-10-06 | Выбор источников ОСВ в чек-листе — локальная функция `_osv_coverage` (нарастающий за квартал версии, иначе квартальные Q1..Qn все; нарастающий приоритетнее; пробел → `missing_quarters` + документы имеющихся кварталов), помечена `# Task 9 заменит на consolidation_periods.select_sources`; при полном отсутствии документов `missing_quarters = []` (перечисляются только при частичном покрытии, как в спеке) | `select_sources` появится в Task 9; план прямо разрешает локальную проверку до него | до Task 9 два места с правилом выбора; Task 9 удаляет локальное |
|
||||||
|
|||||||
@ -100,7 +100,7 @@ MUST NOT агрегироваться между контрагентами. `co
|
|||||||
### Requirement: Ручное сопоставление контрагента
|
### Requirement: Ручное сопоставление контрагента
|
||||||
`POST …/osv/entries/{entry_id}/counterparty` (`CONSOLIDATION_EDIT`, policy, статус «в работе», своя блокировка) с телом
|
`POST …/osv/entries/{entry_id}/counterparty` (`CONSOLIDATION_EDIT`, policy, статус «в работе», своя блокировка) с телом
|
||||||
`{legal_entity_id, move_alias = false}` SHALL в одной транзакции: проверить, что строка принадлежит ОСВ-документу,
|
`{legal_entity_id, move_alias = false}` SHALL в одной транзакции: проверить, что строка принадлежит ОСВ-документу,
|
||||||
привязанному к версии (иначе 404 `ENTRY_NOT_FOUND`), и что ЮЛ существует (без policy; иначе 404 `LEGAL_ENTITY_NOT_FOUND`);
|
привязанному к версии (иначе 404 `NOT_FOUND` с сообщением `ENTRY_NOT_FOUND`; код фиксированный, идентификатор — текст сообщения), и что ЮЛ существует (без policy; иначе 404 `NOT_FOUND` с сообщением `LEGAL_ENTITY_NOT_FOUND`);
|
||||||
вычислить `normalized = normalize_alias(counterparty_name)`: пустой → алиас не создаётся; алиас с таким `normalized`
|
вычислить `normalized = normalize_alias(counterparty_name)`: пустой → алиас не создаётся; алиас с таким `normalized`
|
||||||
у других ЮЛ и `move_alias = false` → 409 `ALIAS_TAKEN` с `owners[] {legal_entity_id, name}`; с `move_alias = true` →
|
у других ЮЛ и `move_alias = false` → 409 `ALIAS_TAKEN` с `owners[] {legal_entity_id, name}`; с `move_alias = true` →
|
||||||
их алиасы удаляются (`audit_event(LEGAL_ENTITY_CHANGE)` на каждый); у выбранного ЮЛ алиаса нет → создаётся
|
их алиасы удаляются (`audit_event(LEGAL_ENTITY_CHANGE)` на каждый); у выбранного ЮЛ алиаса нет → создаётся
|
||||||
|
|||||||
@ -43,7 +43,8 @@ Q4 → 01.01.year+1; ASSUMPTION(OQ-40)), `consolidation_version` №1 (`INITIATE
|
|||||||
версии и консолидации, `company_group {id, name}`, инициатора и его офис, `lock`, `members[]` (ЮЛ: `id`, `name`, `inn`,
|
версии и консолидации, `company_group {id, name}`, инициатора и его офис, `lock`, `members[]` (ЮЛ: `id`, `name`, `inn`,
|
||||||
`is_head_entity`, `checklist[]` — ячейки по правилам подхода, см. «Чек-лист комплекта»), `history[]` (`from_status`,
|
`is_head_entity`, `checklist[]` — ячейки по правилам подхода, см. «Чек-лист комплекта»), `history[]` (`from_status`,
|
||||||
`to_status`, `changed_by`, `changed_at`, `comment`). Консолидация ГК вне видимости и несуществующий идентификатор —
|
`to_status`, `changed_by`, `changed_at`, `comment`). Консолидация ГК вне видимости и несуществующий идентификатор —
|
||||||
404 `CONSOLIDATION_NOT_FOUND` / `VERSION_NOT_FOUND` с одинаковым телом.
|
404 `NOT_FOUND` с сообщением `CONSOLIDATION_NOT_FOUND` / `VERSION_NOT_FOUND` и одинаковым телом (так же во всех
|
||||||
|
спецификациях среза: идентификаторы `…_NOT_FOUND` — текст сообщения при фиксированном коде `NOT_FOUND`).
|
||||||
|
|
||||||
#### Scenario: Дерево версий
|
#### Scenario: Дерево версий
|
||||||
- **WHEN** у ГК консолидации 2024 Q1 (версии 1 и 2) и 2024 Q3 (версия 1), запрошен список
|
- **WHEN** у ГК консолидации 2024 Q1 (версии 1 и 2) и 2024 Q3 (версия 1), запрошен список
|
||||||
@ -51,7 +52,7 @@ Q4 → 01.01.year+1; ASSUMPTION(OQ-40)), `consolidation_version` №1 (`INITIATE
|
|||||||
|
|
||||||
#### Scenario: Чужой филиал
|
#### Scenario: Чужой филиал
|
||||||
- **WHEN** `ka_rf` другого филиала запрашивает карточку версии
|
- **WHEN** `ka_rf` другого филиала запрашивает карточку версии
|
||||||
- **THEN** 404 `VERSION_NOT_FOUND`, тело совпадает с ответом на случайный `uuid`
|
- **THEN** 404 `NOT_FOUND` (сообщение `VERSION_NOT_FOUND`), тело совпадает с ответом на случайный `uuid`
|
||||||
|
|
||||||
### Requirement: Периметр версии
|
### Requirement: Периметр версии
|
||||||
`PUT /consolidations/versions/{id}/members` (`CONSOLIDATION_EDIT`, policy, статус «в работе», своя активная блокировка)
|
`PUT /consolidations/versions/{id}/members` (`CONSOLIDATION_EDIT`, policy, статус «в работе», своя активная блокировка)
|
||||||
|
|||||||
@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
### Requirement: Видимость консолидаций и редактируемость версии
|
### Requirement: Видимость консолидаций и редактируемость версии
|
||||||
Консолидация и её версии SHALL быть видимы тем же пользователям, что и её ГК (`company_group`); чужие и
|
Консолидация и её версии SHALL быть видимы тем же пользователям, что и её ГК (`company_group`); чужие и
|
||||||
несуществующие — 404 с одинаковым телом (`CONSOLIDATION_NOT_FOUND` / `VERSION_NOT_FOUND`). Правка версии (периметр,
|
несуществующие — 404 с одинаковым телом (код `NOT_FOUND`, сообщение `CONSOLIDATION_NOT_FOUND` / `VERSION_NOT_FOUND`;
|
||||||
|
так устроены все 404 этого среза: `NotFoundError` несёт только текст сообщения). Правка версии (периметр,
|
||||||
ручная смена статуса, сопоставление контрагента) SHALL проходить проверки в порядке: право → видимость → статус
|
ручная смена статуса, сопоставление контрагента) SHALL проходить проверки в порядке: право → видимость → статус
|
||||||
версии ∈ {`INITIATED`, `DOCUMENTS_PROCESSING`, `DATA_CORRECTION`} (иначе 409 `VERSION_NOT_EDITABLE`) → активная
|
версии ∈ {`INITIATED`, `DOCUMENTS_PROCESSING`, `DATA_CORRECTION`} (иначе 409 `VERSION_NOT_EDITABLE`) → активная
|
||||||
`consolidation_lock` есть (иначе 409 `LOCK_REQUIRED`) и принадлежит пользователю (иначе 409 `LOCKED_BY_OTHER` с
|
`consolidation_lock` есть (иначе 409 `LOCK_REQUIRED`) и принадлежит пользователю (иначе 409 `LOCKED_BY_OTHER` с
|
||||||
|
|||||||
@ -7,8 +7,9 @@
|
|||||||
или `NEEDS_CHECK` в `CONFIRMED` (`confirmed_by`, `confirmed_at`) с `audit_event(DOCUMENT_CONFIRM, PRIMARY_DOCUMENTS)`;
|
или `NEEDS_CHECK` в `CONFIRMED` (`confirmed_by`, `confirmed_at`) с `audit_event(DOCUMENT_CONFIRM, PRIMARY_DOCUMENTS)`;
|
||||||
из `CONFIRMED` повторно — 409 `ALREADY_CONFIRMED`; документ файла `SUPERSEDED` — 409 `FILE_SUPERSEDED`; документ в любом
|
из `CONFIRMED` повторно — 409 `ALREADY_CONFIRMED`; документ файла `SUPERSEDED` — 409 `FILE_SUPERSEDED`; документ в любом
|
||||||
другом статусе (например, `ERROR`) — 409 `DOCUMENT_NOT_CONFIRMABLE`. Если совпали несколько условий, приоритет у
|
другом статусе (например, `ERROR`) — 409 `DOCUMENT_NOT_CONFIRMABLE`. Если совпали несколько условий, приоритет у
|
||||||
`FILE_SUPERSEDED`. Подтверждение берёт блокировки в порядке «файл → документ → версии консолидации (по возрастанию
|
`FILE_SUPERSEDED`. Подтверждение берёт блокировки в порядке «файл → документ → (advisory-блокировка привязок ГК) → версии консолидации
|
||||||
`id`)» и перечитывает их под `FOR UPDATE`; документ или файл, исчезнувшие за время ожидания блокировки, — 404 `NOT_FOUND`.
|
(по возрастанию `id`)»: файл — `FOR UPDATE`, документ — `FOR NO KEY UPDATE` (R-3; совместима со вставкой привязки
|
||||||
|
`FOR KEY SHARE`), версии — `FOR UPDATE`; состояние перечитывается под этими блокировками; документ или файл, исчезнувшие за время ожидания блокировки, — 404 `NOT_FOUND`.
|
||||||
В той же транзакции документ SHALL привязываться (`consolidation_document`) ко всем версиям «в работе» (`INITIATED`,
|
В той же транзакции документ SHALL привязываться (`consolidation_document`) ко всем версиям «в работе» (`INITIATED`,
|
||||||
`DOCUMENTS_PROCESSING`, `DATA_CORRECTION`), которым он подходит: ЮЛ документа ∈ `consolidation_member` версии,
|
`DOCUMENTS_PROCESSING`, `DATA_CORRECTION`), которым он подходит: ЮЛ документа ∈ `consolidation_member` версии,
|
||||||
`period_year` = год версии, `period_quarter` не NULL и (`is_cumulative` true или NULL → `period_quarter` = квартал
|
`period_year` = год версии, `period_quarter` не NULL и (`is_cumulative` true или NULL → `period_quarter` = квартал
|
||||||
@ -49,8 +50,9 @@
|
|||||||
|
|
||||||
### Requirement: Подхват документов при создании версии и расширении периметра
|
### Requirement: Подхват документов при создании версии и расширении периметра
|
||||||
При создании версии и при добавлении ЮЛ в периметр ядро SHALL привязать уже подтверждённые документы этого ЮЛ из
|
При создании версии и при добавлении ЮЛ в периметр ядро SHALL привязать уже подтверждённые документы этого ЮЛ из
|
||||||
незаменённых файлов, подходящие версии (условия — как при подтверждении); на одну тройку (`document_type`,
|
незаменённых файлов, подходящие версии (условия — как при подтверждении); на один ключ (`document_type`,
|
||||||
`osv_account_code`, `period_quarter`) — документ с самым поздним `confirmed_at`. Удаление ЮЛ из периметра SHALL
|
`osv_account_code`, `period_quarter`, признак «нарастающий»; NULL признака — нарастающий; для ОСВ за Q1 признак не
|
||||||
|
различается, R-1, R-14) — документ с самым поздним `confirmed_at`. Удаление ЮЛ из периметра SHALL
|
||||||
снимать привязку всех его документов в этой версии. Подхват и отвязка статус версии MUST NOT менять.
|
снимать привязку всех его документов в этой версии. Подхват и отвязка статус версии MUST NOT менять.
|
||||||
|
|
||||||
#### Scenario: Фикстуры, подтверждённые до консолидации
|
#### Scenario: Фикстуры, подтверждённые до консолидации
|
||||||
|
|||||||
@ -16,7 +16,7 @@ queued`, `pipeline` по типу: xls/xlsx/xlsb → `tabular`, pdf/png/jpg →
|
|||||||
`message`); код ответа 201, если принят хотя бы один, иначе 422. Тип документа и период при загрузке MUST NOT
|
`message`); код ответа 201, если принят хотя бы один, иначе 422. Тип документа и период при загрузке MUST NOT
|
||||||
запрашиваться (макет `40`). Без доступного S3 — 503 `STORAGE_UNAVAILABLE`.
|
запрашиваться (макет `40`). Без доступного S3 — 503 `STORAGE_UNAVAILABLE`.
|
||||||
Необязательный query-параметр `consolidation_version_id` SHALL проверяться до приёма файлов: версия видима (иначе 404
|
Необязательный query-параметр `consolidation_version_id` SHALL проверяться до приёма файлов: версия видима (иначе 404
|
||||||
`VERSION_NOT_FOUND`), статус «в работе» (иначе 409 `VERSION_NOT_EDITABLE`), ЮЛ ∈ периметру (иначе 422
|
`NOT_FOUND` с сообщением `VERSION_NOT_FOUND`), статус «в работе» (иначе 409 `VERSION_NOT_EDITABLE`), ЮЛ ∈ периметру (иначе 422
|
||||||
`ENTITY_NOT_IN_PERIMETER`); если принят хотя бы один файл и версия в `INITIATED`, она SHALL перейти в
|
`ENTITY_NOT_IN_PERIMETER`); если принят хотя бы один файл и версия в `INITIATED`, она SHALL перейти в
|
||||||
`DOCUMENTS_PROCESSING` (`changed_by` = загрузивший) в отдельной транзакции после приёма файлов. Блокировка версии
|
`DOCUMENTS_PROCESSING` (`changed_by` = загрузивший) в отдельной транзакции после приёма файлов. Блокировка версии
|
||||||
для загрузки MUST NOT требоваться.
|
для загрузки MUST NOT требоваться.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user