142 lines
8.3 KiB
PHP
142 lines
8.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\PurchaseOrder;
|
|
use App\Models\SupplierEvaluation;
|
|
use App\Models\SupplierImprovement;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class SupplierImprovementWorkflow
|
|
{
|
|
public function canParticipate(User $actor, SupplierEvaluation $evaluation): bool
|
|
{
|
|
if ($actor->tenant_id === null || $actor->tenant_id !== $evaluation->tenant_id
|
|
|| ! $actor->hasFunctions(SupplierEvaluationWorkflow::ROLES)
|
|
|| app(OrganizationAccess::class)->maintenanceOnly($actor)
|
|
|| ($actor->departmentScoped() && ($actor->department_id === null || $actor->department_id !== $evaluation->department_id))) {
|
|
return false;
|
|
}
|
|
$guard = app(ConflictGuard::class);
|
|
if ($guard->hasConflict($actor->id, $evaluation->supplier_id, null)) {
|
|
return false;
|
|
}
|
|
foreach (PurchaseOrder::query()->whereIn('id', $evaluation->payload['order_ids'])->pluck('requisition_id') as $id) {
|
|
if ($guard->hasConflict($actor->id, $evaluation->supplier_id, (int) $id)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function canAct(User $actor, SupplierEvaluation $evaluation, SupplierImprovement $measure, string $action): bool
|
|
{
|
|
if ($evaluation->status !== 'approved' || $measure->tenant_id !== $actor->tenant_id
|
|
|| $measure->supplier_evaluation_id !== $evaluation->id || ! $this->canParticipate($actor, $evaluation)) {
|
|
return false;
|
|
}
|
|
|
|
return match ($action) {
|
|
'plan' => in_array($measure->status, ['open', 'returned'], true),
|
|
'execute' => $measure->responsible_id === $actor->id && in_array($measure->status, ['open', 'returned'], true),
|
|
'review' => $measure->status === 'review' && ! in_array($actor->id, $measure->contributors, true),
|
|
default => false,
|
|
};
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
public function snapshot(SupplierImprovement $measure): array
|
|
{
|
|
return $measure->only(['id', 'supplier_evaluation_id', 'created_by', 'responsible_id', 'title', 'description', 'due_on', 'version', 'status', 'contributors', 'result', 'evidence', 'reviewed_by', 'review_reason', 'reviewed_at', 'created_at', 'updated_at']);
|
|
}
|
|
|
|
private function assignee(User $actor, SupplierEvaluation $evaluation, int $id): User
|
|
{
|
|
$user = User::query()->where('tenant_id', $actor->tenant_id)->lockForUpdate()->find($id);
|
|
if (! $user || ! $user->hasFunctions(SupplierEvaluationWorkflow::ROLES)
|
|
|| ($user->departmentScoped() && ($user->department_id === null || $user->department_id !== $evaluation->department_id))
|
|
|| ! $this->canParticipate($user, $evaluation)) {
|
|
throw ValidationException::withMessages(['responsible_id' => 'Escolha um responsável autorizado desta empresa, sem conflito no âmbito avaliado.']);
|
|
}
|
|
|
|
return $user;
|
|
}
|
|
|
|
/** @param array<string,mixed> $data */
|
|
public function execute(User $actor, int $evaluationId, ?int $id, string $command, array $data): SupplierImprovement
|
|
{
|
|
return DB::transaction(function () use ($actor, $evaluationId, $id, $command, $data) {
|
|
app(ConflictGuard::class)->lockCompany($actor);
|
|
$actor = app(CurrentActor::class)->lock($actor, SupplierEvaluationWorkflow::ROLES);
|
|
$evaluation = SupplierEvaluation::query()->visibleTo($actor)->lockForUpdate()->findOrFail($evaluationId);
|
|
abort_unless($evaluation->status === 'approved', 409, 'Associe medidas à revisão aprovada da avaliação.');
|
|
abort_unless($this->canParticipate($actor, $evaluation), 403, 'Existe um conflito no âmbito desta avaliação.');
|
|
if ($command === 'create') {
|
|
$plan = ['title' => $data['title'], 'description' => $data['description'], 'responsible_id' => (int) $data['responsible_id'], 'due_on' => $data['due_on']];
|
|
$hash = hash('sha256', json_encode([$evaluationId, $plan], JSON_THROW_ON_ERROR));
|
|
$existing = SupplierImprovement::query()->where('operation_key', $data['operation_key'])->first();
|
|
if ($existing) {
|
|
abort_unless($existing->created_by === $actor->id && $existing->creation_hash === $hash, 409, 'Pedido de criação já utilizado.');
|
|
|
|
return $existing;
|
|
}
|
|
$responsible = $this->assignee($actor, $evaluation, $plan['responsible_id']);
|
|
$measure = SupplierImprovement::query()->create([...$plan, 'tenant_id' => $actor->tenant_id, 'supplier_evaluation_id' => $evaluationId,
|
|
'created_by' => $actor->id, 'operation_key' => $data['operation_key'], 'creation_hash' => $hash,
|
|
'contributors' => array_values(array_unique([$actor->id, $responsible->id]))]);
|
|
$before = null;
|
|
} else {
|
|
$measure = SupplierImprovement::query()->where('supplier_evaluation_id', $evaluationId)->lockForUpdate()->findOrFail($id);
|
|
if ($measure->version !== (int) $data['version']) {
|
|
throw ValidationException::withMessages(['version' => 'A medida foi alterada. Consulte a versão actual antes de comparar; os seus dados não foram sobrepostos.']);
|
|
}
|
|
abort_if($measure->status === 'closed', 409, 'A medida já está encerrada e preservada.');
|
|
$before = $this->snapshot($measure);
|
|
if ($command === 'plan') {
|
|
abort_unless($this->canAct($actor, $evaluation, $measure, 'plan'), 409);
|
|
$responsible = $this->assignee($actor, $evaluation, (int) $data['responsible_id']);
|
|
$measure->fill(['title' => $data['title'], 'description' => $data['description'], 'responsible_id' => $responsible->id, 'due_on' => $data['due_on'],
|
|
'contributors' => array_values(array_unique([...$measure->contributors, $actor->id, $responsible->id]))]);
|
|
if (! $measure->isDirty()) {
|
|
return $measure;
|
|
}
|
|
if (mb_strlen(trim($data['reason'] ?? '')) < 10) {
|
|
throw ValidationException::withMessages(['reason' => 'Justifique a alteração do plano, prazo ou responsável.']);
|
|
}
|
|
} elseif (in_array($command, ['progress', 'submit'], true)) {
|
|
abort_unless($this->canAct($actor, $evaluation, $measure, 'execute'), 403);
|
|
$measure->fill(['result' => $data['result'] ?? '', 'evidence' => $data['evidence'] ?? '']);
|
|
if ($command === 'submit') {
|
|
if (trim($measure->result ?? '') === '' || trim($measure->evidence ?? '') === '') {
|
|
throw ValidationException::withMessages(['evidence' => 'Registe resultado e referência da evidência antes de pedir encerramento.']);
|
|
}
|
|
$measure->status = 'review';
|
|
}
|
|
if (! $measure->isDirty()) {
|
|
return $measure;
|
|
}
|
|
} else {
|
|
abort_unless(in_array($command, ['close', 'return'], true) && $measure->status === 'review', 409);
|
|
abort_unless($this->canAct($actor, $evaluation, $measure, 'review'), 403, 'O encerramento exige outra pessoa, independente da preparação e execução.');
|
|
if (mb_strlen(trim($data['reason'] ?? '')) < 10) {
|
|
throw ValidationException::withMessages(['reason' => 'Fundamente a decisão sobre a execução.']);
|
|
}
|
|
$measure->fill(['status' => $command === 'close' ? 'closed' : 'returned', 'reviewed_by' => $actor->id, 'reviewed_at' => now(), 'review_reason' => $data['reason']]);
|
|
}
|
|
$measure->version++;
|
|
$measure->save();
|
|
}
|
|
$after = $this->snapshot($measure);
|
|
if ($command === 'plan') {
|
|
$after['change_reason'] = $data['reason'];
|
|
}
|
|
app(RequisitionWorkflow::class)->audit($actor, 'supplier_improvement.'.$command, null, $before, $after);
|
|
|
|
return $measure;
|
|
});
|
|
}
|
|
}
|