commit13
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -59,6 +59,45 @@ test('internal request is the exit document and partial deliveries consume only
|
||||
$this->get('/stock/requisicoes/'.$this->r->id)->assertOk()->assertInertia(fn (Assert $p) => $p->component('stock/request')->where('progress.state', 'closed')->where('progress.delivered.0', 4000));
|
||||
});
|
||||
|
||||
test('reservations protect stock from unrelated issues and cancellation releases only the undelivered balance', function () {
|
||||
warehouseCommand($this, $this->f['issuer'], 'submit', ['partial_allowed' => true]);
|
||||
warehouseCommand($this, $this->reviewer, 'authorize', ['partial_allowed' => true]);
|
||||
expect(app(WarehouseFulfilment::class)->progress($this->r)['reserved'])->toBe([4000]);
|
||||
$this->actingAs($this->f['receiver']);
|
||||
$issue=['kind'=>'issue','reference'=>'UNRELATED','idempotency_key'=>(string)Str::uuid(),'responsible_id'=>$this->f['issuer']->id,'warehouse_id'=>$this->f['warehouse']->id,'stock_item_id'=>$this->f['stock']->id,'quantity'=>'7.000','destination'=>'Outra oficina','reason'=>'Tentativa de saída que consumiria reserva.'];
|
||||
expect(fn()=>app(StockWorkflow::class)->record($this->f['receiver'],$issue))->toThrow(\Illuminate\Validation\ValidationException::class);
|
||||
expect(app(StockWorkflow::class)->balance($this->f['stock']->id,$this->f['warehouse']->id)['quantity'])->toBe(10000);
|
||||
warehouseCommand($this,$this->f['receiver'],'deliver',['lines'=>[['index'=>0,'quantity'=>'1.500']]]);
|
||||
expect(app(WarehouseFulfilment::class)->progress($this->r)['reserved'])->toBe([2500]);
|
||||
$cancel=warehouseCommand($this,$this->reviewer,'cancel');
|
||||
$this->postJson('/stock/requisicoes/'.$this->r->id.'/eventos',$cancel)->assertRedirect();
|
||||
$p=app(WarehouseFulfilment::class)->progress($this->r);
|
||||
expect($p['state'])->toBe('cancelled')->and($p['reserved'])->toBe([0])->and($p['cancelled'])->toBe([2500])->and($p['remaining'])->toBe([0]);
|
||||
$this->actingAs($this->f['receiver']);
|
||||
app(StockWorkflow::class)->record($this->f['receiver'],$issue);
|
||||
expect(app(StockWorkflow::class)->balance($this->f['stock']->id,$this->f['warehouse']->id)['quantity'])->toBe(1500);
|
||||
});
|
||||
|
||||
test('competing integral authorizations do not overbook and partial reservations can be replenished explicitly', function () {
|
||||
warehouseCommand($this,$this->f['issuer'],'submit');
|
||||
warehouseCommand($this,$this->reviewer,'authorize');
|
||||
$original=$this->r;
|
||||
$this->actingAs($this->f['issuer']);
|
||||
$data=$this->draftData;$data['operation_key']=(string)Str::uuid();$data['lines'][0]['quantity']='8.000';
|
||||
$this->r=app(WarehouseRequests::class)->draft($this->f['issuer'],$data);
|
||||
warehouseCommand($this,$this->f['issuer'],'submit',['partial_allowed'=>true]);
|
||||
$this->actingAs($this->reviewer)->postJson('/stock/requisicoes/'.$this->r->id.'/eventos',['operation_key'=>(string)Str::uuid(),'version'=>1,'action'=>'authorize','reason'=>'Tentativa de reservar mais que o stock livre.'])->assertUnprocessable();
|
||||
expect(app(WarehouseFulfilment::class)->progress($this->r)['state'])->toBe('submitted');
|
||||
warehouseCommand($this,$this->reviewer,'authorize',['partial_allowed'=>true]);
|
||||
expect(app(WarehouseFulfilment::class)->progress($this->r)['reserved'])->toBe([6000]);
|
||||
$second=$this->r;$this->r=$original;
|
||||
warehouseCommand($this,$this->reviewer,'cancel');
|
||||
$this->r=$second;
|
||||
warehouseCommand($this,$this->reviewer,'reserve');
|
||||
expect(app(WarehouseFulfilment::class)->progress($this->r)['reserved'])->toBe([8000]);
|
||||
expect(fn()=>DB::table('warehouse_reservations')->where('warehouse_request_id',$second->id)->update(['quantity_milli'=>0]))->toThrow(\Illuminate\Database\QueryException::class);
|
||||
});
|
||||
|
||||
test('warehouse authorization rejects self decision stale version and unauthorized physical delivery', function () {
|
||||
supplyGrant($this->f['issuer'], ['warehouse_request_review']);
|
||||
warehouseCommand($this, $this->f['issuer'], 'submit');
|
||||
|
||||
Reference in New Issue
Block a user