$approverIds — ordered list of user IDs */ public function createChain( Model $approvable, array $approverIds, string $type = 'general', ?int $initiatedBy = null, ?string $notes = null, ): ApprovalChain { $chain = ApprovalChain::create([ 'approvable_type' => $approvable->getMorphClass(), 'approvable_id' => $approvable->getKey(), 'type' => $type, 'status' => ApprovalChainStatus::Pending, 'initiated_by' => $initiatedBy, 'notes' => $notes, ]); foreach ($approverIds as $order => $userId) { ApprovalStep::create([ 'approval_chain_id' => $chain->id, 'approver_id' => $userId, 'order' => $order, 'status' => ApprovalStepStatus::Pending, ]); } // Auto-transition to InReview and notify first approver $chain->transitionTo(ApprovalChainStatus::InReview); $firstStep = $chain->currentStep(); if ($firstStep) { ApprovalRequired::dispatch($chain, $firstStep); } return $chain->load('steps.approver'); } /** * Approve the current pending step. */ public function approve(ApprovalChain $chain, User $approver, ?string $notes = null): ApprovalChain { $step = $this->getCurrentStepForApprover($chain, $approver); $step->update([ 'status' => ApprovalStepStatus::Approved, 'notes' => $notes, 'acted_at' => now(), ]); StepApproved::dispatch($chain, $step); // Check if all steps are approved if ($chain->isFullyApproved()) { $chain->transitionTo(ApprovalChainStatus::Approved); ApprovalCompleted::dispatch($chain); } else { // Notify next approver $nextStep = $chain->currentStep(); if ($nextStep) { ApprovalRequired::dispatch($chain, $nextStep); } } return $chain->fresh('steps.approver'); } /** * Reject the current pending step (rejects the entire chain). */ public function reject(ApprovalChain $chain, User $approver, ?string $notes = null): ApprovalChain { $step = $this->getCurrentStepForApprover($chain, $approver); $step->update([ 'status' => ApprovalStepStatus::Rejected, 'notes' => $notes, 'acted_at' => now(), ]); // Skip remaining steps $chain->steps() ->where('order', '>', $step->order) ->where('status', 'pending') ->update(['status' => ApprovalStepStatus::Skipped->value]); $chain->transitionTo(ApprovalChainStatus::Rejected); StepRejected::dispatch($chain, $step); ApprovalCompleted::dispatch($chain); return $chain->fresh('steps.approver'); } public function getNextApprover(ApprovalChain $chain): ?User { $step = $chain->currentStep(); return $step?->approver; } private function getCurrentStepForApprover(ApprovalChain $chain, User $approver): ApprovalStep { $step = $chain->currentStep(); if (!$step) { throw new \InvalidArgumentException('No pending step found in this approval chain.'); } if ($step->approver_id !== $approver->id) { throw new \InvalidArgumentException('You are not the current approver for this step.'); } return $step; } }