> */ private const TRANSITIONS = [ 'received' => ['in_review', 'qualification', 'contacted', 'closed', 'cancelled', 'refused'], 'submitted' => ['in_review', 'qualification', 'contacted', 'closed', 'cancelled', 'refused'], 'in_review' => ['qualification', 'qualified', 'contacted', 'referred', 'closed', 'cancelled', 'refused'], 'reviewed' => ['qualification', 'qualified', 'contacted', 'referred', 'closed', 'cancelled', 'refused'], 'qualification' => ['qualified', 'contacted', 'referred', 'closed', 'cancelled', 'refused'], 'qualified' => ['contacted', 'contact_accepted', 'due_diligence', 'referred', 'closed', 'cancelled', 'refused'], 'contacted' => ['contact_accepted', 'due_diligence', 'negotiation', 'referred', 'closed', 'cancelled', 'refused'], 'contact_accepted' => ['due_diligence', 'negotiation', 'closed', 'cancelled', 'refused'], 'referred' => ['in_review', 'contacted', 'closed', 'cancelled'], 'due_diligence' => ['negotiation', 'closed', 'cancelled', 'refused'], 'negotiation' => ['converted', 'closed', 'cancelled', 'refused'], 'pending' => ['in_review', 'contacted', 'closed', 'cancelled', 'refused'], 'closed' => ['in_review'], 'cancelled' => [], 'refused' => [], 'converted' => [], ]; public function transition(User $actor, InterestRequest $interest, string $status, ?string $note = null, ?string $nextAction = null, ?string $dueAt = null, ?int $assigneeId = null, bool $assignmentProvided = false, bool $actionProvided = false): InterestRequest { if (! in_array($status, array_column(InterestStatus::cases(), 'value'), true)) { throw ValidationException::withMessages(['status' => 'Estado inválido.']); } return DB::transaction(function () use ($actor, $interest, $status, $note, $nextAction, $dueAt, $assigneeId, $assignmentProvided, $actionProvided) { $locked = InterestRequest::query()->lockForUpdate()->findOrFail($interest->id); abort_unless(app(ScopedPortalAdministration::class)->interest($actor, $locked), 403); $from = $locked->status; $actionDate = $dueAt ? Carbon::parse($dueAt)->toDateString() : null; $followUpChanged = ($actionProvided && ($locked->next_action !== $nextAction || $locked->next_action_due_at?->toDateString() !== $actionDate)) || ($assignmentProvided && $locked->assigned_to !== $assigneeId); if ($from !== $status && ! in_array($status, self::TRANSITIONS[$from] ?? [], true)) { throw ValidationException::withMessages(['status' => 'Esta transição não é permitida a partir do estado actual.']); } $locked->forceFill([ 'status' => $status, 'reviewed_at' => now(), 'reviewed_by' => $actor->id, ...($actionProvided ? ['next_action' => $nextAction, 'next_action_due_at' => $dueAt] : []), ...($assignmentProvided ? ['assigned_to' => $assigneeId] : []), ])->save(); if ($followUpChanged) { DB::table('tasks')->where('interest_request_id', $locked->id)->where('task_type', 'interest_follow_up') ->whereIn('status', ['open', 'in_progress'])->update([ 'status' => 'cancelled', 'completed_at' => now(), 'outcome' => 'superseded', 'updated_at' => now(), ]); } if ($followUpChanged && $locked->next_action && $locked->next_action_due_at) { $correlationId = (string) Str::uuid(); $assignee = $assignmentProvided ? $assigneeId : $locked->assigned_to; $externalId = 'InterestFollowUp:'.$locked->id.':'.hash('sha256', $locked->next_action.'|'.$locked->next_action_due_at.'|'.($assignee ?? '')); DB::table('tasks')->insertOrIgnore([ 'source_system' => 'portal-interest', 'external_id' => $externalId, 'version_no' => 1, 'data_classification' => 'confidential', 'correlation_id' => $correlationId, 'assignee_user_id' => $assignee, 'interest_request_id' => $locked->id, 'task_type' => 'interest_follow_up', 'title' => mb_substr($locked->next_action, 0, 200), 'status' => 'open', 'priority' => 3, 'due_at' => Carbon::parse($locked->next_action_due_at)->endOfDay(), 'created_at' => now(), 'updated_at' => now(), ]); } if ($from !== $status) { $correlationId = (string) Str::uuid(); DB::table('business_workflow_transitions')->insert([ 'id' => (string) Str::uuid(), 'interest_request_id' => $locked->id, 'from_status' => $from, 'to_status' => $status, 'actor_id' => $actor->id, 'reason' => $note ? Crypt::encryptString($note) : null, 'correlation_id' => $correlationId, 'metadata' => json_encode(['source' => 'management-interest'], JSON_THROW_ON_ERROR), 'created_at' => now(), ]); } return $locked->refresh(); }); } }