110 lines
5.8 KiB
PHP
110 lines
5.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Domain\Interests\Enums\InterestStatus;
|
|
use App\Models\InterestRequest;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
final class InterestWorkflow
|
|
{
|
|
/** @var array<string, list<string>> */
|
|
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();
|
|
});
|
|
}
|
|
}
|