162 lines
13 KiB
PHP
162 lines
13 KiB
PHP
<?php
|
|
|
|
use App\Models\SupplyRecord;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Services\InventoryCorrectionPage;
|
|
use App\Services\InventoryDispositions;
|
|
use App\Services\InventoryReversals;
|
|
use App\Services\ReceiptWorkflow;
|
|
use App\Services\StockWorkflow;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Inertia\Testing\AssertableInertia as Assert;
|
|
|
|
require_once __DIR__.'/../Support/ReceiptFixture.php';
|
|
require_once __DIR__.'/../Support/SupplyFixture.php';
|
|
|
|
beforeEach(function () {
|
|
$this->f = receiptFixture('100.000');
|
|
supplyGrant($this->f['receiver'], ['warehouse', 'inventory_disposition_prepare', 'inventory_reversal_prepare']);
|
|
supplyGrant($this->f['issuer'], ['inventory_disposition_review', 'inventory_reversal_review']);
|
|
$this->actingAs($this->f['receiver']);
|
|
$this->receipt = app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], receiptPayload($this->f, '10.000'));
|
|
$this->receiptItem = $this->receipt->items->first();
|
|
});
|
|
|
|
function correctionPageDraft(object $test, string $kind): SupplyRecord
|
|
{
|
|
$workflow = $kind === 'stock_event' ? app(InventoryDispositions::class) : app(InventoryReversals::class);
|
|
$data = $kind === 'stock_event' ? ['kind' => 'supplier_return', 'stock_item_id' => $test->f['stock']->id, 'warehouse_id' => $test->f['warehouse']->id,
|
|
'receipt_item_id' => $test->receiptItem->id, 'quantity' => '1.000'] : ['source_kind' => 'receipt', 'receipt_item_id' => $test->receiptItem->id];
|
|
|
|
return $workflow->save($test->f['receiver'], supplyInput(['reference' => 'FILTER-MATCH-ROOT', ...$data]));
|
|
}
|
|
|
|
test('correction pagination reaches old pending processes without changing approvals stock or history', function (string $kind, string $path) {
|
|
$first = correctionPageDraft($this, $kind);
|
|
$beforeRecord = $first->fresh()->getAttributes();
|
|
for ($i = 0; $i < 209; $i++) {
|
|
$copy = $first->replicate();
|
|
$copy->record_key = (string) Str::uuid();
|
|
$copy->operation_key = (string) Str::uuid();
|
|
$copy->data = [...$copy->data, 'reference' => 'ARCHIVE-'.$i];
|
|
$copy->created_at = $first->created_at;
|
|
$copy->save();
|
|
}
|
|
$before = [SupplyRecord::query()->count(), DB::table('audit_logs')->count(), DB::table('stock_movements')->count(), DB::table('budget_entries')->count()];
|
|
$this->actingAs($this->f['issuer'])->get($path)->assertInertia(fn (Assert $page) => $page->where('recordPagination.total', 210)->has('records', 25));
|
|
$this->get($path.'?records_page=9')->assertHeader('Cache-Control', 'max-age=0, no-store, private')->assertInertia(fn (Assert $page) => $page
|
|
->has('records', 10)->where('records.9.id', $first->id)->where('records.9.canApprove', true));
|
|
$this->get($path.'?record_id='.$first->id.'&records_page=9')->assertInertia(fn (Assert $page) => $page->where('recordPagination', null)
|
|
->has('records', 1)->where('records.0.id', $first->id)->has('history.'.$first->record_key, 1));
|
|
expect([SupplyRecord::query()->count(), DB::table('audit_logs')->count(), DB::table('stock_movements')->count(), DB::table('budget_entries')->count()])->toBe($before)
|
|
->and($first->fresh()->getAttributes())->toBe($beforeRecord);
|
|
})->with([['stock_event', '/stock/ocorrencias'], ['stock_reversal', '/stock/estornos']]);
|
|
|
|
test('correction pagination filters only current versions and direct links preserve current independent actions', function (string $kind, string $path) {
|
|
$first = correctionPageDraft($this, $kind);
|
|
$workflow = $kind === 'stock_event' ? app(InventoryDispositions::class) : app(InventoryReversals::class);
|
|
$data = [...$first->data, 'reference' => 'CURRENT-REFERENCE'];
|
|
if ($kind === 'stock_event') {
|
|
$data['quantity'] = '1.000';
|
|
}
|
|
$updated = $workflow->save($this->f['receiver'], supplyInput($data, 'save', $first));
|
|
$this->actingAs($this->f['issuer']);
|
|
$approved = $workflow->save($this->f['issuer'], supplyInput([], 'approve', $updated));
|
|
$before = [$first->fresh()->getAttributes(), $approved->fresh()->getAttributes(), DB::table('audit_logs')->count(), DB::table('stock_movements')->count()];
|
|
$this->get($path.'?search=filter-match-root')->assertInertia(fn (Assert $page) => $page->where('recordPagination.total', 0)->has('records', 0));
|
|
$this->get($path.'?search=current-reference&state=approved')->assertInertia(fn (Assert $page) => $page->where('recordPagination.total', 1)
|
|
->where('records.0.id', $approved->id)->has('history.'.$first->record_key, 3));
|
|
$this->get($path.'?search=current-reference&state=draft')->assertInertia(fn (Assert $page) => $page->where('recordPagination.total', 0));
|
|
$this->actingAs($this->f['receiver'])->get($path.'?record_id='.$first->id.'&search=NO-MATCH&state=executed')->assertInertia(fn (Assert $page) => $page
|
|
->where('focusChanged', true)->where('records.0.id', $approved->id)->where('records.0.canExecute', true)->where('recordPagination', null));
|
|
expect([$first->fresh()->getAttributes(), $approved->fresh()->getAttributes(), DB::table('audit_logs')->count(), DB::table('stock_movements')->count()])->toBe($before);
|
|
})->with([['stock_event', '/stock/ocorrencias'], ['stock_reversal', '/stock/estornos']]);
|
|
|
|
test('correction pagination isolates companies and validates pages states reference and focus type', function (string $kind, string $path) {
|
|
$first = correctionPageDraft($this, $kind);
|
|
foreach (['records_page=0', 'records_page=1000001', 'state=unknown', 'search='.str_repeat('A', 101)] as $query) {
|
|
$this->getJson($path.'?'.$query)->assertUnprocessable();
|
|
}
|
|
$wrongPath = $kind === 'stock_event' ? '/stock/estornos' : '/stock/ocorrencias';
|
|
$this->getJson($wrongPath.'?record_id='.$first->id)->assertUnprocessable();
|
|
$tenant = Tenant::query()->create(['name' => 'Empresa isolada de correcções']);
|
|
$foreign = User::factory()->create(['tenant_id' => $tenant->id]);
|
|
supplyGrant($foreign, ['inventory_disposition_read', 'inventory_reversal_read']);
|
|
$this->actingAs($foreign)->get($path)->assertInertia(fn (Assert $page) => $page->where('recordPagination.total', 0)->has('records', 0));
|
|
$this->getJson($path.'?record_id='.$first->id)->assertUnprocessable();
|
|
})->with([['stock_event', '/stock/ocorrencias'], ['stock_reversal', '/stock/estornos']]);
|
|
|
|
test('correction pagination preserves maintenance and membership expiry boundaries', function (string $kind, string $path) {
|
|
correctionPageDraft($this, $kind);
|
|
DB::table('organization_memberships')->where('user_id', $this->f['receiver']->id)->update(['maintenance' => true, 'expires_at' => now()->addHour()]);
|
|
$this->get($path)->assertForbidden();
|
|
DB::table('organization_memberships')->where('user_id', $this->f['receiver']->id)->update(['expires_at' => now()->subMinute()]);
|
|
$this->get($path)->assertForbidden();
|
|
})->with([['stock_event', '/stock/ocorrencias'], ['stock_reversal', '/stock/estornos']]);
|
|
|
|
test('correction pagination preserves an older selected receipt and excludes it after a native reversal', function () {
|
|
$draft = correctionPageDraft($this, 'stock_event');
|
|
for ($i = 0; $i < 200; $i++) {
|
|
app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], receiptPayload($this->f, '0.001'));
|
|
}
|
|
$before = [$draft->fresh()->getAttributes(), DB::table('stock_movements')->count(), DB::table('audit_logs')->count()];
|
|
expect(app(InventoryCorrectionPage::class)->receipts(collect(), mb_strtolower($this->receipt->number))->sole()->id)->toBe($this->receiptItem->id);
|
|
$this->get('/stock/ocorrencias?record_id='.$draft->id)->assertInertia(fn (Assert $page) => $page->has('receipts', 201)
|
|
->where('receipts', fn ($rows) => collect($rows)->where('id', $this->receiptItem->id)->count() === 1));
|
|
expect([$draft->fresh()->getAttributes(), DB::table('stock_movements')->count(), DB::table('audit_logs')->count()])->toBe($before);
|
|
$workflow = app(InventoryReversals::class);
|
|
$correction = $workflow->save($this->f['receiver'], supplyInput(['reference' => 'CORRECT-OLD-RECEIPT', 'source_kind' => 'receipt', 'receipt_item_id' => $this->receiptItem->id]));
|
|
$this->actingAs($this->f['issuer']);
|
|
$approved = $workflow->save($this->f['issuer'], supplyInput([], 'approve', $correction));
|
|
$this->actingAs($this->f['receiver']);
|
|
$workflow->save($this->f['receiver'], supplyInput(['event_on' => today()->toDateString(), 'evidence' => 'Correcção fictícia para validar a origem antiga.'], 'execute', $approved));
|
|
$this->get('/stock/ocorrencias?record_id='.$draft->id)->assertInertia(fn (Assert $page) => $page->has('receipts', 200)
|
|
->where('receipts', fn ($rows) => collect($rows)->where('id', $this->receiptItem->id)->isEmpty()));
|
|
$this->get('/stock/ocorrencias?source_search='.urlencode($this->receipt->number).'&selected_receipt_id='.$this->receiptItem->id)->assertInertia(fn (Assert $page) => $page->has('receipts', 0));
|
|
expect($draft->fresh()->getAttributes())->toBe($before[0])->and($this->receiptItem->fresh()->accepted_milli)->toBe(10000);
|
|
});
|
|
|
|
test('correction pagination preserves an older selected operation without reintroducing an executed correction', function () {
|
|
$issue = function (string $quantity) {
|
|
return app(StockWorkflow::class)->record($this->f['receiver'], ['kind' => 'issue', 'reference' => 'OLD-ISSUE-'.Str::uuid(), 'idempotency_key' => (string) Str::uuid(),
|
|
'warehouse_id' => $this->f['warehouse']->id, 'stock_item_id' => $this->f['stock']->id, 'responsible_id' => $this->f['receiver']->id,
|
|
'quantity' => $quantity, 'reason' => 'Saída fictícia para testar a escolha de origens antigas.']);
|
|
};
|
|
$old = $issue('1.000');
|
|
$workflow = app(InventoryReversals::class);
|
|
$draft = $workflow->save($this->f['receiver'], supplyInput(['reference' => 'CORRECT-OLD-ISSUE', 'source_kind' => 'operation', 'source_operation_id' => $old->id]));
|
|
for ($i = 0; $i < 200; $i++) {
|
|
$issue('0.001');
|
|
}
|
|
$before = [$draft->fresh()->getAttributes(), $old->fresh()->getAttributes(), DB::table('stock_movements')->count(), DB::table('audit_logs')->count()];
|
|
expect(app(InventoryCorrectionPage::class)->operations(collect(), mb_strtolower($old->reference))->sole()->id)->toBe($old->id);
|
|
$this->get('/stock/estornos?record_id='.$draft->id)->assertInertia(fn (Assert $page) => $page->has('operations', 201)
|
|
->where('operations', fn ($rows) => collect($rows)->where('id', $old->id)->count() === 1));
|
|
expect([$draft->fresh()->getAttributes(), $old->fresh()->getAttributes(), DB::table('stock_movements')->count(), DB::table('audit_logs')->count()])->toBe($before);
|
|
$updated = $workflow->save($this->f['receiver'], supplyInput($draft->data, 'save', $draft));
|
|
$this->actingAs($this->f['issuer']);
|
|
$approved = $workflow->save($this->f['issuer'], supplyInput([], 'approve', $updated));
|
|
$this->actingAs($this->f['receiver']);
|
|
$executed = $workflow->save($this->f['receiver'], supplyInput(['event_on' => today()->toDateString(), 'evidence' => 'Correcção fictícia conferida sobre a saída antiga.'], 'execute', $approved));
|
|
$this->get('/stock/estornos?record_id='.$executed->id)->assertInertia(fn (Assert $page) => $page->has('operations', 200)
|
|
->where('operations', fn ($rows) => collect($rows)->where('id', $old->id)->isEmpty()));
|
|
$this->get('/stock/estornos?source_search='.urlencode($old->reference).'&selected_operation_id='.$old->id)->assertInertia(fn (Assert $page) => $page->has('operations', 0));
|
|
expect($old->fresh()->getAttributes())->toBe($before[1]);
|
|
});
|
|
|
|
test('correction source search preserves eligible selections and validates company and access', function () {
|
|
$page = app(InventoryCorrectionPage::class);
|
|
expect($page->receipts(collect(), 'NO-MATCH-FICTICIO'))->toHaveCount(0);
|
|
$this->get('/stock/ocorrencias?source_search=NO-MATCH-FICTICIO&selected_receipt_id='.$this->receiptItem->id)->assertInertia(fn (Assert $p) => $p->has('receipts', 1)->where('receipts.0.id', $this->receiptItem->id));
|
|
$this->getJson('/stock/estornos?source_search='.str_repeat('A', 101))->assertUnprocessable();
|
|
$this->get('/stock/estornos?source_search='.urlencode("' OR 1=1 --"))->assertInertia(fn (Assert $p) => $p->has('receipts', 0)->has('operations', 0));
|
|
$tenant = Tenant::query()->create(['name' => 'Empresa de pesquisa isolada']);
|
|
$foreign = User::factory()->create(['tenant_id' => $tenant->id]);
|
|
supplyGrant($foreign, ['inventory_disposition_prepare', 'inventory_reversal_prepare']);
|
|
$this->actingAs($foreign)->getJson('/stock/ocorrencias?selected_receipt_id='.$this->receiptItem->id)->assertUnprocessable();
|
|
supplyGrant($this->f['otherReceiver'], ['inventory_disposition_read', 'inventory_reversal_read']);
|
|
$this->actingAs($this->f['otherReceiver'])->get('/stock/estornos?source_search=TEST')->assertInertia(fn (Assert $p) => $p->where('capabilities.prepare', false)->has('receipts', 0)->has('operations', 0));
|
|
});
|