commit1
tests / ci (push) Waiting to run

This commit is contained in:
2026-10-28 12:23:16 +02:00
parent 4d42f083ce
commit eb1c8b8074
20 changed files with 2141 additions and 58 deletions
+20 -5
View File
@@ -3,10 +3,13 @@
namespace App\Services;
use App\Enums\PurchasingRole;
use App\Models\ProposalEvaluation;
use App\Models\PurchaseAward;
use App\Models\PurchaseOrder;
use App\Models\Quotation;
use App\Models\QuotationItem;
use App\Models\Requisition;
use App\Models\Supplier;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Support\Facades\DB;
@@ -112,6 +115,11 @@ class PurchaseWorkflow
...$source->only(['unit', 'category', 'purchase_type']), 'tenant_id' => $user->tenant_id,
];
}
$validUntil = $quote->attributesToArray()['valid_until'] ?? null;
$deliveryDate = $award->attributesToArray()['delivery_date'] ?? null;
if (! is_string($validUntil) || ! is_string($deliveryDate)) {
throw ValidationException::withMessages(['delivery_date' => 'As datas da proposta e da entrega devem estar completas antes da emissão.']);
}
$snapshot = [
'development_only' => true, 'budget_checked' => false,
'company' => Tenant::query()->findOrFail($user->tenant_id)->only(['id', 'name']),
@@ -120,12 +128,12 @@ class PurchaseWorkflow
'cost_center' => $requisition->costCenter->only(['id', 'code', 'name']),
'requester' => $requisition->requester?->only(['id', 'name']),
'supplier' => $supplier->only(['id', 'name', 'nuit', 'contact', 'email', 'phone', 'address']),
'quotation' => $quote->only(['id', 'reference', 'currency', 'payment_terms', 'delivery_days', 'valid_until']),
'award' => $award->only(['id', 'justification', 'delivery_date', 'delivery_location']),
'quotation' => [...$quote->only(['id', 'reference', 'currency', 'payment_terms', 'delivery_days']), 'valid_until' => $validUntil],
'award' => [...$award->only(['id', 'justification', 'delivery_location']), 'delivery_date' => $deliveryDate],
'issuer' => $user->only(['id', 'name']), 'approval_rules' => $award->approval_snapshot,
'estimate_approvals' => $requisition->steps()->get()->toArray(),
'final_approvals' => $award->approvals()->get()->toArray(),
'decision_matrix' => $award->decision_matrix, 'items' => $lines,
'decision_matrix' => $this->comparison->inspect($requisition)['rows'], 'items' => $lines,
];
$order = PurchaseOrder::query()->create([
'tenant_id' => $user->tenant_id, 'requisition_id' => $requisition->id, 'purchase_award_id' => $award->id,
@@ -161,6 +169,13 @@ class PurchaseWorkflow
/** @return array{rows: list<array<string, mixed>>, ready: bool, top_ids: list<int>, fingerprint: string} */
private function checkComparison(User $user, Requisition $requisition, Quotation $quote): array
{
// Also coordinate with supplier reviews, which lock supplier rows without the company mutex.
$quotes = Quotation::query()->where('requisition_id', $requisition->id)->orderBy('id')->lockForUpdate()->get(['id', 'supplier_id']);
Supplier::query()->whereIn('id', $quotes->pluck('supplier_id'))->orderBy('id')->lockForUpdate()->get();
QuotationItem::query()->whereIn('quotation_id', $quotes->pluck('id'))->orderBy('id')->lockForUpdate()->get();
ProposalEvaluation::query()->whereIn('quotation_id', $quotes->pluck('id'))->orderBy('id')->lockForUpdate()->get();
User::query()->where('tenant_id', $user->tenant_id)->orderBy('id')->lockForUpdate()->get();
$requisition->items()->orderBy('id')->lockForUpdate()->get();
$this->guard->assertParticipation($user, $requisition);
$this->guard->assertProcess($requisition, $quote->supplier_id, $user);
$result = $this->comparison->inspect($requisition);
@@ -213,7 +228,7 @@ class PurchaseWorkflow
throw ValidationException::withMessages(['approvals' => 'A requisição deve ter todas as aprovações obrigatórias da estimativa, preservadas no histórico.']);
}
foreach ($steps as $index => $step) {
if ($step->decision !== 'approved' || $step->role !== $roles[$index] || $step->approver_id === null || in_array($step->approver_id, $seen, true) ||
if ($step->decision !== 'approved' || $step->decided_at === null || $step->role !== $roles[$index] || $step->approver_id === null || in_array($step->approver_id, $seen, true) ||
! User::query()->where('tenant_id', $requisition->tenant_id)->whereKey($step->approver_id)->exists()) {
throw ValidationException::withMessages(['approvals' => 'O histórico de aprovação da estimativa está incompleto ou inconsistente.']);
}
@@ -232,7 +247,7 @@ class PurchaseWorkflow
}
foreach ($steps as $index => $step) {
$user = User::query()->where('tenant_id', $requisition->tenant_id)->find($step->approver_id);
if (! $user || $step->decision !== 'approved' || $step->role !== $roles[$index] || $user->purchasing_role->value !== $step->role ||
if (! $user || $step->decision !== 'approved' || $step->decided_at === null || $step->role !== $roles[$index] || $user->purchasing_role->value !== $step->role ||
($step->role === 'department_head' && $user->department_id !== $requisition->department_id) || in_array($user->id, $seen, true)) {
throw ValidationException::withMessages(['approvals' => 'As aprovações do valor final devem continuar elegíveis e ser de pessoas distintas.']);
}