@@ -6,11 +6,34 @@ use App\Enums\PurchasingRole;
|
||||
use App\Models\PurchaseControlPolicy;
|
||||
use App\Models\PurchaseControlReview;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class PurchaseControlPolicyWorkflow
|
||||
{
|
||||
/** @return Builder<PurchaseControlPolicy> */
|
||||
protected function policies(): Builder
|
||||
{
|
||||
return PurchaseControlPolicy::query();
|
||||
}
|
||||
|
||||
/** @return Builder<PurchaseControlReview> */
|
||||
protected function reviews(): Builder
|
||||
{
|
||||
return PurchaseControlReview::query();
|
||||
}
|
||||
|
||||
protected function mode(): string
|
||||
{
|
||||
return 'unissued_award';
|
||||
}
|
||||
|
||||
protected function auditPrefix(): string
|
||||
{
|
||||
return 'purchase_control';
|
||||
}
|
||||
|
||||
private function admin(User $user): void
|
||||
{
|
||||
abort_unless(auth()->id() === $user->id && $user->tenant_id !== null && $user->hasFunctions([PurchasingRole::Admin]), 403);
|
||||
@@ -25,12 +48,12 @@ class PurchaseControlPolicyWorkflow
|
||||
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') {
|
||||
$latest = $this->policies()->latest('revision')->first();
|
||||
if ((int) $data['previous_id'] !== ($latest->id ?? 0) || $data['mode'] !== $this->mode()) {
|
||||
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());
|
||||
$policy = $this->policies()->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, $this->auditPrefix().'.proposed', null, null, $policy->toArray());
|
||||
|
||||
return $policy;
|
||||
});
|
||||
@@ -39,6 +62,7 @@ class PurchaseControlPolicyWorkflow
|
||||
public function review(User $user, PurchaseControlPolicy $policy, string $decision, string $reason): void
|
||||
{
|
||||
$this->admin($user);
|
||||
abort_unless($policy->getTable() === $this->policies()->getModel()->getTable(), 403);
|
||||
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.']);
|
||||
@@ -47,8 +71,8 @@ class PurchaseControlPolicyWorkflow
|
||||
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();
|
||||
$policy = $this->policies()->lockForUpdate()->findOrFail($policy->id);
|
||||
$existing = $this->reviews()->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.']);
|
||||
@@ -56,35 +80,35 @@ class PurchaseControlPolicyWorkflow
|
||||
|
||||
return;
|
||||
}
|
||||
if ($policy->status !== 'pending' || PurchaseControlPolicy::query()->max('revision') !== $policy->revision) {
|
||||
if ($policy->status !== 'pending' || $this->policies()->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();
|
||||
$ids = $this->reviews()->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->hasFunctions([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()]);
|
||||
$this->reviews()->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) {
|
||||
} elseif ($this->reviews()->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]);
|
||||
app(RequisitionWorkflow::class)->audit($user, $this->auditPrefix().'.'.$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();
|
||||
return $this->policies()->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) {
|
||||
if (! $policy || $policy->valid_until < today()->format('Y-m-d') || $policy->mode !== $this->mode() || ! $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.']);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user