fix(files): удаление файла убирает формы ББ/ОФР и комментарии документов (C-1, T8.2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
parent
234e830476
commit
dd7f7cc42d
@ -574,8 +574,8 @@ S3 (storage); без S3 — предупреждение в логе, цикл
|
||||
`{column_index, relation, expected, actual}`). Мягкие проверки (в т. ч. межформенная `1370 − 1370(начало года) = 2400`)
|
||||
подтверждение не блокируют. Подтверждённая форма привязывается к версиям консолидации и закрывает ячейки ББ/ОФР чек-листа, как ОСВ.
|
||||
- **Порядок блокировок** правки и акцепта — «файл → документ» (как у подтверждения); шапка перечитывается под блокировкой.
|
||||
Комментарии аудит не пишут; при удалении файла комментарии его документов остаются в таблице `comment` без владельца
|
||||
(полиморфная связь без FK, решение №18; наружу не отдаются).
|
||||
Комментарии аудит не пишут; при удалении файла комментарии его документов удаляются вместе с документами
|
||||
(полиморфная связь без FK; решение №18 закрыто).
|
||||
|
||||
Расчёт — `src/domain/statement_mapping.py` и `src/domain/statement_checks.py` (чистые функции), доступ к данным —
|
||||
`src/repositories/statements.py`, правка и акцепт — `src/services/statements.py`.
|
||||
@ -590,7 +590,7 @@ S3 (storage); без S3 — предупреждение в логе, цикл
|
||||
Документ без года или квартала в замене не участвует. Если новый файл содержит лишь часть документов прежнего, прежний
|
||||
уходит в `SUPERSEDED` целиком (OQ-115). Автозамены одного ЮЛ идут под advisory-блокировкой.
|
||||
|
||||
**Удаление.** `DELETE /files/{id}` удаляет строки ОСВ, документы, задания и `stored_file`, проставляет объектам S3
|
||||
**Удаление.** `DELETE /files/{id}` удаляет формы ББ/ОФР (шапки, колонки, строки, значения), комментарии документов, строки ОСВ, документы, задания и `stored_file`, проставляет объектам S3
|
||||
(`uploads`, `results`) из папок этого файла (`uploads/{file_id}/`, `results/{file_id}/`) `expires_at = now()`
|
||||
(физически объект не удаляется — ретеншн OQ-22, OQ-111) и пишет `FILE_DELETE` с карточкой файла в `before_value`. Нельзя удалить файл с документом `CONFIRMED` (`409 FILE_HAS_CONFIRMED_DOCUMENTS`)
|
||||
и файл, который заменил другие (`409 FILE_SUPERSEDES_OTHERS`); `SUPERSEDED` файл удалить можно. Блокировки — в порядке
|
||||
|
||||
@ -22,6 +22,7 @@ from src.models.directories import LegalEntity
|
||||
from src.models.enums import FileStatus, S3BucketRole
|
||||
from src.models.files import Document, RecognitionJob, S3Object, StoredFile
|
||||
from src.models.osv import OsvEntry
|
||||
from src.repositories import statements as statements_repo
|
||||
|
||||
COMMENT_OBJECT_TYPE = "document" # `comment.object_type` комментариев документа
|
||||
|
||||
@ -199,12 +200,18 @@ async def supersedes_others(session: AsyncSession, file_id: uuid.UUID) -> bool:
|
||||
|
||||
|
||||
async def delete_file_rows(session: AsyncSession, file: StoredFile) -> None:
|
||||
"""Удаляет строки ОСВ, документы, все задания и сам файл. FK-цикл `stored_file.current_recognition_job_id` ↔
|
||||
`recognition_job.file_id` разрывается обнулением ссылки до удаления заданий. Блокировки — у вызывающего."""
|
||||
"""Удаляет формы ББ/ОФР (шапки, колонки, строки, значения), комментарии документов, строки ОСВ, документы, все
|
||||
задания и сам файл (формы и комментарии — до документов и заданий: на них FK без CASCADE). FK-цикл
|
||||
`stored_file.current_recognition_job_id` ↔ `recognition_job.file_id` разрывается обнулением ссылки до удаления
|
||||
заданий. Блокировки — у вызывающего."""
|
||||
file_id = file.id
|
||||
file.current_recognition_job_id = None
|
||||
await session.flush()
|
||||
document_ids = select(Document.id).where(Document.stored_file_id == file_id)
|
||||
await statements_repo.delete_file_statements(session, file_id)
|
||||
await session.execute(
|
||||
delete(Comment).where(Comment.object_type == COMMENT_OBJECT_TYPE, Comment.object_id.in_(document_ids))
|
||||
)
|
||||
await session.execute(delete(OsvEntry).where(OsvEntry.document_id.in_(document_ids)))
|
||||
await session.execute(delete(Document).where(Document.stored_file_id == file_id))
|
||||
await session.execute(delete(RecognitionJob).where(RecognitionJob.file_id == file_id))
|
||||
|
||||
@ -287,7 +287,8 @@ class PrimaryFilesService:
|
||||
`FILE_SUPERSEDES_OTHERS` — на файл ссылаются `superseded_by_file_id` (цепочка версий не рвётся). Файл,
|
||||
исчезнувший, пока ждали блокировку, — 404.
|
||||
|
||||
Удаляются `osv_entry`, `document`, все задания и `stored_file`; `s3_object` файла и результатов заданий
|
||||
Удаляются формы ББ/ОФР (`financial_statement_*`), комментарии документов, `osv_entry`, `document`, все задания
|
||||
и `stored_file`; `s3_object` файла и результатов заданий
|
||||
остаются, им ставится `expires_at = now` — только объектам из папок файла `uploads/{file_id}/` и
|
||||
`results/{file_id}/` (M-3). ASSUMPTION(OQ-22): физически объекты S3 не удаляются — их убирает
|
||||
ретеншн бакета. `before_value` аудита — карточка файла."""
|
||||
|
||||
@ -6,6 +6,7 @@ design D-10).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
@ -16,11 +17,18 @@ from sqlalchemy import func, select, text, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from src.core.config import get_settings
|
||||
from src.models.audit_ex import AuditEvent
|
||||
from src.models.access import AppUser
|
||||
from src.models.audit_ex import AuditEvent, Comment
|
||||
from src.models.directories import LegalEntity, RegionalOffice
|
||||
from src.models.enums import AuditAction, AuditSection, DocumentStatus, FileStatus, RecognitionStatus
|
||||
from src.models.files import Document, RecognitionJob, S3Object, StoredFile
|
||||
from src.models.osv import OsvEntry
|
||||
from src.models.statements import (
|
||||
FinancialStatementColumn,
|
||||
FinancialStatementLine,
|
||||
FinancialStatementMeta,
|
||||
FinancialStatementValue,
|
||||
)
|
||||
from src.services.result_mapping import process_once
|
||||
from tests.conftest import AuthClientFactory, FakeStorage
|
||||
from tests.factories import (
|
||||
@ -31,6 +39,7 @@ from tests.factories import (
|
||||
make_result_object,
|
||||
make_stored_file,
|
||||
)
|
||||
from tests.fixtures_helpers import GOLDEN_STATEMENTS, map_golden_snapshot
|
||||
|
||||
HTTP_NO_CONTENT = 204
|
||||
HTTP_FORBIDDEN = 403
|
||||
@ -102,6 +111,56 @@ async def test_delete_error_file(seeded: AsyncSession, auth_client: AuthClientFa
|
||||
assert event.after_value is None
|
||||
|
||||
|
||||
MEZH_BALANCE = GOLDEN_STATEMENTS / "mezheninovskaya" / "2025-Q3" / "1_Форма1_бух_09.json"
|
||||
|
||||
|
||||
async def _form_file_with_comment(session: AsyncSession, fixtures_dir: Path) -> uuid.UUID:
|
||||
"""Файл с формой ББ (golden) и комментарием к её документу; возвращает id файла."""
|
||||
entity = await _rostov_entity(session)
|
||||
(document,) = await map_golden_snapshot(session, entity=entity, path=fixtures_dir / MEZH_BALANCE, uploaded_by=None)
|
||||
author = (await session.execute(select(AppUser.id).where(AppUser.email == KA_RF))).scalar_one()
|
||||
session.add(
|
||||
Comment(
|
||||
object_type="document",
|
||||
object_id=document.id,
|
||||
text="проверить",
|
||||
author_id=author,
|
||||
created_at=datetime.datetime.now(datetime.UTC),
|
||||
)
|
||||
)
|
||||
await session.flush()
|
||||
return document.stored_file_id
|
||||
|
||||
|
||||
async def _assert_form_rows_gone(session: AsyncSession, file_id: uuid.UUID, document_ids: list[uuid.UUID]) -> None:
|
||||
assert await _count(session, StoredFile, StoredFile.id == file_id) == 0
|
||||
assert await _count(session, Document, Document.stored_file_id == file_id) == 0
|
||||
for model in (FinancialStatementMeta, FinancialStatementColumn, FinancialStatementLine, FinancialStatementValue):
|
||||
assert await _count(session, model) == 0, model.__tablename__
|
||||
assert await _count(session, Comment, Comment.object_id.in_(document_ids)) == 0
|
||||
|
||||
|
||||
@pytest.mark.fixtures
|
||||
@pytest.mark.parametrize("superseded", [False, True])
|
||||
async def test_delete_file_with_statement_form(
|
||||
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory, superseded: bool
|
||||
) -> None:
|
||||
"""C-1, T8.2: удаление файла с формой ББ/ОФР убирает шапку, колонки, строки, значения и комментарии документов
|
||||
(раньше — падение по FK `financial_statement_meta`); то же для файла `SUPERSEDED`."""
|
||||
file_id = await _form_file_with_comment(seeded, fixtures_dir)
|
||||
document_ids = list((await seeded.execute(select(Document.id).where(Document.stored_file_id == file_id))).scalars())
|
||||
assert await _count(seeded, FinancialStatementValue) > 0 and await _count(seeded, Comment) == 1
|
||||
if superseded:
|
||||
await seeded.execute(update(StoredFile).where(StoredFile.id == file_id).values(status=FileStatus.SUPERSEDED))
|
||||
await seeded.flush()
|
||||
client = await auth_client(KA_RF)
|
||||
|
||||
response = await client.delete(f"/files/{file_id}")
|
||||
|
||||
assert response.status_code == HTTP_NO_CONTENT, response.text
|
||||
await _assert_form_rows_gone(seeded, file_id, document_ids)
|
||||
|
||||
|
||||
async def test_delete_expires_only_own_objects(seeded: AsyncSession, auth_client: AuthClientFactory) -> None:
|
||||
"""M-3: `expires_at` ставится только объектам папок файла (`uploads/{file_id}/`, `results/{file_id}/`); ссылка
|
||||
задания на объект другого файла (inbox переиспользует `s3_object` при конфликте `(bucket, key)`) его не трогает."""
|
||||
|
||||
@ -177,5 +177,5 @@ seed-dev` с нуля.
|
||||
| 15 | 2026-10-07 | R-5: сверка golden ОСВ в `test_mapping_golden` ограничена документами `OSV` | смешанный снимок `expected_osv_parse/synthetic/s12_statement_and_osv.json` содержит BALANCE, которую теперь тоже маппит ядро; сравнение ОСВ-строк не ослаблено | форма из s12 в этом тесте не сверяется (покрывается тестами форм) |
|
||||
| 16 | 2026-10-07 | R-6: документ `BALANCE`/`INCOME` без `financial_statement_meta` (создан до Среза 3b) подтверждается без условий D-9; `PATCH`/`GET …/statement` по нему — 409 `DOCUMENT_NOT_STATEMENT` | проверять нечего: формы в БД нет; блокировать подтверждение старых документов 3b не вправе | старый документ без данных формы подтверждается без проверок периода/§7 |
|
||||
| 17 | 2026-10-07 | R-7: список нарушений `CHECKS_NOT_PASSED` отдаётся в `detail.failures` (строки сумм), не в отдельном ключе `details` | единый формат ошибок ядра `{"detail": {"code","message", **extra}}` | — |
|
||||
| 18 | 2026-10-07 | R-8: при удалении файла (`DELETE /files/{id}`) комментарии `comment(object_type='document')` удалённых документов остаются в таблице | связь полиморфная, FK нет; чтение идёт через видимый документ, наружу строки не попадают; спека о судьбе комментариев молчит | осиротевшие строки `comment` (единицы на файл); чистка — отдельной задачей |
|
||||
| 18 | 2026-10-07 | R-8: при удалении файла (`DELETE /files/{id}`) комментарии `comment(object_type='document')` удалённых документов остаются в таблице | связь полиморфная, FK нет; чтение идёт через видимый документ, наружу строки не попадают; спека о судьбе комментариев молчит | осиротевшие строки `comment` (единицы на файл); чистка — отдельной задачей. **Закрыто (fix wave, T8.2): комментарии удаляются вместе с документами** в `delete_file_rows` — удаление файла и так обязано чистить зависимые от документов строки (формы ББ/ОФР, C-1), лишний `DELETE` дёшев, а осиротевшие строки не нужны никому |
|
||||
| 19 | 2026-10-07 | R-9: значения ячеек CSV чек-листа (название ЮЛ, ИНН) не нейтрализуются от формул Excel (`=`, `+`, `-`, `@`) | спека не требует; названия из справочника ЕЦП внутреннего контура | теоретическая формула в названии ЮЛ откроется в Excel как формула |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user