*/ protected function policies(): Builder { return PurchaseControlPolicy::query(); } /** @return Builder */ 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); } /** @param array $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 = $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 = $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; }); } 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.']); } 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 = $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.']); } return; } 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 = $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.']); } } $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 ($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, $this->auditPrefix().'.'.$decision, null, null, ['policy_id' => $policy->id, 'status' => $policy->status]); }); } public function current(): ?PurchaseControlPolicy { 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 !== $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.']); } return $policy; } }