from datetime import datetime, timezone from sqlalchemy import select from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from app.models.fehlbestand import Fehlbestand, FehlbestandStatus from app.models.mindermenge import MindermengeStatus, MindermengenGenehmigung from app.services import historie as historie_service class FehlbestandBereitsErledigtError(Exception): """Ein bereits erledigter Fehlbestand braucht keine Mindermengen-Bewertung mehr.""" class BereitsGenehmigtError(Exception): """Für diesen Fehlbestand existiert bereits eine aktive Genehmigung.""" async def genehmigen( db: AsyncSession, *, fehlbestand: Fehlbestand, begruendung: str, genehmigt_von: int, zustaendiger_server_id: int, ) -> MindermengenGenehmigung: """Prompt 04: reine fachliche Bewertung - ändert NIEMALS Ist-Menge, Fehlmenge oder den Fehlbestand-Status selbst (Leitplanke, U2). Verknüpft mit der Kontrolle, die den Fehlbestand ausgelöst hat ("aktuelle Kontrolle des Objekts", Prompt 04.3.4).""" if fehlbestand.status == FehlbestandStatus.erledigt: raise FehlbestandBereitsErledigtError() if fehlbestand.kontrolle_id is None: # Sollte praktisch nicht vorkommen (jeder Fehlbestand entsteht aus einer # Kontrolle, Sprint 3), aber das Schema erlaubt kontrolle_id=NULL. raise ValueError("Fehlbestand ohne auslösende Kontrolle kann nicht genehmigt werden") # Row-Lock auf den Fehlbestand serialisiert parallele Genehmigungsversuche # (sql-expert-Review-Fund: TOCTOU zwischen Check und Insert ohne Lock). await db.execute(select(Fehlbestand).where(Fehlbestand.id == fehlbestand.id).with_for_update()) result = await db.execute( select(MindermengenGenehmigung).where( MindermengenGenehmigung.fehlbestand_id == fehlbestand.id, MindermengenGenehmigung.status == MindermengeStatus.aktiv, ) ) if result.scalar_one_or_none() is not None: raise BereitsGenehmigtError() genehmigung = MindermengenGenehmigung( fehlbestand_id=fehlbestand.id, genehmigt_von=genehmigt_von, begruendung=begruendung, genehmigt_am=datetime.now(timezone.utc), ausloesende_kontrolle_id=fehlbestand.kontrolle_id, status=MindermengeStatus.aktiv, ) db.add(genehmigung) try: await db.flush() except IntegrityError as exc: # Sicherheitsnetz gegen den partiellen Unique-Index (Migration 0007), # falls der Lock oben je umgangen würde (z. B. anderer Isolation-Level). raise BereitsGenehmigtError() from exc await historie_service.log( db, zustaendiger_server_id=zustaendiger_server_id, benutzer_id=genehmigt_von, ereignistyp="mindermenge_genehmigt", entitaet_typ="mindermengen_genehmigung", entitaet_id=genehmigung.id, neuer_wert={"fehlbestand_id": str(fehlbestand.id), "status": "aktiv"}, begruendung=begruendung, ) return genehmigung