This commit is contained in:
2026-11-03 11:51:44 +02:00
parent badaada043
commit e753f7a0a4
309 changed files with 14537 additions and 529 deletions
@@ -13,8 +13,10 @@ use App\Services\ReceiptWorkflow;
use App\Services\RequisitionWorkflow;
use App\Services\StockWorkflow;
use App\Services\SupplyPlanning;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Inertia\Testing\AssertableInertia as Assert;
require_once __DIR__.'/../Support/ReceiptFixture.php';
require_once __DIR__.'/../Support/SupplyFixture.php';
@@ -189,3 +191,83 @@ test('HTTP menu and commands use the same functional permissions without automat
$this->postJson('/stock/ocorrencias', supplyInput(dispositionData($this)))->assertForbidden();
expect(app(SupplyPlanning::class)->latest('stock_event')->sole()->state)->toBe('executed');
});
test('disposition task connects independent review and physical execution once without financial documents', function () {
$draft = app(InventoryDispositions::class)->save($this->f['receiver'], supplyInput(dispositionData($this)));
$path = '/stock/ocorrencias?record_id='.$draft->id;
supplyGrant($this->f['receiver'], ['inventory_disposition_review']);
supplyGrant($this->f['issuer'], ['warehouse']);
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->get($path)->assertInertia(fn (Assert $page) => $page->where('records.0.canApprove', false));
$this->actingAs($this->f['issuer'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)
->where('tasks.data.0.task', 'disposition_approve')->where('tasks.data.0.href', $path.'#disposition-'.$draft->id)->missing('tasks.data.0.snapshot'));
$before = [SupplyRecord::query()->count(), StockMovement::query()->count(), DB::table('audit_logs')->count(), Invoice::query()->count()];
$this->get($path)->assertHeader('Cache-Control', 'max-age=0, no-store, private')->assertInertia(fn (Assert $page) => $page->has('records', 1)->where('focusChanged', false)->where('records.0.canApprove', true));
expect([SupplyRecord::query()->count(), StockMovement::query()->count(), DB::table('audit_logs')->count(), Invoice::query()->count()])->toBe($before);
$approved = app(InventoryDispositions::class)->save($this->f['issuer'], supplyInput([], 'approve', $draft));
expect(app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['quantity'])->toBe(10000);
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->get($path)->assertInertia(fn (Assert $page) => $page->where('focusChanged', true)->where('records.0.id', $approved->id)->where('records.0.canExecute', false)->where('records.0.canReturn', true));
$this->actingAs($this->f['receiver'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'disposition_execute'));
$command = supplyInput(['event_on' => today()->toDateString(), 'evidence' => 'Saída física confirmada no ensaio descartável.'], 'execute', $approved);
$this->postJson('/stock/ocorrencias', $command)->assertRedirect();
$this->postJson('/stock/ocorrencias', $command)->assertRedirect();
expect(app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['quantity'])->toBe(7000)
->and(Invoice::query()->count())->toBe($before[3])->and(StockMovement::query()->count())->toBe($before[1] + 1);
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
});
test('disposition task keeps changed stock sources pending for return without permitting silent execution', function () {
$approved = approvedDisposition($this, ['kind' => 'write_off']);
$this->f['stock']->update(['name' => 'Identificação revista depois da autorização']);
$this->actingAs($this->f['receiver'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.blocked', true));
$this->postJson('/stock/ocorrencias', supplyInput(['event_on' => today()->toDateString(), 'evidence' => 'A saída não pode ignorar alteração da origem.'], 'execute', $approved))->assertUnprocessable();
expect(app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['quantity'])->toBe(10000);
$this->actingAs($this->f['issuer']);
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'disposition_return')->where('tasks.data.0.blocked', true));
$returned = app(InventoryDispositions::class)->save($this->f['issuer'], supplyInput([], 'return', $approved));
expect($returned->state)->toBe('returned')->and($returned->snapshot['source'])->toBe($approved->snapshot['source']);
$this->actingAs($this->f['receiver'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
});
test('disposition task excludes every preparer while retaining the separately authorised physical executor', function () {
$draft = app(InventoryDispositions::class)->save($this->f['receiver'], supplyInput(dispositionData($this)));
supplyGrant($this->f['issuer'], ['inventory_disposition_prepare']);
$this->actingAs($this->f['issuer']);
$updated = app(InventoryDispositions::class)->save($this->f['issuer'], supplyInput(dispositionData($this), 'save', $draft));
supplyGrant($this->f['receiver'], ['inventory_disposition_review']);
foreach ([$this->f['issuer'], $this->f['receiver']] as $person) {
expect(app(InventoryDispositions::class)->canAct($person, $updated, 'approve'))->toBeFalse();
}
supplyGrant($this->f['otherReceiver'], ['inventory_disposition_review']);
$this->actingAs($this->f['otherReceiver'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1));
$approved = app(InventoryDispositions::class)->save($this->f['otherReceiver'], supplyInput([], 'approve', $updated));
expect(app(InventoryDispositions::class)->canAct($this->f['otherReceiver'], $approved, 'execute'))->toBeFalse()
->and(app(InventoryDispositions::class)->canAct($this->f['receiver'], $approved, 'execute'))->toBeTrue();
});
test('disposition task preserves oldest pending events beyond the initial display cap', function () {
$first = app(InventoryDispositions::class)->save($this->f['receiver'], supplyInput(dispositionData($this)));
for ($index = 1; $index < 105; $index++) {
app(InventoryDispositions::class)->save($this->f['receiver'], supplyInput(dispositionData($this)));
}
$this->actingAs($this->f['issuer'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 105)->has('tasks.data', 25)
->where('tasks.data.0.id', 'disposition:'.$first->id.':approve'));
$this->get('/requisicoes/tarefas?module=warehouse&page=5')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 105)->has('tasks.data', 5));
$this->get('/stock/ocorrencias')->assertInertia(fn (Assert $page) => $page->has('records', 25)->where('recordPagination.total', 105));
$this->get('/stock/ocorrencias?record_id='.$first->id)->assertInertia(fn (Assert $page) => $page->has('records', 1)->where('records.0.id', $first->id)->where('records.0.canApprove', true));
});
test('disposition task preserves company isolation maintenance and expired assignments', function () {
$draft = app(InventoryDispositions::class)->save($this->f['receiver'], supplyInput(dispositionData($this)));
$path = '/stock/ocorrencias?record_id='.$draft->id;
$foreign = receiptFixture();
supplyGrant($foreign['receiver'], ['warehouse']);
$this->actingAs($foreign['receiver'])->getJson($path)->assertUnprocessable();
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
DB::table('organization_memberships')->where('user_id', $this->f['issuer']->id)->update(['maintenance' => true, 'expires_at' => now()->addHour()]);
$this->actingAs($this->f['issuer'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->get($path)->assertForbidden();
expect(app(InventoryDispositions::class)->canAct($this->f['issuer'], $draft, 'approve'))->toBeFalse();
DB::table('organization_memberships')->where('user_id', $this->f['issuer']->id)->update(['expires_at' => now()->subMinute()]);
$this->get('/requisicoes/tarefas?module=warehouse')->assertForbidden();
});