From f137f38cdd578da72d4ddc69fb383d42bb89138c Mon Sep 17 00:00:00 2001 From: Leopoldadmin Date: Sat, 4 Jul 2026 01:31:19 +0000 Subject: [PATCH] =?UTF-8?q?C3:=20Fix=20sequence=5Fservice.py=20=E2=80=94?= =?UTF-8?q?=20add=20delete=5Fsequence=20function=20(soft-delete=20with=20a?= =?UTF-8?q?udit)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/sequence_service.py | 159 ++++++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 1 deletion(-) diff --git a/app/services/sequence_service.py b/app/services/sequence_service.py index 41f3a4f..1b07eeb 100644 --- a/app/services/sequence_service.py +++ b/app/services/sequence_service.py @@ -1 +1,158 @@ -§§include(/a0/usr/chats/XttavUaL/messages/seq_svc_update.py) \ No newline at end of file +"""Sequence service — get_next_number with SELECT FOR UPDATE locking, CRUD, delete.""" + +from __future__ import annotations + +import uuid +from datetime import UTC, datetime +from typing import Any + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.audit import log_audit +from app.models.sequence import Sequence + + +def _sequence_to_dict(s: Sequence) -> dict[str, Any]: + """Serialize a Sequence ORM object to dict.""" + return { + "id": str(s.id), + "name": s.name, + "prefix": s.prefix, + "next_number": s.next_number, + "padding": s.padding, + "created_at": s.created_at.isoformat() if s.created_at else None, + "updated_at": s.updated_at.isoformat() if s.updated_at else None, + } + + +async def get_next_number( + db: AsyncSession, + name: str, + tenant_id: uuid.UUID, +) -> str: + """Get the next formatted number for a sequence. + + Uses SELECT FOR UPDATE to lock the row, increment next_number, + and return the formatted string (prefix + zero-padded number). + + Raises ValueError if the sequence does not exist. + """ + q = ( + select(Sequence) + .where( + Sequence.name == name, + Sequence.tenant_id == tenant_id, + Sequence.deleted_at.is_(None), + ) + .with_for_update() + ) + result = await db.execute(q) + seq = result.scalar_one_or_none() + if seq is None: + raise ValueError(f"Sequence '{name}' not found for tenant {tenant_id}") + + current_number = seq.next_number + formatted = f"{seq.prefix}{str(current_number).zfill(seq.padding)}" + seq.next_number = current_number + 1 + + await db.flush() + return formatted + + +async def create_sequence( + db: AsyncSession, + tenant_id: uuid.UUID, + user_id: uuid.UUID, + data: dict[str, Any], +) -> dict[str, Any]: + """Create a new sequence.""" + sequence = Sequence( + tenant_id=tenant_id, + name=data["name"], + prefix=data.get("prefix", ""), + next_number=1, + padding=data.get("padding", 4), + ) + db.add(sequence) + await db.flush() + await db.refresh(sequence) + await log_audit( + db, tenant_id, user_id, "create", "sequence", sequence.id, + changes={"name": sequence.name, "prefix": sequence.prefix}, + ) + return _sequence_to_dict(sequence) + + +async def list_sequences( + db: AsyncSession, + tenant_id: uuid.UUID, +) -> dict[str, Any]: + """List all sequences for a tenant.""" + q = select(Sequence).where( + Sequence.tenant_id == tenant_id, + Sequence.deleted_at.is_(None), + ).order_by(Sequence.name.asc()) + result = await db.execute(q) + sequences = result.scalars().all() + return { + "items": [_sequence_to_dict(s) for s in sequences], + "total": len(sequences), + } + + +async def update_sequence( + db: AsyncSession, + tenant_id: uuid.UUID, + user_id: uuid.UUID, + sequence_id: uuid.UUID, + data: dict[str, Any], +) -> dict[str, Any] | None: + """Update a sequence (name, prefix, padding only — NOT next_number).""" + q = select(Sequence).where( + Sequence.id == sequence_id, + Sequence.tenant_id == tenant_id, + Sequence.deleted_at.is_(None), + ) + result = await db.execute(q) + sequence = result.scalar_one_or_none() + if sequence is None: + return None + + changes: dict[str, Any] = {} + for field in ("name", "prefix", "padding"): + if field in data and data[field] is not None: + old_val = getattr(sequence, field) + changes[field] = {"old": old_val, "new": data[field]} + setattr(sequence, field, data[field]) + + await db.flush() + await db.refresh(sequence) + await log_audit(db, tenant_id, user_id, "update", "sequence", sequence_id, changes=changes) + return _sequence_to_dict(sequence) + + +async def delete_sequence( + db: AsyncSession, + tenant_id: uuid.UUID, + user_id: uuid.UUID, + sequence_id: uuid.UUID, +) -> bool: + """Soft-delete a sequence.""" + q = select(Sequence).where( + Sequence.id == sequence_id, + Sequence.tenant_id == tenant_id, + Sequence.deleted_at.is_(None), + ) + result = await db.execute(q) + sequence = result.scalar_one_or_none() + if sequence is None: + return False + + sequence.deleted_at = datetime.now(UTC) + await db.flush() + await log_audit( + db, tenant_id, user_id, "delete", "sequence", sequence_id, + changes={"name": sequence.name}, + ) + return True