Files
compras/app/Services/PurchaseControlPolicyWorkflow.php
T

94 lines
5.4 KiB
PHP

<?php
namespace App\Services;
use App\Enums\PurchasingRole;
use App\Models\PurchaseControlPolicy;
use App\Models\PurchaseControlReview;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class PurchaseControlPolicyWorkflow
{
private function admin(User $user): void
{
abort_unless(auth()->id() === $user->id && $user->tenant_id !== null && $user->purchasing_role === PurchasingRole::Admin, 403);
}
/** @param array<string,mixed> $data */
public function propose(User $user, array $data): PurchaseControlPolicy
{
$this->admin($user);
return DB::transaction(function () use ($user, $data) {
app(ConflictGuard::class)->lockCompany($user);
$user = User::query()->where('tenant_id', $user->tenant_id)->lockForUpdate()->findOrFail($user->id);
$this->admin($user);
$latest = PurchaseControlPolicy::query()->latest('revision')->first();
if ((int) $data['previous_id'] !== ($latest->id ?? 0) || $data['mode'] !== 'unissued_award') {
throw ValidationException::withMessages(['cancellation' => 'Versão desactualizada ou regra não suportada. Não se presume um circuito de excepção.']);
}
$policy = PurchaseControlPolicy::query()->create(['tenant_id' => $user->tenant_id, 'created_by' => $user->id, 'revision' => ($latest->revision ?? 0) + 1, 'mode' => $data['mode'], 'enabled' => $data['enabled'], 'valid_from' => $data['valid_from'], 'valid_until' => $data['valid_until'], 'reason' => $data['reason']]);
app(RequisitionWorkflow::class)->audit($user, 'purchase_control.proposed', null, null, $policy->toArray());
return $policy;
});
}
public function review(User $user, PurchaseControlPolicy $policy, string $decision, string $reason): void
{
$this->admin($user);
abort_unless($policy->tenant_id === $user->tenant_id && $policy->created_by !== $user->id, 403);
if (! in_array($decision, ['approved', 'rejected']) || trim($reason) === '') {
throw ValidationException::withMessages(['cancellation' => 'Decisão e fundamento obrigatórios.']);
}
DB::transaction(function () use ($user, $policy, $decision, $reason) {
app(ConflictGuard::class)->lockCompany($user);
$user = User::query()->where('tenant_id', $user->tenant_id)->lockForUpdate()->findOrFail($user->id);
$this->admin($user);
$policy = PurchaseControlPolicy::query()->lockForUpdate()->findOrFail($policy->id);
$existing = PurchaseControlReview::query()->where('purchase_control_policy_id', $policy->id)->where('reviewed_by', $user->id)->first();
if ($existing) {
if ($existing->decision !== $decision || $existing->reason !== $reason) {
throw ValidationException::withMessages(['cancellation' => 'Decisão anterior imutável.']);
}
return;
}
if ($policy->status !== 'pending' || PurchaseControlPolicy::query()->max('revision') !== $policy->revision) {
throw ValidationException::withMessages(['cancellation' => 'Versão decidida ou substituída por proposta mais recente.']);
}
if ($decision === 'approved') {
$ids = PurchaseControlReview::query()->where('purchase_control_policy_id', $policy->id)->where('decision', 'approved')->pluck('reviewed_by')->push($policy->created_by)->push($user->id)->unique();
$actors = User::query()->where('tenant_id', $user->tenant_id)->whereIn('id', $ids)->orderBy('id')->lockForUpdate()->get();
if ($actors->count() !== $ids->count() || $actors->contains(fn (User $actor) => $actor->purchasing_role !== PurchasingRole::Admin)) {
throw ValidationException::withMessages(['cancellation' => 'Proponente e validadores devem continuar elegíveis até à publicação.']);
}
}
PurchaseControlReview::query()->create(['tenant_id' => $user->tenant_id, 'purchase_control_policy_id' => $policy->id, 'reviewed_by' => $user->id, 'decision' => $decision, 'reason' => $reason, 'created_at' => now()]);
if ($decision === 'rejected') {
$policy->update(['status' => 'rejected', 'decided_at' => now()]);
} elseif (PurchaseControlReview::query()->where('purchase_control_policy_id', $policy->id)->where('decision', 'approved')->count() === 2) {
$policy->update(['status' => 'approved', 'decided_at' => now()]);
}
app(RequisitionWorkflow::class)->audit($user, 'purchase_control.'.$decision, null, null, ['policy_id' => $policy->id, 'status' => $policy->status]);
});
}
public function current(): ?PurchaseControlPolicy
{
return PurchaseControlPolicy::query()->where('status', 'approved')->where('valid_from', '<=', today()->format('Y-m-d'))->latest('revision')->first();
}
public function requireCurrent(): PurchaseControlPolicy
{
$policy = $this->current();
if (! $policy || $policy->valid_until < today()->format('Y-m-d') || $policy->mode !== 'unissued_award' || ! $policy->enabled) {
throw ValidationException::withMessages(['cancellation' => 'A empresa deve activar o cancelamento de adjudicações sem ordem emitida numa política vigente, com duas aprovações administrativas independentes.']);
}
return $policy;
}
}