Files
compras/tests/Feature/SupplierQuotationTest.php
2026-11-02 20:27:11 +02:00

312 lines
26 KiB
PHP

<?php
use App\Enums\PurchasingRole;
use App\Models\AuditLog;
use App\Models\CostCenter;
use App\Models\Department;
use App\Models\Quotation;
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 Illuminate\Support\Facades\DB;
use Inertia\Testing\AssertableInertia as Assert;
require_once __DIR__.'/../Support/SupplyFixture.php';
function supplierPayload(string $nuit = '123456789'): array
{
return ['name' => 'Fornecedor de teste', 'nuit' => $nuit, 'contact' => 'Contacto', 'email' => 'test@example.com', 'phone' => '123456789', 'address' => 'Maputo', 'category' => 'Material', 'beneficiaries' => 'Beneficiário teste', 'document_references' => 'Alvará e declaração verificados no processo 123.', 'diligence_notes' => 'Documentação conferida e sem impedimentos.', 'blocked' => false, 'next_review' => now()->addYear()->format('Y-m-d')];
}
beforeEach(function () {
$this->tenant = Tenant::query()->create(['name' => 'Cotação A']);
$this->department = Department::query()->create(['tenant_id' => $this->tenant->id, 'name' => 'Produção']);
$this->center = CostCenter::query()->create(['tenant_id' => $this->tenant->id, 'department_id' => $this->department->id, 'name' => 'Produção', 'code' => 'P']);
$this->buyer = User::factory()->create(['tenant_id' => $this->tenant->id, 'department_id' => $this->department->id, 'purchasing_role' => PurchasingRole::Procurement]);
$this->finance = User::factory()->create(['tenant_id' => $this->tenant->id, 'department_id' => $this->department->id, 'purchasing_role' => PurchasingRole::Finance]);
$this->owner = User::factory()->create(['tenant_id' => $this->tenant->id, 'department_id' => $this->department->id, 'purchasing_role' => PurchasingRole::Requester]);
$this->actingAs($this->buyer);
$this->supplier = Supplier::query()->create([...supplierPayload(), 'tenant_id' => $this->tenant->id, 'created_by' => $this->buyer->id, 'status' => 'active']);
$this->r = Requisition::query()->create(['tenant_id' => $this->tenant->id, 'requester_id' => $this->owner->id, 'department_id' => $this->department->id, 'cost_center_id' => $this->center->id, 'number' => 'REQ-TEST', 'priority' => 'normal', 'needed_by' => now()->addWeek(), 'justification' => 'Pedido aprovado para testes.', 'status' => 'approved', 'subtotal_minor' => 15, 'tax_minor' => 2, 'total_minor' => 17]);
$this->item = $this->r->items()->create(['tenant_id' => $this->tenant->id, 'description' => 'Material', 'category' => 'Material', 'purchase_type' => 'consumable', 'quantity' => '3', 'unit' => 'un', 'unit_price_minor' => 5, 'tax_rate' => '16', 'subtotal_minor' => 15, 'tax_minor' => 2, 'total_minor' => 17]);
$this->quote = ['supplier_id' => $this->supplier->id, 'reference' => 'PROP-01', 'delivery_days' => 10, 'valid_until' => now()->addMonth()->format('Y-m-d'), 'payment_terms' => '30 dias', 'no_conflict' => true, 'items' => [['requisition_item_id' => $this->item->id, 'unit_price' => '0.05']]];
});
test('supplier creation enforces role tenant and unique nuit', function () {
$this->actingAs($this->owner)->post('/fornecedores', supplierPayload('987654321'))->assertForbidden();
$this->actingAs($this->buyer)->post('/fornecedores', [...supplierPayload('987654321'), 'status' => 'active', 'tenant_id' => 999])->assertSessionHasNoErrors()->assertRedirect();
$s = Supplier::query()->where('nuit', '987654321')->firstOrFail();
expect($s->status)->toBe('pending')->and($s->tenant_id)->toBe($this->tenant->id);
$this->post('/fornecedores', supplierPayload('987654321'))->assertSessionHasErrors('nuit');
$this->post('/fornecedores', supplierPayload('invalid'))->assertSessionHasErrors('nuit');
});
test('supplier review is independent and prevents blocked activation', function () {
$body = ['status' => 'active', 'comment' => 'Documentação verificada por Finanças.', 'version' => $this->supplier->version];
$this->post("/fornecedores/{$this->supplier->id}/validar", $body)->assertForbidden();
$this->buyer->forceFill(['purchasing_role' => PurchasingRole::Finance])->save();
$this->actingAs($this->buyer)->post("/fornecedores/{$this->supplier->id}/validar", $body)->assertForbidden();
$this->actingAs($this->finance)->post("/fornecedores/{$this->supplier->id}/validar", $body)->assertSessionHasNoErrors()->assertRedirect();
$this->supplier->update(['blocked' => true]);
$this->post("/fornecedores/{$this->supplier->id}/validar", [...$body, 'version' => $this->supplier->fresh()->version])->assertSessionHasErrors('status');
});
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']);
$this->post($url, $this->quote)->assertForbidden();
$this->r->update(['status' => 'approved']);
$this->supplier->update(['status' => 'pending']);
$this->post($url, $this->quote)->assertSessionHasErrors('supplier_id');
$this->supplier->update(['status' => 'active']);
$this->post($url, [...$this->quote, 'valid_until' => now()->subDay()->format('Y-m-d')])->assertSessionHasErrors('valid_until');
$this->post($url, [...$this->quote, 'items' => [['requisition_item_id' => 999, 'unit_price' => '1']]])->assertSessionHasErrors('items');
$this->post($url, [...$this->quote, 'no_conflict' => false])->assertSessionHasErrors('no_conflict');
expect(Quotation::query()->count())->toBe(0);
});
test('two independent evaluations and three eligible suppliers complete comparison', function () {
foreach (['123456789', '123456788', '123456787'] as $index => $nuit) {
$s = $index === 0 ? $this->supplier : Supplier::query()->create([...supplierPayload($nuit), 'tenant_id' => $this->tenant->id, 'created_by' => $this->buyer->id, 'status' => 'active']);
$this->actingAs($this->buyer)->post("/requisicoes/{$this->r->id}/cotacoes", [...$this->quote, 'supplier_id' => $s->id])->assertSessionHasNoErrors();
$q = Quotation::query()->latest('id')->firstOrFail();
$ev = ['quality' => 8, 'history' => 5, 'payment' => 8, 'comment' => 'Avaliação técnica independente.', 'no_conflict' => true];
$this->post("/cotacoes/{$q->id}/avaliar", $ev)->assertSessionHasNoErrors();
$this->post("/cotacoes/{$q->id}/avaliar", $ev)->assertSessionHasErrors('evaluation');
$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));
});
test('requester cannot evaluate own purchase after role change', function () {
$this->post("/requisicoes/{$this->r->id}/cotacoes", $this->quote);
$q = Quotation::query()->firstOrFail();
$this->owner->forceFill(['purchasing_role' => PurchasingRole::Finance])->save();
$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->forceFill(['purchasing_role' => PurchasingRole::Finance])->save();
$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();
$tenant = Tenant::query()->create(['name' => 'Empresa B']);
$other = User::factory()->create(['tenant_id' => $tenant->id, 'purchasing_role' => PurchasingRole::Finance]);
$this->actingAs($other)->get('/fornecedores')->assertInertia(fn (Assert $p) => $p->has('suppliers.data', 0));
$this->post("/fornecedores/{$this->supplier->id}/validar", ['status' => 'active', 'comment' => 'Tentativa de outro inquilino.'])->assertNotFound();
$this->get("/requisicoes/{$this->r->id}/cotacoes")->assertNotFound();
$this->post("/cotacoes/{$q->id}/avaliar", ['quality' => 8, 'history' => 5, 'payment' => 8, 'comment' => 'Tentativa de outro inquilino.', 'no_conflict' => true])->assertNotFound();
});
function quotationTaskRegister(object $test): Quotation
{
$test->actingAs($test->buyer)->post('/requisicoes/'.$test->r->id.'/cotacoes', $test->quote)->assertSessionHasNoErrors();
return Quotation::query()->sole();
}
function quotationTaskEvaluation(): array
{
return ['quality' => 8, 'history' => 5, 'payment' => 8, 'comment' => 'Avaliação sintética fundamentada.', 'no_conflict' => true];
}
test('quotation tasks link missing proposals and each independent evaluation to the existing forms', function () {
$path = '/requisicoes/'.$this->r->id.'/cotacoes';
$this->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'quotation_prepare'));
$this->get($path.'?prepare=1')->assertInertia(fn (Assert $page) => $page->where('prepare', true)->where('canCreate', true)->where('requisition.items.0.id', $this->item->id));
$quote = quotationTaskRegister($this);
$this->actingAs($this->finance)->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)
->where('tasks.data.0.href', $path.'?quotation_id='.$quote->id.'#quotation-'.$quote->id)->missing('tasks.data.0.total_minor')->missing('tasks.data.0.evaluations'));
$this->get($path.'?quotation_id='.$quote->id)->assertHeader('Cache-Control', 'max-age=0, no-store, private')
->assertInertia(fn (Assert $page) => $page->where('focusQuotationId', $quote->id)->where('quotations.0.canEvaluate', true));
$this->post('/cotacoes/'.$quote->id.'/avaliar', quotationTaskEvaluation())->assertSessionHasNoErrors();
$this->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->actingAs($this->buyer)->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 2));
$this->post('/cotacoes/'.$quote->id.'/avaliar', quotationTaskEvaluation())->assertSessionHasNoErrors();
$this->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'quotation_prepare'));
});
test('quotation tasks use preserved comparison minima and hand the ready result to procurement', function () {
$policy = [...ComparisonPolicy::DEFAULTS, 'revision' => 1, 'configured' => true, 'minimum_proposals' => 1, 'minimum_evaluators' => 1];
$this->r->update(['comparison_policy' => $policy]);
$quote = quotationTaskRegister($this);
$this->actingAs($this->finance)->post('/cotacoes/'.$quote->id.'/avaliar', quotationTaskEvaluation())->assertSessionHasNoErrors();
$this->tenant->update(['comparison_policy' => [...$policy, 'revision' => 2, 'minimum_proposals' => 5, 'minimum_evaluators' => 5]]);
$this->actingAs($this->buyer)->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)
->where('tasks.data.0.task', 'comparison_review')->where('tasks.data.0.href', '/requisicoes/'.$this->r->id.'/adjudicacao'));
$this->actingAs($this->finance)->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
expect(DB::table('purchase_awards')->count())->toBe(0)->and(DB::table('purchase_orders')->count())->toBe(0);
});
test('quotation tasks retain comparison impediments without asking for replacement data', function () {
$this->r->update(['comparison_policy' => [...ComparisonPolicy::DEFAULTS, 'revision' => 1, 'configured' => true, 'minimum_proposals' => 1]]);
$quote = quotationTaskRegister($this);
$quote->update(['valid_until' => today()->subDay()]);
$before = [DB::table('quotations')->get()->toArray(), DB::table('proposal_evaluations')->count(), DB::table('audit_logs')->count()];
$this->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)
->where('tasks.data.0.task', 'quotation_check')->where('tasks.data.0.href', '/requisicoes/'.$this->r->id.'/cotacoes'));
$this->actingAs($this->finance)->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->get('/requisicoes/'.$this->r->id.'/cotacoes?quotation_id='.$quote->id)->assertInertia(fn (Assert $page) => $page->where('quotations.0.canEvaluate', false));
expect([DB::table('quotations')->get()->toArray(), DB::table('proposal_evaluations')->count(), DB::table('audit_logs')->count()])->toEqual($before);
$this->post('/cotacoes/'.$quote->id.'/avaliar', quotationTaskEvaluation())->assertSessionHasErrors('evaluation');
});
test('quotation tasks respect requester independence department scope and quotation focus ownership', function () {
$quote = quotationTaskRegister($this);
$this->owner->forceFill(['purchasing_role' => PurchasingRole::Finance])->save();
$this->actingAs($this->owner)->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->get('/requisicoes/'.$this->r->id.'/cotacoes')->assertOk()->assertInertia(fn (Assert $page) => $page->where('quotations.0.canEvaluate', false));
$department = Department::query()->create(['tenant_id' => $this->tenant->id, 'name' => 'Outra unidade fictícia']);
$head = User::factory()->create(['tenant_id' => $this->tenant->id, 'department_id' => $department->id, 'purchasing_role' => PurchasingRole::DepartmentHead]);
$this->actingAs($head)->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->get('/requisicoes/'.$this->r->id.'/cotacoes')->assertNotFound();
$other = $this->r->replicate();
$other->number = 'REQ-OUTRA-ORIGEM';
$other->save();
$alien = $quote->replicate();
$alien->requisition_id = $other->id;
$alien->save();
$this->actingAs($this->finance)->getJson('/requisicoes/'.$this->r->id.'/cotacoes?quotation_id='.$alien->id)->assertUnprocessable();
});
test('quotation tasks never keep stale finance or procurement authority during a command', function () {
$quote = quotationTaskRegister($this);
$this->actingAs($this->finance);
DB::table('users')->where('id', $this->finance->id)->update(['purchasing_role' => 'warehouse']);
$this->post('/cotacoes/'.$quote->id.'/avaliar', quotationTaskEvaluation())->assertForbidden();
$this->actingAs($this->buyer);
DB::table('users')->where('id', $this->buyer->id)->update(['purchasing_role' => 'warehouse']);
$this->post('/requisicoes/'.$this->r->id.'/cotacoes', [...$this->quote, 'reference' => 'NOVA-TENTATIVA'])->assertForbidden();
expect(Quotation::query()->count())->toBe(1)->and(DB::table('proposal_evaluations')->count())->toBe(0);
});
test('quotation tasks exclude maintenance from preparation and evaluation', function (string $function) {
quotationTaskRegister($this);
$actor = $function === 'procurement' ? $this->buyer : $this->finance;
supplyGrant($actor, [$function]);
DB::table('organization_memberships')->where('user_id', $actor->id)->update(['maintenance' => true, 'expires_at' => now()->addHour()]);
$this->actingAs($actor)->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->get('/requisicoes/'.$this->r->id.'/cotacoes')->assertInertia(fn (Assert $page) => $page->where('canCreate', false)->where('canEvaluate', false)->where('quotations.0.canEvaluate', false));
})->with(['procurement', 'finance']);
test('quotation tasks keep older market consultations visible through pagination and search', function () {
$this->r->forceFill(['created_at' => now()->subMonths(2)])->save();
for ($index = 1; $index < 30; $index++) {
$request = $this->r->replicate();
$request->number = 'REQ-NEW-'.$index;
$request->created_at = now()->subDay();
$request->save();
}
$this->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 30)->has('tasks.data', 25)->where('tasks.data.0.reference', $this->r->number));
$this->get('/requisicoes/tarefas?module=purchases&page=2')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 30)->has('tasks.data', 5));
$this->get('/requisicoes/tarefas?module=purchases&search=REQ-TEST')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.reference', $this->r->number));
});
test('quotation tasks and forms immediately respect a declared participation conflict', function () {
$quote = quotationTaskRegister($this);
$this->actingAs($this->finance)->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1));
$this->post('/conflitos', ['scope' => 'process_supplier', 'supplier_id' => $this->supplier->id, 'requisition_id' => $this->r->id,
'has_conflict' => true, 'known_since' => today()->toDateString(), 'reason' => 'Relação sintética que impede a participação.'])->assertSessionHasNoErrors();
$this->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->get('/requisicoes/'.$this->r->id.'/cotacoes?quotation_id='.$quote->id)->assertInertia(fn (Assert $page) => $page->where('quotations.0.canEvaluate', false)->where('quotations.0.conflict_blocked', true));
$this->post('/cotacoes/'.$quote->id.'/avaliar', quotationTaskEvaluation())->assertSessionHasErrors('conflict');
expect(DB::table('proposal_evaluations')->count())->toBe(0);
});
test('quotation tasks recheck qualified evaluator authority without replacing the stored evaluation', function () {
$this->r->update(['comparison_policy' => [...ComparisonPolicy::DEFAULTS, 'revision' => 1, 'configured' => true, 'minimum_proposals' => 1, 'minimum_evaluators' => 1]]);
$quote = quotationTaskRegister($this);
$this->actingAs($this->finance)->post('/cotacoes/'.$quote->id.'/avaliar', quotationTaskEvaluation())->assertSessionHasNoErrors();
$history = DB::table('proposal_evaluations')->get()->toArray();
DB::table('users')->where('id', $this->finance->id)->update(['purchasing_role' => 'warehouse']);
$this->actingAs($this->buyer)->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'quotation_evaluate'));
expect(DB::table('proposal_evaluations')->get()->toArray())->toEqual($history);
$this->post('/cotacoes/'.$quote->id.'/avaliar', quotationTaskEvaluation())->assertSessionHasNoErrors();
expect(DB::table('proposal_evaluations')->count())->toBe(2);
$this->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'comparison_review'));
});