in_progress -> awaiting_approval -> completed * \-> completed (skips approval if not required) * \-> failed -> in_progress (retry) * awaiting_approval -> rejected -> in_progress (retry) * pending | in_progress -> skipped * * This class only manages state — it doesn't know how to actually generate * a script, render video, etc. Whatever does that work (a future AI * provider integration, a human, an Artisan command) calls completeStep()/ * failStep() to report the result. */ class WorkflowEngine { /** * Create the step sequence for a project and activate the first one. * * @param array $steps */ public function start(VideoProject $project, array $steps): WorkflowStep { if ($project->steps()->exists()) { throw new WorkflowTransitionException("Video project {$project->id} already has workflow steps."); } if (empty($steps)) { throw new WorkflowTransitionException('Cannot start a workflow with no steps.'); } foreach (array_values($steps) as $order => $step) { $project->steps()->create([ 'type' => $step['type'], 'order' => $order, 'status' => 'pending', 'requires_approval' => $step['requires_approval'] ?? false, ]); } $project->update(['status' => 'in_progress']); $first = $project->steps()->orderBy('order')->firstOrFail(); return $this->activate($first); } public function startFromTemplate(VideoProject $project, WorkflowTemplate $template): WorkflowStep { $project->update(['workflow_template_id' => $template->id]); return $this->start($project, $template->steps); } /** * The step currently blocking progress — pending, running, awaiting * approval, or stuck on failure/rejection. Null once every step is * completed or skipped. */ public function currentStep(VideoProject $project): ?WorkflowStep { return $project->steps() ->whereNotIn('status', ['completed', 'skipped']) ->orderBy('order') ->first(); } public function completeStep(WorkflowStep $step, array $output = []): WorkflowStep { $this->assertStatus($step, 'in_progress'); $step->update([ 'output' => $output, 'error' => null, 'completed_at' => now(), 'status' => $step->requires_approval ? 'awaiting_approval' : 'completed', ]); if ($step->status === 'completed') { $this->activateNext($step); } return $step->fresh(); } public function approveStep(WorkflowStep $step): WorkflowStep { $this->assertStatus($step, 'awaiting_approval'); $step->update(['status' => 'completed']); $this->activateNext($step); return $step->fresh(); } /** * Rejecting does not advance the pipeline — it stays blocked on this * step until retryStep() is called. */ public function rejectStep(WorkflowStep $step, ?string $reason = null): WorkflowStep { $this->assertStatus($step, 'awaiting_approval'); $step->update(['status' => 'rejected', 'error' => $reason]); return $step->fresh(); } public function failStep(WorkflowStep $step, string $error): WorkflowStep { $this->assertStatus($step, 'in_progress'); $step->update(['status' => 'failed', 'error' => $error]); return $step->fresh(); } public function retryStep(WorkflowStep $step): WorkflowStep { if (! in_array($step->status, ['failed', 'rejected'], true)) { throw new WorkflowTransitionException("Step {$step->id} can only be retried from failed or rejected, currently '{$step->status}'."); } $step->update([ 'status' => 'in_progress', 'error' => null, 'output' => null, 'started_at' => now(), 'completed_at' => null, ]); return $step->fresh(); } public function skipStep(WorkflowStep $step): WorkflowStep { if (! in_array($step->status, ['pending', 'in_progress'], true)) { throw new WorkflowTransitionException("Step {$step->id} can only be skipped from pending or in_progress, currently '{$step->status}'."); } $step->update(['status' => 'skipped', 'completed_at' => now()]); $this->activateNext($step); return $step->fresh(); } private function activate(WorkflowStep $step): WorkflowStep { $step->update(['status' => 'in_progress', 'started_at' => now()]); return $step->fresh(); } private function activateNext(WorkflowStep $justFinished): void { $project = $justFinished->videoProject; $next = $project->steps() ->where('order', '>', $justFinished->order) ->whereNotIn('status', ['completed', 'skipped']) ->orderBy('order') ->first(); if ($next) { $this->activate($next); return; } $project->update(['status' => 'completed']); } private function assertStatus(WorkflowStep $step, string $expected): void { if ($step->status !== $expected) { throw new WorkflowTransitionException("Step {$step->id} must be '{$expected}' for this transition, currently '{$step->status}'."); } } }