"""Server-side workflow state machine. Every stage change flows through `transition()`, which validates against ALLOWED_TRANSITIONS and records both a StageTransition row (timestamps + acting user, for aging/cycle-time reporting) and an audit entry.""" from app.domain import ALLOWED_TRANSITIONS, Stage from app.models import Ncr, StageTransition from app.models.base import utcnow from app.services.audit import audit_event from sqlalchemy.ext.asyncio import AsyncSession class InvalidTransitionError(Exception): def __init__(self, from_stage: str, to_stage: str): self.from_stage = from_stage self.to_stage = to_stage super().__init__(f"Invalid stage transition: {from_stage} -> {to_stage}") def transition( db: AsyncSession, ncr: Ncr, to_stage: Stage, *, action: str, actor_id: int, note: str | None = None, ) -> None: from_stage = Stage(ncr.stage) if to_stage not in ALLOWED_TRANSITIONS.get(from_stage, set()): raise InvalidTransitionError(from_stage.value, to_stage.value) now = utcnow() ncr.stage = to_stage.value ncr.stage_entered_at = now db.add( StageTransition( ncr_id=ncr.id, from_stage=from_stage.value, to_stage=to_stage.value, action=action, acted_by_id=actor_id, acted_at=now, note=note, ) ) audit_event( db, ncr_id=ncr.id, user_id=actor_id, action=action, field_name="stage", old_value=from_stage.value, new_value=to_stage.value, detail=note, ) def record_creation(db: AsyncSession, ncr: Ncr, actor_id: int) -> None: db.add( StageTransition( ncr_id=ncr.id, from_stage=None, to_stage=Stage.NEW_REQUEST.value, action="create", acted_by_id=actor_id, acted_at=ncr.created_at, ) ) audit_event( db, ncr_id=ncr.id, user_id=actor_id, action="create", detail=f"NCR {ncr.ncr_number} created", )