This commit is contained in:
2026-11-02 08:10:00 +02:00
parent 504b01b244
commit b44378b899
14 changed files with 655 additions and 121 deletions
+53 -45
View File
@@ -1,7 +1,6 @@
<?php
use App\Models\Requisition;
use App\Models\User;
use App\Services\AutomaticPurchases;
use App\Services\RequisitionWorkflow;
use App\Services\StockWorkflow;
@@ -14,76 +13,85 @@ use Inertia\Testing\AssertableInertia as Assert;
require_once __DIR__.'/../Support/SuggestionFixture.php';
beforeEach(function () {
$this->f=suggestionFixture();
$this->f = suggestionFixture();
app(SupplyPlanning::class)->save($this->f['receiver'], 'policy', supplyInput([], 'suspend', $this->f['policy']));
$this->actingAs($this->f['issuer']);
$this->input=['operation_key'=>(string)Str::uuid(),'version'=>0,'action'=>'save','reason'=>'Parametrização autorizada para o ensaio isolado.',
'data'=>['policy_key'=>$this->f['policy']->record_key,'requester_id'=>$this->f['issuer']->id,'cost_center_id'=>$this->f['r']->cost_center_id,'unit_price'=>null,'tax_rate'=>null,'needed_days'=>5,'valid_until'=>today()->addMonth()->toDateString()]];
$policy=app(SupplyPlanning::class)->save($this->f['issuer'],'policy',supplyInput([...supplyPolicy($this->f),'purpose'=>'internal','agreement_key'=>$this->f['agreement']->record_key]));
$this->actingAs($this->f['receiver']);
$this->f['policy']=app(SupplyPlanning::class)->save($this->f['receiver'],'policy',supplyInput([],'approve',$policy));
$this->actingAs($this->f['issuer']);
$this->input = ['operation_key' => (string) Str::uuid(), 'version' => 0, 'action' => 'save', 'reason' => 'Parametrização autorizada para o ensaio isolado.',
'data' => ['policy_key' => $this->f['policy']->record_key, 'requester_id' => $this->f['issuer']->id, 'cost_center_id' => $this->f['r']->cost_center_id, 'unit_price' => null, 'tax_rate' => null, 'needed_days' => 5, 'valid_until' => today()->addMonth()->toDateString()]];
});
function activateAutomaticRule($test) {
function activateAutomaticRule($test)
{
$test->actingAs($test->f['issuer']);
$rule=app(AutomaticPurchases::class)->save($test->f['issuer'],$test->input);
$rule = app(AutomaticPurchases::class)->save($test->f['issuer'], $test->input);
$test->actingAs($test->f['receiver']);
return app(AutomaticPurchases::class)->save($test->f['receiver'],['operation_key'=>(string)Str::uuid(),'record_key'=>$rule->record_key,'version'=>$rule->version,'action'=>'approve','reason'=>'Activação independente da parametrização verificada.','data'=>[]]);
return app(AutomaticPurchases::class)->save($test->f['receiver'], ['operation_key' => (string) Str::uuid(), 'record_key' => $rule->record_key, 'version' => $rule->version, 'action' => 'approve', 'reason' => 'Activação independente da parametrização verificada.', 'data' => []]);
}
function automaticIssue($test,string $quantity) {
function automaticIssue($test, string $quantity)
{
$test->actingAs($test->f['receiver']);
return app(StockWorkflow::class)->record($test->f['receiver'],['kind'=>'issue','reference'=>'TEST-'.Str::uuid(),'idempotency_key'=>(string)Str::uuid(),'responsible_id'=>$test->f['issuer']->id,'warehouse_id'=>$test->f['warehouse']->id,'stock_item_id'=>$test->f['stock']->id,'quantity'=>$quantity,'destination'=>'Manutenção interna','reason'=>'Consumo físico confirmado no ensaio isolado.']);
return app(StockWorkflow::class)->record($test->f['receiver'], ['kind' => 'issue', 'reference' => 'TEST-'.Str::uuid(), 'idempotency_key' => (string) Str::uuid(), 'responsible_id' => $test->f['issuer']->id, 'warehouse_id' => $test->f['warehouse']->id, 'stock_item_id' => $test->f['stock']->id, 'quantity' => $quantity, 'destination' => 'Manutenção interna', 'reason' => 'Consumo físico confirmado no ensaio isolado.']);
}
test('automatic purchase reuses approved policy catalogue agreement and tax while preserving human submission and approvals',function(){
$rule=activateAutomaticRule($this);
test('automatic purchase reuses approved policy catalogue agreement and tax while preserving human submission and approvals', function () {
$rule = activateAutomaticRule($this);
expect($rule->data['unit_price'])->toBe('10.00');
$count=Requisition::query()->count();
automaticIssue($this,'100.000');
app(AutomaticPurchases::class)->evaluate($this->f['receiver'],$this->f['stock']->id,$this->f['warehouse']->id);
$need=app(SupplyPlanning::class)->latest('auto_need')->sole();
$r=Requisition::query()->findOrFail($need->data['purchases'][0]['id']);
expect(Requisition::query()->count())->toBe($count+1)->and($r->status)->toBe('draft')->and($r->requester_id)->toBe($this->f['issuer']->id)->and($r->cost_center_id)->toBe($this->f['r']->cost_center_id)
$count = Requisition::query()->count();
automaticIssue($this, '100.000');
app(AutomaticPurchases::class)->evaluate($this->f['receiver'], $this->f['stock']->id, $this->f['warehouse']->id);
$need = app(SupplyPlanning::class)->latest('auto_need')->sole();
$r = Requisition::query()->findOrFail($need->data['purchases'][0]['id']);
expect(Requisition::query()->count())->toBe($count + 1)->and($r->status)->toBe('draft')->and($r->requester_id)->toBe($this->f['issuer']->id)->and($r->cost_center_id)->toBe($this->f['r']->cost_center_id)
->and($r->items()->sole()->quantity)->toBe('84.000')->and($r->steps()->count())->toBe(0)->and(DB::table('budget_entries')->count())->toBe(0)
->and(DB::table('audit_logs')->where('action','requisition.automatically_created')->value('user_id'))->toBe($this->f['receiver']->id);
app(AutomaticPurchases::class)->evaluate($this->f['receiver'],$this->f['stock']->id,$this->f['warehouse']->id);
expect(Requisition::query()->count())->toBe($count+1)->and(app(SupplyPlanning::class)->latest('auto_need')->sole()->state)->toBe('covered');
->and(DB::table('audit_logs')->where('action', 'requisition.automatically_created')->value('user_id'))->toBe($this->f['receiver']->id);
app(AutomaticPurchases::class)->evaluate($this->f['receiver'], $this->f['stock']->id, $this->f['warehouse']->id);
expect(Requisition::query()->count())->toBe($count + 1)->and(app(SupplyPlanning::class)->latest('auto_need')->sole()->state)->toBe('covered');
$this->actingAs($this->f['issuer']);
app(RequisitionWorkflow::class)->submit($this->f['issuer'],$r);
app(RequisitionWorkflow::class)->submit($this->f['issuer'], $r);
expect($r->fresh()->status)->toBe('pending')->and($r->steps()->first()->role)->toBe('department_head');
});
test('missing configuration records a pending need and never invents a requester or budget',function(){
$before=Requisition::query()->count();
automaticIssue($this,'120.000');
app(AutomaticPurchases::class)->evaluate($this->f['receiver'],$this->f['stock']->id,$this->f['warehouse']->id);
$need=app(SupplyPlanning::class)->latest('auto_need')->sole();
test('missing configuration records a pending need and never invents a requester or budget', function () {
$before = Requisition::query()->count();
automaticIssue($this, '120.000');
app(AutomaticPurchases::class)->evaluate($this->f['receiver'], $this->f['stock']->id, $this->f['warehouse']->id);
$need = app(SupplyPlanning::class)->latest('auto_need')->sole();
expect($need->state)->toBe('pending')->and($need->data['purchases'])->toBe([])->and($need->data['message'])->toContain('Parametrize')->and(Requisition::query()->count())->toBe($before);
});
test('a preparer cannot activate their own rule and stale or changed commands do not create duplicate rules',function(){
supplyGrant($this->f['issuer'],['supply_review']);
$rule=app(AutomaticPurchases::class)->save($this->f['issuer'],$this->input);
expect(app(AutomaticPurchases::class)->save($this->f['issuer'],$this->input)->id)->toBe($rule->id);
$decision=['operation_key'=>(string)Str::uuid(),'record_key'=>$rule->record_key,'version'=>1,'action'=>'approve','reason'=>'Tentativa de activar a própria regra.','data'=>[]];
expect(fn()=>app(AutomaticPurchases::class)->save($this->f['issuer'],$decision))->toThrow(ValidationException::class);
$this->input['data']['needed_days']=9;
expect(fn()=>app(AutomaticPurchases::class)->save($this->f['issuer'],$this->input))->toThrow(ValidationException::class);
test('a preparer cannot activate their own rule and stale or changed commands do not create duplicate rules', function () {
supplyGrant($this->f['issuer'], ['supply_review']);
$rule = app(AutomaticPurchases::class)->save($this->f['issuer'], $this->input);
expect(app(AutomaticPurchases::class)->save($this->f['issuer'], $this->input)->id)->toBe($rule->id);
$decision = ['operation_key' => (string) Str::uuid(), 'record_key' => $rule->record_key, 'version' => 1, 'action' => 'approve', 'reason' => 'Tentativa de activar a própria regra.', 'data' => []];
expect(fn () => app(AutomaticPurchases::class)->save($this->f['issuer'], $decision))->toThrow(ValidationException::class);
$this->input['data']['needed_days'] = 9;
expect(fn () => app(AutomaticPurchases::class)->save($this->f['issuer'], $this->input))->toThrow(ValidationException::class);
$this->actingAs($this->f['receiver']);
$decision['version']=0;
expect(fn()=>app(AutomaticPurchases::class)->save($this->f['receiver'],$decision))->toThrow(ValidationException::class);
$decision['version'] = 0;
expect(fn () => app(AutomaticPurchases::class)->save($this->f['receiver'], $decision))->toThrow(ValidationException::class);
});
test('expired or changed sources keep the need pending without preventing the physical operation',function(){
test('expired or changed sources keep the need pending without preventing the physical operation', function () {
activateAutomaticRule($this);
$before=Requisition::query()->count();
$before = Requisition::query()->count();
$this->travel(32)->days();
automaticIssue($this,'120.000');
app(AutomaticPurchases::class)->evaluate($this->f['receiver'],$this->f['stock']->id,$this->f['warehouse']->id);
automaticIssue($this, '120.000');
app(AutomaticPurchases::class)->evaluate($this->f['receiver'], $this->f['stock']->id, $this->f['warehouse']->id);
expect(app(SupplyPlanning::class)->latest('auto_need')->sole()->state)->toBe('pending')->and(Requisition::query()->count())->toBe($before)
->and(app(StockWorkflow::class)->balance($this->f['stock']->id,$this->f['warehouse']->id)['quantity'])->toBe(0);
->and(app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['quantity'])->toBe(0);
});
test('configuration UI separates preparation review and read and does not generate purchases on GET',function(){
$before=Requisition::query()->count();
$this->get('/stock/reposicao')->assertOk()->assertInertia(fn(Assert $p)=>$p->component('stock/replenishment')->where('canPrepare',true)->where('canReview',false)->has('requesters',1));
$this->actingAs($this->f['receiver'])->get('/stock/reposicao')->assertOk()->assertInertia(fn(Assert $p)=>$p->where('canPrepare',false)->where('canReview',true)->has('requesters',0));
test('configuration UI separates preparation review and read and does not generate purchases on GET', function () {
$before = Requisition::query()->count();
$this->get('/stock/reposicao')->assertOk()->assertInertia(fn (Assert $p) => $p->component('stock/replenishment')->where('canPrepare', true)->where('canReview', false)->has('requesters', 1));
$this->actingAs($this->f['receiver'])->get('/stock/reposicao')->assertOk()->assertInertia(fn (Assert $p) => $p->where('canPrepare', false)->where('canReview', true)->has('requesters',0));
expect(Requisition::query()->count())->toBe($before);
});