from __future__ import annotations from datetime import UTC, datetime from uuid import UUID from src.domain.contracts import ( CurrentUser, Role, WorkflowStageCreateRequest, WorkflowStageResponse, WorkflowStageUpdateRequest, WorkflowTemplateAuditEventListResponse, WorkflowTemplateCreateRequest, WorkflowTemplateListResponse, WorkflowTemplateResponse, WorkflowTemplateStatus, WorkflowTemplateSummary, WorkflowTemplateUpdateRequest, ) _TEMPLATE_CREATED_EVENT_TYPE = "WORKFLOW_TEMPLATE_CREATED" _TEMPLATE_UPDATED_EVENT_TYPE = "WORKFLOW_TEMPLATE_UPDATED" _STAGE_CREATED_EVENT_TYPE = "WORKFLOW_STAGE_CREATED" _STAGE_UPDATED_EVENT_TYPE = "WORKFLOW_STAGE_UPDATED" _STAGE_DELETED_EVENT_TYPE = "WORKFLOW_STAGE_DELETED" _STAGES_REORDERED_EVENT_TYPE = "WORKFLOW_STAGES_REORDERED" _TEMPLATE_ACTIVATED_EVENT_TYPE = "WORKFLOW_TEMPLATE_ACTIVATED" _TEMPLATE_ARCHIVED_EVENT_TYPE = "WORKFLOW_TEMPLATE_ARCHIVED" def create_workflow_template( repository: object, *, request: WorkflowTemplateCreateRequest, current_user: CurrentUser, ) -> WorkflowTemplateResponse: now = _now() workflow = repository.workflow_templates.create( name=request.name, slug=request.slug, description=request.description, status=WorkflowTemplateStatus.DRAFT, version=1, created_by=current_user.id, updated_by=current_user.id, created_at=now, updated_at=now, ) _create_event( repository, workflow_id=workflow.id, event_type=_TEMPLATE_CREATED_EVENT_TYPE, current_user=current_user, payload={"slug": workflow.slug, "version": workflow.version}, created_at=now, ) return WorkflowTemplateResponse(workflow=workflow) def list_workflow_templates( repository: object, *, current_user: CurrentUser, ) -> WorkflowTemplateListResponse: if current_user.role == Role.ADMIN: workflows = repository.workflow_templates.list() else: workflows = repository.workflow_templates.list_active() return WorkflowTemplateListResponse(workflows=workflows) def get_workflow_template( repository: object, *, workflow_id: UUID, current_user: CurrentUser, ) -> WorkflowTemplateResponse: workflow = repository.workflow_templates.get(workflow_id) if current_user.role != Role.ADMIN and workflow.status != WorkflowTemplateStatus.ACTIVE: raise LookupError(f"Workflow template not found: {workflow_id}") return WorkflowTemplateResponse(workflow=workflow) def update_workflow_template( repository: object, *, workflow_id: UUID, request: WorkflowTemplateUpdateRequest, current_user: CurrentUser, ) -> WorkflowTemplateResponse: existing = repository.workflow_templates.get(workflow_id) _require_draft(existing) changes = request.model_dump(exclude_unset=True) now = _now() workflow = repository.workflow_templates.update( workflow_id=workflow_id, name=changes.get("name", existing.name), slug=changes.get("slug", existing.slug), description=changes.get("description", existing.description), updated_by=current_user.id, updated_at=now, ) _create_event( repository, workflow_id=workflow.id, event_type=_TEMPLATE_UPDATED_EVENT_TYPE, current_user=current_user, payload={"changed_fields": sorted(changes.keys())}, created_at=now, ) return WorkflowTemplateResponse(workflow=workflow) def add_workflow_stage( repository: object, *, workflow_id: UUID, request: WorkflowStageCreateRequest, current_user: CurrentUser, ) -> WorkflowStageResponse: workflow = repository.workflow_templates.get(workflow_id) _require_draft(workflow) now = _now() stage = repository.workflow_template_stages.create( workflow_id=workflow_id, stable_key=request.stable_key, display_name=request.display_name, description=request.description, position=request.position, owner_role=request.owner_role, runner_profile_key=request.runner_profile_key, required_inputs=request.required_inputs, expected_outputs=request.expected_outputs, acceptance_criteria=request.acceptance_criteria, requires_human_approval=request.requires_human_approval, retry_policy=request.retry_policy, parts=_parts_json(request.parts), updated_by=current_user.id, created_at=now, updated_at=now, ) _create_event( repository, workflow_id=workflow_id, event_type=_STAGE_CREATED_EVENT_TYPE, current_user=current_user, payload={ "stage_id": str(stage.id), "stable_key": stage.stable_key, "position": stage.position, }, created_at=now, ) return WorkflowStageResponse(stage=stage) def update_workflow_stage( repository: object, *, workflow_id: UUID, stage_id: UUID, request: WorkflowStageUpdateRequest, current_user: CurrentUser, ) -> WorkflowStageResponse: workflow = repository.workflow_templates.get(workflow_id) _require_draft(workflow) existing = repository.workflow_template_stages.get( workflow_id=workflow_id, stage_id=stage_id, ) changes = request.model_dump(exclude_unset=True) now = _now() stage = repository.workflow_template_stages.update( workflow_id=workflow_id, stage_id=stage_id, stable_key=changes.get("stable_key", existing.stable_key), display_name=changes.get("display_name", existing.display_name), description=changes.get("description", existing.description), position=changes.get("position", existing.position), owner_role=request.owner_role or existing.owner_role, runner_profile_key=changes.get( "runner_profile_key", existing.runner_profile_key, ), required_inputs=changes.get("required_inputs", existing.required_inputs), expected_outputs=changes.get("expected_outputs", existing.expected_outputs), acceptance_criteria=changes.get( "acceptance_criteria", existing.acceptance_criteria, ), requires_human_approval=changes.get( "requires_human_approval", existing.requires_human_approval, ), retry_policy=changes.get("retry_policy", existing.retry_policy), parts=( _parts_json(request.parts) if request.parts is not None else _parts_json(existing.parts) ), updated_by=current_user.id, updated_at=now, ) _create_event( repository, workflow_id=workflow_id, event_type=_STAGE_UPDATED_EVENT_TYPE, current_user=current_user, payload={"stage_id": str(stage_id), "changed_fields": sorted(changes.keys())}, created_at=now, ) return WorkflowStageResponse(stage=stage) def delete_workflow_stage( repository: object, *, workflow_id: UUID, stage_id: UUID, current_user: CurrentUser, ) -> WorkflowTemplateResponse: workflow = repository.workflow_templates.get(workflow_id) _require_draft(workflow) existing = repository.workflow_template_stages.get( workflow_id=workflow_id, stage_id=stage_id, ) now = _now() repository.workflow_template_stages.delete( workflow_id=workflow_id, stage_id=stage_id, updated_by=current_user.id, updated_at=now, ) _create_event( repository, workflow_id=workflow_id, event_type=_STAGE_DELETED_EVENT_TYPE, current_user=current_user, payload={"stage_id": str(stage_id), "stable_key": existing.stable_key}, created_at=now, ) return WorkflowTemplateResponse(workflow=repository.workflow_templates.get(workflow_id)) def reorder_workflow_stages( repository: object, *, workflow_id: UUID, stage_ids: list[UUID], current_user: CurrentUser, ) -> WorkflowTemplateResponse: workflow = repository.workflow_templates.get(workflow_id) _require_draft(workflow) now = _now() repository.workflow_template_stages.reorder( workflow_id=workflow_id, stage_ids=stage_ids, updated_by=current_user.id, updated_at=now, ) _create_event( repository, workflow_id=workflow_id, event_type=_STAGES_REORDERED_EVENT_TYPE, current_user=current_user, payload={"stage_ids": [str(stage_id) for stage_id in stage_ids]}, created_at=now, ) return WorkflowTemplateResponse(workflow=repository.workflow_templates.get(workflow_id)) def activate_workflow_template( repository: object, *, workflow_id: UUID, current_user: CurrentUser, ) -> WorkflowTemplateResponse: workflow = repository.workflow_templates.get(workflow_id) _require_draft(workflow) now = _now() activated = repository.workflow_templates.activate( workflow_id=workflow_id, updated_by=current_user.id, activated_at=now, ) _create_event( repository, workflow_id=workflow_id, event_type=_TEMPLATE_ACTIVATED_EVENT_TYPE, current_user=current_user, payload={"version": activated.version}, created_at=now, ) return WorkflowTemplateResponse(workflow=activated) def archive_workflow_template( repository: object, *, workflow_id: UUID, current_user: CurrentUser, ) -> WorkflowTemplateResponse: repository.workflow_templates.get(workflow_id) now = _now() archived = repository.workflow_templates.archive( workflow_id=workflow_id, updated_by=current_user.id, archived_at=now, ) _create_event( repository, workflow_id=workflow_id, event_type=_TEMPLATE_ARCHIVED_EVENT_TYPE, current_user=current_user, payload={"version": archived.version}, created_at=now, ) return WorkflowTemplateResponse(workflow=archived) def list_workflow_template_audit_events( repository: object, *, workflow_id: UUID, ) -> WorkflowTemplateAuditEventListResponse: repository.workflow_templates.get(workflow_id) return WorkflowTemplateAuditEventListResponse( events=repository.workflow_template_events.list_for_workflow(workflow_id) ) def _create_event( repository: object, *, workflow_id: UUID, event_type: str, current_user: CurrentUser, payload: dict[str, object], created_at: datetime, ) -> None: repository.workflow_template_events.create( workflow_id=workflow_id, event_type=event_type, actor_user_id=current_user.id, payload=payload, created_at=created_at, ) def _require_draft(workflow: WorkflowTemplateSummary) -> None: if workflow.status != WorkflowTemplateStatus.DRAFT: raise ValueError("Only draft workflow templates can be mutated.") def _parts_json(parts: list[object]) -> list[dict[str, object]]: return [ part.model_dump(mode="json") if hasattr(part, "model_dump") else dict(part) for part in parts ] def _now() -> datetime: return datetime.now(UTC)