"""Уточнение ОСВ в транзакции запроса с сохранением первички и аудитом.""" import uuid from sqlalchemy import delete, select from sqlalchemy.exc import IntegrityError from src.common.clock import utc_now from src.common.exceptions import ConflictError, NotFoundError from src.domain.osv_header import HEADER_FIELDS, corrected_header from src.domain.policy import IN_WORK_STATUSES from src.models.consolidation import ConsolidationDocument from src.models.enums import AuditAction, AuditSection, ConsolidationStatus, DocumentStatus, DocumentType, FileStatus from src.models.files import Document from src.repositories import files as files_repo from src.schemas.documents import DocumentOut, OsvHeaderPatch from src.services import consolidation_documents from src.services.documents import DocumentService class OsvHeaderService(DocumentService): """Разделяет видимость, карточку и транзакцию с обычным сервисом документов.""" async def _twin(self, document: Document, account: object) -> uuid.UUID | None: return await self._session.scalar( select(Document.id).where( Document.stored_file_id == document.stored_file_id, Document.document_type == DocumentType.OSV, Document.osv_account_code == account, Document.id != document.id, ) ) async def patch(self, document_id: uuid.UUID, body: OsvHeaderPatch) -> DocumentOut: visible = await self._visible(document_id) file = await files_repo.lock_file(self._session, visible.stored_file_id) document = await files_repo.lock_document(self._session, document_id) if file is None or document is None: raise NotFoundError("документ не найден") if document.document_type is not DocumentType.OSV: raise ConflictError("DOCUMENT_NOT_OSV", "Документ не является ОСВ") if file.status is FileStatus.SUPERSEDED: raise ConflictError("FILE_SUPERSEDED", "Файл заменён новой версией") current = {key: getattr(document, key) for key in HEADER_FIELDS} result = corrected_header(current, body.model_dump(exclude_unset=True)) twin = await self._twin(document, result["osv_account_code"]) if twin is not None: raise ConflictError("DUPLICATE_ACCOUNT", "Счёт уже есть в файле", document_id=str(twin)) versions = await consolidation_documents.bound_versions(self._session, documents=[document]) blocked = [ str(vid) for vid, status in versions if status not in IN_WORK_STATUSES | {ConsolidationStatus.CANCELLED} ] if blocked: raise ConflictError("VERSION_NOT_EDITABLE", "Документ используется в закрытой версии", version_ids=blocked) changes = {key: value for key, value in result.items() if value != current[key]} if not changes: return await self._card(document, file) before = {key: current[key] for key in changes} after = dict(changes) try: async with self._session.begin_nested(): if document.original_values is None: document.original_values = current for key, value in changes.items(): setattr(document, key, value) document.is_modified = result != document.original_values document.modified_by = self._audit.user_id document.modified_at = document.updated_at = utc_now() if document.status is DocumentStatus.CONFIRMED: before.update( status=document.status.value, confirmed_by=str(document.confirmed_by) if document.confirmed_by else None, confirmed_at=document.confirmed_at.isoformat() if document.confirmed_at else None, ) document.status = DocumentStatus.NEEDS_CHECK document.confirmed_by = document.confirmed_at = None after.update(status=document.status.value, confirmed_by=None, confirmed_at=None) working = [vid for vid, status in versions if status in IN_WORK_STATUSES] await self._session.execute( delete(ConsolidationDocument).where( ConsolidationDocument.document_id == document.id, ConsolidationDocument.consolidation_version_id.in_(working), ) ) after["unbound_version_ids"] = [str(vid) for vid in working] await self._session.flush() except IntegrityError: # SAVEPOINT rollback expires the changed ORM object; reload asynchronously before accessing fields. document = await files_repo.lock_document(self._session, document_id) if document is None: raise twin = await self._twin(document, result["osv_account_code"]) if twin is None: raise raise ConflictError("DUPLICATE_ACCOUNT", "Счёт уже есть в файле", document_id=str(twin)) from None await self._audit.record( action=AuditAction.RECOGNIZED_DATA_EDIT, section=AuditSection.RECOGNITION, object_type="document", object_id=document.id, before_value=before, after_value=after, ) return await self._card(document, file)