Implementa admissão formal por política e adopção do processo

This commit is contained in:
2026-10-28 20:28:15 +02:00
parent 03ab308e02
commit d020be53ed
33 changed files with 1926 additions and 30 deletions
+76
View File
@@ -1,6 +1,8 @@
<?php
use App\Enums\PurchasingRole;
use App\Models\AdmissionAdoption;
use App\Models\AdmissionDecision;
use App\Models\AuditLog;
use App\Models\AwardCancellation;
use App\Models\Budget;
@@ -27,7 +29,9 @@ use App\Models\User;
use App\Models\Warehouse;
use App\Services\BudgetWorkflow;
use App\Services\ConflictGuard;
use App\Services\DocumentaryQualificationWorkflow;
use App\Services\EquipmentWorkflow;
use App\Services\FormalAdmission;
use App\Services\ProcurementReviewReport;
use App\Services\ProposalComparison;
use App\Services\ProposalOpeningWorkflow;
@@ -57,6 +61,29 @@ function selectPurchaseAward(object $test): PurchaseAward
return PurchaseAward::query()->firstOrFail();
}
function prepareFormalAwardAdmission(object $test): void
{
$workflow = app(FormalAdmission::class);
$test->actingAs($test->admin);
$policy = $workflow->policy($test->admin, ['previous_id' => 0, 'enabled' => true, 'roles' => ['finance', 'department_head'], 'required_count' => 2, 'reason' => 'Política fictícia para adjudicação']);
$test->actingAs($test->buyer);
$workflow->adopt($test->buyer, $test->r, $policy->id, 0, 'Adopção fictícia antes da adjudicação');
$opening = app(ProposalOpeningWorkflow::class);
$documentary = app(DocumentaryQualificationWorkflow::class);
$set = $documentary->define($test->buyer, $test->r, ['previous_id' => 0, 'basis_hash' => $opening->basis($opening->proposals($test->r)), 'reason' => 'Requisito exclusivamente fictício', 'criteria' => [['label' => 'Conformidade documentada', 'category' => 'technical', 'required' => true]]]);
foreach ($test->quotes as $quote) {
$test->actingAs($test->finance);
$documentary->assess($test->finance, $set, ['previous_id' => 0, 'quotation_id' => $quote->id, 'reason' => 'Análise exclusivamente fictícia', 'answers' => [['criterion_id' => 1, 'result' => 'met', 'evidence' => 'Documento fictício', 'reason' => 'Conformidade confirmada no teste']]]);
$test->actingAs($test->buyer);
$decision = $workflow->propose($test->buyer, $test->r, ['quotation_id' => $quote->id, 'previous_id' => 0, 'basis_hash' => $workflow->basis($test->r, $quote->id)['hash'], 'outcome' => 'admit', 'reason' => 'Proposta fundamentada no teste']);
foreach ([$test->finance, $test->head] as $reviewer) {
$test->actingAs($reviewer);
$workflow->review($reviewer, $decision, 'approved', 'Conferência independente fictícia');
}
}
$test->actingAs($test->buyer);
}
function approvePurchaseAward(object $test, PurchaseAward $award): void
{
foreach ($award->approvals()->get() as $step) {
@@ -644,3 +671,52 @@ test('review report reflects tied source scores and existing frozen orders witho
$report = app(ProcurementReviewReport::class)->build($this->buyer, $this->r->id);
expect($report['order']['number'])->toBe($order->number)->and($report['order']['total_minor'])->toBe(10000);
});
test('adopted formal admission preserves scoring and binds award and issued order', function () {
$before = app(ProposalComparison::class)->inspect($this->r);
prepareFormalAwardAdmission($this);
$after = app(ProposalComparison::class)->inspect($this->r);
expect($after['ready'])->toBeTrue()->and(array_column($after['rows'], 'score_minor'))->toBe(array_column($before['rows'], 'score_minor'));
$award = selectPurchaseAward($this);
expect($award->admission_snapshot['review_ids'])->toHaveCount(2)->and($award->admission_snapshot['adoption_id'])->toBeGreaterThan(0);
$order = issuePurchaseOrder($this, $award);
expect($order->snapshot['admission'])->toBe($award->admission_snapshot);
$this->suppliers[0]->update(['name' => 'Alteração posterior à emissão']);
$this->actingAs($this->buyer)->post('/adjudicacoes/'.$award->id.'/emitir')->assertSessionHasNoErrors();
expect(PurchaseOrder::query()->count())->toBe(1)->and($order->refresh()->snapshot['supplier']['name'])->toBe('Fornecedor 0');
$this->post('/requisicoes/'.$this->r->id.'/admissao/adoptar', ['policy_id' => 1, 'previous_id' => 1, 'reason' => 'Tentativa após a emissão'])->assertSessionHasErrors('admission');
});
test('formal exclusions cannot enter selection and minimum three eligible suppliers remains', function () {
prepareFormalAwardAdmission($this);
$workflow = app(FormalAdmission::class);
$adoption = $workflow->adoption($this->r);
$previous = AdmissionDecision::query()->where('quotation_id', $this->quotes[0]->id)->latest('id')->firstOrFail();
$decision = $workflow->propose($this->buyer, $this->r, ['quotation_id' => $this->quotes[0]->id, 'previous_id' => $previous->id, 'basis_hash' => $previous->basis_hash, 'outcome' => 'exclude', 'reason' => 'Exclusão fictícia fundamentada']);
foreach ([$this->finance, $this->head] as $reviewer) {
$this->actingAs($reviewer);
$workflow->review($reviewer, $decision, 'approved', 'Conferência de exclusão fictícia');
}
expect($workflow->status($decision, $adoption))->toBe('confirmed')->and(app(ProposalComparison::class)->inspect($this->r)['ready'])->toBeFalse();
$this->actingAs($this->buyer)->post($this->awardUrl, $this->selection)->assertSessionHasErrors();
expect(PurchaseAward::query()->count())->toBe(0)->and(BudgetEntry::query()->count())->toBe(0);
});
test('admission basis changes block emission and existing legacy award cannot adopt retroactively', function () {
prepareFormalAwardAdmission($this);
$award = selectPurchaseAward($this);
approvePurchaseAward($this, $award);
$this->suppliers[0]->update(['name' => 'Nome alterado antes de emitir']);
$this->actingAs($this->buyer)->post('/adjudicacoes/'.$award->id.'/emitir')->assertSessionHasErrors();
expect(PurchaseOrder::query()->count())->toBe(0);
});
test('legacy award keeps null admission basis and refuses later adoption', function () {
$award = selectPurchaseAward($this);
expect($award->admission_snapshot)->toBeNull();
$this->actingAs($this->admin);
$policy = app(FormalAdmission::class)->policy($this->admin, ['previous_id' => 0, 'enabled' => true, 'roles' => ['finance'], 'required_count' => 1, 'reason' => 'Política fictícia posterior']);
$this->actingAs($this->buyer)->post('/requisicoes/'.$this->r->id.'/admissao/adoptar', ['policy_id' => $policy->id, 'previous_id' => 0, 'reason' => 'Adopção retroactiva impedida'])->assertSessionHasErrors('admission');
expect(AdmissionAdoption::query()->count())->toBe(0);
issuePurchaseOrder($this, $award);
});