@@ -9,6 +9,9 @@ use App\Models\Requisition;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\ComparisonPolicy;
|
||||
use App\Services\ProposalComparison;
|
||||
use App\Services\RequisitionWorkflow;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
function supplierPayload(string $nuit = '123456789'): array
|
||||
@@ -50,15 +53,52 @@ test('supplier review is independent and prevents blocked activation', function
|
||||
});
|
||||
|
||||
test('quotes calculate decimal totals inherit tax and reject duplicates', function () {
|
||||
$this->get('/requisicoes/criar')->assertInertia(fn (Assert $page) => $page->where('defaultTaxRate', '16.00'));
|
||||
$this->post("/requisicoes/{$this->r->id}/cotacoes", [...$this->quote, 'total_minor' => 999, 'tenant_id' => 999])->assertSessionHasNoErrors()->assertRedirect();
|
||||
$q = Quotation::query()->firstOrFail();
|
||||
expect($q->subtotal_minor)->toBe(15)->and($q->tax_minor)->toBe(2)->and($q->total_minor)->toBe(17)->and($q->tenant_id)->toBe($this->tenant->id);
|
||||
expect((float) $q->items()->first()->tax_rate)->toBe(16.0);
|
||||
$result = app(ProposalComparison::class)->inspect($this->r);
|
||||
$basis = array_map(fn ($row) => [
|
||||
'quotation' => array_intersect_key($row, array_flip(['id', 'supplier_id', 'reference', 'delivery_days', 'valid_until', 'payment_terms', 'subtotal_minor', 'tax_minor', 'total_minor', 'currency', 'weights'])),
|
||||
'items' => $row['items'], 'evaluations' => $row['evaluations'], 'qualified_ids' => array_column($row['qualified_evaluations'], 'id'),
|
||||
'eligible' => $row['eligible'], 'score_minor' => $row['score_minor'],
|
||||
], $result['rows']);
|
||||
expect($result['fingerprint'])->toBe(hash('sha256', json_encode($basis, JSON_THROW_ON_ERROR)));
|
||||
$this->post("/requisicoes/{$this->r->id}/cotacoes", $this->quote)->assertSessionHasErrors('supplier_id');
|
||||
expect(Quotation::query()->count())->toBe(1);
|
||||
expect(AuditLog::query()->where('action', 'quotation.created')->count())->toBe(1);
|
||||
});
|
||||
|
||||
test('unit discounts are explicit calculated before vat and preserved for comparison', function (string $type, string $discount, int $net, int $tax) {
|
||||
$this->post("/requisicoes/{$this->r->id}/cotacoes", [...$this->quote, 'items' => [['requisition_item_id' => $this->item->id, 'unit_price' => '10.00', 'discount_type' => $type, 'discount_value' => $discount]]])->assertSessionHasNoErrors();
|
||||
$quote = Quotation::query()->sole();
|
||||
expect($quote->items()->sole()->unit_price_minor)->toBe($net)
|
||||
->and($quote->subtotal_minor)->toBe(3 * $net)->and($quote->tax_minor)->toBe($tax)
|
||||
->and($quote->commercial_terms['lines'][0]['gross_unit_minor'])->toBe(1000)
|
||||
->and($quote->commercial_terms['lines'][0]['discount_type'])->toBe($type);
|
||||
$result = app(ProposalComparison::class)->inspect($this->r);
|
||||
expect($result['rows'][0]['eligible'])->toBeTrue();
|
||||
$terms = $quote->commercial_terms;
|
||||
$terms['lines'][0]['discount_value'] = '99.00';
|
||||
$quote->update(['commercial_terms' => $terms]);
|
||||
expect(app(ProposalComparison::class)->inspect($this->r)['rows'][0]['eligible'])->toBeFalse();
|
||||
})->with([['percent', '12.50', 875, 420], ['amount', '1.25', 875, 420]]);
|
||||
|
||||
test('missing prices invalid discounts and free units cannot become comparison minima', function (array $line) {
|
||||
$this->postJson("/requisicoes/{$this->r->id}/cotacoes", [...$this->quote, 'items' => [['requisition_item_id' => $this->item->id, ...$line]]])->assertUnprocessable();
|
||||
expect(Quotation::query()->count())->toBe(0);
|
||||
})->with([
|
||||
[['unit_price' => '']],
|
||||
[['unit_price' => '0']],
|
||||
[['unit_price' => '10', 'discount_type' => 'percent', 'discount_value' => '100']],
|
||||
[['unit_price' => '10', 'discount_type' => 'percent', 'discount_value' => '101']],
|
||||
[['unit_price' => '10', 'discount_type' => 'amount', 'discount_value' => '11']],
|
||||
[['unit_price' => '10', 'discount_type' => 'none', 'discount_value' => '1']],
|
||||
[['unit_price' => '10', 'discount_type' => 'percent']],
|
||||
[['unit_price' => '0.01', 'discount_type' => 'percent', 'discount_value' => '50.01']],
|
||||
]);
|
||||
|
||||
test('quotes require approval active suppliers valid dates and exact request items', function () {
|
||||
$url = "/requisicoes/{$this->r->id}/cotacoes";
|
||||
$this->r->update(['status' => 'draft']);
|
||||
@@ -84,6 +124,7 @@ test('two independent evaluations and three eligible suppliers complete comparis
|
||||
$this->actingAs($this->finance)->post("/cotacoes/{$q->id}/avaliar", $ev)->assertSessionHasNoErrors();
|
||||
}
|
||||
$this->get("/requisicoes/{$this->r->id}/cotacoes")->assertInertia(fn (Assert $page) => $page->component('quotations/show')->where('ready', true)->has('quotations', 3)->where('quotations.0.score', 8.65));
|
||||
expect(app(ProposalComparison::class)->inspect($this->r)['top_ids'])->toHaveCount(3);
|
||||
$this->supplier->update(['status' => 'suspended']);
|
||||
$this->get("/requisicoes/{$this->r->id}/cotacoes")->assertInertia(fn (Assert $page) => $page->where('ready', false)->where('quotations.0.eligible', false));
|
||||
});
|
||||
@@ -95,6 +136,38 @@ test('requester cannot evaluate own purchase after role change', function () {
|
||||
$this->actingAs($this->owner)->post("/cotacoes/{$q->id}/avaliar", ['quality' => 8, 'history' => 5, 'payment' => 8, 'comment' => 'Avaliação de pedido próprio.', 'no_conflict' => true])->assertForbidden();
|
||||
});
|
||||
|
||||
test('configured comparison uses the preserved minimum and weights and retains independent evaluators', function () {
|
||||
$policy = app(ComparisonPolicy::class)->checked(['revision' => 1, 'configured' => true, 'minimum_proposals' => 1, 'minimum_evaluators' => 1,
|
||||
'weights' => ['price' => 100, 'quality' => 0, 'delivery' => 0, 'history' => 0, 'payment' => 0]]);
|
||||
$this->r->update(['comparison_policy' => $policy]);
|
||||
$this->post("/requisicoes/{$this->r->id}/cotacoes", $this->quote)->assertSessionHasNoErrors();
|
||||
$quote = Quotation::query()->sole();
|
||||
expect($quote->weights)->toBe($policy['weights'])->and(app(ProposalComparison::class)->inspect($this->r)['ready'])->toBeFalse();
|
||||
$this->post("/cotacoes/{$quote->id}/avaliar", ['quality' => 8, 'history' => 5, 'payment' => 8, 'comment' => 'Conferência independente da proposta.', 'no_conflict' => true])->assertSessionHasNoErrors();
|
||||
$before = app(ProposalComparison::class)->inspect($this->r);
|
||||
expect($before['ready'])->toBeTrue()->and($before['rows'][0]['score_minor'])->toBe(1000);
|
||||
$this->tenant->update(['comparison_policy' => ComparisonPolicy::DEFAULTS]);
|
||||
expect(app(ProposalComparison::class)->inspect($this->r)['fingerprint'])->toBe($before['fingerprint']);
|
||||
$this->get("/requisicoes/{$this->r->id}/cotacoes")->assertInertia(fn (Assert $page) => $page->where('comparisonPolicy.minimum_evaluators', 1)->where('ready', true));
|
||||
$this->owner->update(['purchasing_role' => PurchasingRole::Finance]);
|
||||
$this->actingAs($this->owner)->post("/cotacoes/{$quote->id}/avaliar", ['quality' => 8, 'history' => 5, 'payment' => 8, 'comment' => 'O solicitante continua impedido.', 'no_conflict' => true])->assertForbidden();
|
||||
});
|
||||
|
||||
test('new requests freeze company comparison rules and edits cannot replace that snapshot', function () {
|
||||
$policy = [...ComparisonPolicy::DEFAULTS, 'revision' => 1, 'configured' => true, 'minimum_proposals' => 2];
|
||||
$this->tenant->update(['comparison_policy' => $policy]);
|
||||
$this->actingAs($this->owner);
|
||||
$data = ['cost_center_id' => $this->center->id, 'priority' => 'normal', 'needed_by' => now()->addWeek()->format('Y-m-d'), 'justification' => 'Nova requisição com regra da empresa.',
|
||||
'items' => [['description' => 'Material', 'category' => 'Material', 'purchase_type' => 'consumable', 'quantity' => '1', 'unit' => 'un', 'unit_price' => '10.00', 'tax_rate' => '16']]];
|
||||
$workflow = app(RequisitionWorkflow::class);
|
||||
$created = $workflow->save($this->owner, [...$data, 'comparison_policy' => ['minimum_proposals' => 0]]);
|
||||
expect($created->comparison_policy)->toBe($policy)->and($this->r->fresh()->comparison_policy)->toBeNull();
|
||||
$next = [...$policy, 'revision' => 2, 'minimum_proposals' => 1];
|
||||
$this->tenant->update(['comparison_policy' => $next]);
|
||||
$workflow->save($this->owner, [...$data, 'comparison_policy' => $next], $created);
|
||||
expect($created->fresh()->comparison_policy)->toBe($policy)->and($workflow->save($this->owner, $data)->comparison_policy)->toBe($next);
|
||||
});
|
||||
|
||||
test('supplier quote and evaluation routes isolate companies', function () {
|
||||
$this->post("/requisicoes/{$this->r->id}/cotacoes", $this->quote);
|
||||
$q = Quotation::query()->firstOrFail();
|
||||
|
||||
Reference in New Issue
Block a user