Files
compras/tests/Feature/SupplyPaginationTest.php
T
2026-11-03 11:51:44 +02:00

167 lines
14 KiB
PHP

<?php
use App\Models\StockItem;
use App\Models\SupplyRecord;
use App\Models\Tenant;
use App\Models\User;
use App\Services\SupplyHistory;
use App\Services\SupplyPlanning;
use App\Services\SupplySuggestions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia as Assert;
require_once __DIR__.'/../Support/SuggestionFixture.php';
beforeEach(function () {
$this->f = suggestionFixture();
supplyGrant($this->f['issuer'], ['supply_read', 'supply_history_prepare', 'import_prepare', 'import_read']);
$this->actingAs($this->f['issuer']);
});
test('agreement source search finds old approved sources and preserves selection without reviving suspension', function () {
$old = $this->f['agreement']->replicate();
$old->record_key = (string) Str::uuid();
$old->operation_key = (string) Str::uuid();
$old->created_at = now();
$old->data = [...$old->data, 'reference' => 'ACORDO-ANTIGO-PESQUISA'];
$old->save();
for ($i = 0; $i < 100; $i++) {
$copy = $old->replicate();
$copy->record_key = (string) Str::uuid();
$copy->operation_key = (string) Str::uuid();
$copy->data = [...$copy->data, 'reference' => 'RECENTE-'.$i];
$copy->created_at = now();
$copy->save();
}
$base = '/compras/fornecimento?view=calendar&agreement_stock_item_id='.$this->f['stock']->id.'&agreement_warehouse_id='.$this->f['warehouse']->id;
$before = [SupplyRecord::query()->count(), DB::table('audit_logs')->count(), DB::table('requisitions')->count(), DB::table('stock_movements')->count()];
$this->get('/compras/fornecimento?view=calendar')->assertInertia(fn (Assert $p) => $p->where('agreements', fn ($rows) => collect($rows)->where('record_key', $old->record_key)->isEmpty()));
$this->get($base.'&agreement_search=acordo-antigo')->assertInertia(fn (Assert $p) => $p
->where('agreements', fn ($rows) => collect($rows)->where('record_key', $old->record_key)->count() === 1));
$this->get($base.'&agreement_search=NO-MATCH&selected_agreement_key='.$old->record_key)->assertInertia(fn (Assert $p) => $p
->where('agreements', fn ($rows) => collect($rows)->where('record_key', $old->record_key)->count() === 1));
expect([SupplyRecord::query()->count(), DB::table('audit_logs')->count(), DB::table('requisitions')->count(), DB::table('stock_movements')->count()])->toBe($before);
$policy = app(SupplyPlanning::class)->save($this->f['issuer'], 'policy', supplyInput([...supplyPolicy($this->f), 'agreement_key' => $old->record_key]));
expect($policy->data['agreement_version'])->toBe($old->version)->and($policy->data['lead_days'])->toBe($old->data['lead_days']);
$this->actingAs($this->f['receiver']);
app(SupplyPlanning::class)->save($this->f['receiver'], 'agreement', supplyInput([], 'suspend', $old));
$this->actingAs($this->f['issuer'])->get($base.'&selected_agreement_key='.$old->record_key)->assertInertia(fn (Assert $p) => $p
->where('agreements', fn ($rows) => collect($rows)->where('record_key', $old->record_key)->isEmpty()));
expect($old->fresh()->state)->toBe('approved');
});
test('agreement source search validates company and scope and leaves readers without preparation choices', function () {
$base = '/compras/fornecimento?view=calendar';
$this->getJson($base.'&agreement_search=TEST')->assertUnprocessable();
$this->getJson($base.'&agreement_stock_item_id='.$this->f['stock']->id)->assertUnprocessable();
$scope = '&agreement_stock_item_id='.$this->f['stock']->id.'&agreement_warehouse_id='.$this->f['warehouse']->id;
$this->getJson($base.$scope.'&agreement_search='.str_repeat('A', 101))->assertUnprocessable();
$otherItem = StockItem::query()->create(['tenant_id' => $this->f['tenant']->id, 'created_by' => $this->f['issuer']->id, 'code' => 'OTHER-AGREEMENT', 'name' => 'Outro artigo do ensaio', 'unit' => 'cx']);
$this->get($base.'&agreement_stock_item_id='.$otherItem->id.'&agreement_warehouse_id='.$this->f['warehouse']->id.'&selected_agreement_key='.$this->f['agreement']->record_key)
->assertInertia(fn (Assert $p) => $p->has('agreements', 0));
$foreign = suggestionFixture();
$this->actingAs($this->f['issuer'])->getJson($base.$scope.'&selected_agreement_key='.$foreign['agreement']->record_key)->assertUnprocessable();
$this->get($base.$scope.'&agreement_search='.urlencode("' OR 1=1 --"))->assertInertia(fn (Assert $p) => $p->has('agreements', 1));
supplyGrant($this->f['otherReceiver'], ['supply_read']);
$this->actingAs($this->f['otherReceiver'])->get($base.$scope)->assertInertia(fn (Assert $p) => $p->where('canPrepare', false)->has('agreements', 0));
});
test('supply pagination retains old records beyond one hundred and preserves direct focus without writes', function (string $view) {
$first = match ($view) {
'agreements' => $this->f['agreement'],
'calendar' => $this->f['policy'],
'suggestions' => app(SupplySuggestions::class)->save($this->f['issuer'], supplyInput(['policy_key' => $this->f['policy']->record_key])),
'history' => app(SupplyHistory::class)->save($this->f['issuer'], supplyInput(['stock_item_id' => $this->f['stock']->id,
'warehouse_id' => $this->f['warehouse']->id, 'purpose' => 'internal', 'period_from' => today()->subDays(30)->toDateString(),
'period_until' => today()->subDay()->toDateString(), 'quality' => 'complete', 'pattern' => 'regular'])),
};
for ($i = 0; $i < 104; $i++) {
$copy = $first->replicate();
$copy->record_key = (string) Str::uuid();
$copy->operation_key = (string) Str::uuid();
$copy->state = 'draft';
$copy->version = 1;
$copy->created_at = $first->created_at;
$copy->save();
}
$before = [SupplyRecord::query()->count(), DB::table('audit_logs')->count(), DB::table('stock_movements')->count(), DB::table('requisitions')->count(), $first->fresh()->getAttributes()];
$this->get('/compras/fornecimento?view='.$view)->assertInertia(fn (Assert $page) => $page->where('recordPagination.total', 105)->has('records', 25));
$this->get('/compras/fornecimento?view='.$view.'&records_page=5')->assertHeader('Cache-Control', 'max-age=0, no-store, private')->assertInertia(fn (Assert $page) => $page
->where('recordPagination.current_page', 5)->has('records', 5)->where('records.4.id', $first->id)
->where('recordPagination.prev_page_url', fn ($url) => str_contains($url, 'view='.$view)));
$this->get('/compras/fornecimento?view='.$view.'&record_id='.$first->id.'&records_page=5')->assertInertia(fn (Assert $page) => $page->where('recordPagination', null)
->has('records', 1)->where('records.0.id', $first->id));
expect([SupplyRecord::query()->count(), DB::table('audit_logs')->count(), DB::table('stock_movements')->count(), DB::table('requisitions')->count(), $first->fresh()->getAttributes()])->toBe($before);
})->with(['agreements', 'calendar', 'suggestions', 'history']);
test('supply pagination keeps import milestones in one current record and preserves its historical source', function () {
$this->f = receiptFixture();
supplyGrant($this->f['issuer'], ['procurement', 'import_prepare']);
$this->actingAs($this->f['issuer']);
$service = app(SupplyPlanning::class);
$first = $service->save($this->f['issuer'], 'import', supplyInput(['purchase_order_id' => $this->f['order']->id, 'purpose' => 'internal',
'jurisdiction' => 'Jurisdição sintética', 'origin_country' => 'País A', 'dispatch_country' => 'País B', 'regime' => 'Regime fictício de teste']));
$before = $first->fresh()->getAttributes();
$current = $service->save($this->f['issuer'], 'import', supplyInput(['evidence' => 'Documento fictício de trânsito', 'event_date' => today()->toDateString()], 'transit', $first));
$this->get('/compras/fornecimento?view=imports')->assertInertia(fn (Assert $page) => $page->where('recordPagination.total', 1)->has('records', 1)->where('records.0.id', $current->id));
$this->get('/compras/fornecimento?view=imports&record_id='.$first->id)->assertInertia(fn (Assert $page) => $page->where('recordPagination', null)
->where('focusChanged', true)->where('records.0.id', $current->id)->has('history.'.$first->record_key, 2));
expect($first->fresh()->getAttributes())->toBe($before);
});
test('supply pagination enforces company scope and page validation across the views', function (string $view) {
$this->getJson('/compras/fornecimento?view='.$view.'&records_page=0')->assertUnprocessable();
$this->getJson('/compras/fornecimento?view='.$view.'&records_page=1000001')->assertUnprocessable();
$tenant = Tenant::query()->create(['name' => 'Empresa sintética isolada']);
$foreign = User::factory()->create(['tenant_id' => $tenant->id]);
supplyGrant($foreign, ['supply_read', 'import_read']);
$this->actingAs($foreign)->get('/compras/fornecimento?view='.$view)->assertInertia(fn (Assert $page) => $page->where('recordPagination.total', 0)->has('records', 0));
$this->getJson('/compras/fornecimento?view='.$view.'&record_id='.$this->f['agreement']->id)->assertUnprocessable();
})->with(['agreements', 'calendar', 'suggestions', 'history', 'imports']);
test('supply pagination preserves maintenance and membership expiry boundaries', function () {
DB::table('organization_memberships')->where('user_id', $this->f['issuer']->id)->update(['maintenance' => true, 'expires_at' => now()->addHour()]);
$this->get('/compras/fornecimento?view=agreements')->assertForbidden();
DB::table('organization_memberships')->where('user_id', $this->f['issuer']->id)->update(['expires_at' => now()->subMinute()]);
$this->get('/compras/fornecimento?view=calendar')->assertForbidden();
});
test('supply pagination preserves the approved policy of an older suggestion outside recent choices', function () {
$suggestion = app(SupplySuggestions::class)->save($this->f['issuer'], supplyInput(['policy_key' => $this->f['policy']->record_key]));
for ($i = 0; $i < 100; $i++) {
$item = StockItem::query()->create(['tenant_id' => $this->f['tenant']->id, 'created_by' => $this->f['issuer']->id,
'code' => 'POLICY-CHOICE-'.$i, 'name' => 'Artigo sintético de política '.$i, 'unit' => 'cx']);
$this->actingAs($this->f['issuer']);
$draft = app(SupplyPlanning::class)->save($this->f['issuer'], 'policy', supplyInput([...supplyPolicy($this->f), 'stock_item_id' => $item->id]));
$this->actingAs($this->f['receiver']);
app(SupplyPlanning::class)->save($this->f['receiver'], 'policy', supplyInput([], 'approve', $draft));
}
$before = [$suggestion->fresh()->getAttributes(), DB::table('audit_logs')->count(), DB::table('requisitions')->count()];
$this->actingAs($this->f['issuer'])->get('/compras/fornecimento?view=suggestions&record_id='.$suggestion->id)->assertInertia(fn (Assert $page) => $page
->has('policies', 101)->where('policies', fn ($rows) => collect($rows)->where('key', $this->f['policy']->record_key)->count() === 1)
->where('records.0.data.policy_key', $this->f['policy']->record_key));
expect([$suggestion->fresh()->getAttributes(), DB::table('audit_logs')->count(), DB::table('requisitions')->count()])->toBe($before);
$this->actingAs($this->f['receiver']);
app(SupplyPlanning::class)->save($this->f['receiver'], 'policy', supplyInput([], 'suspend', $this->f['policy']));
$this->actingAs($this->f['issuer'])->get('/compras/fornecimento?view=suggestions&record_id='.$suggestion->id)->assertInertia(fn (Assert $page) => $page
->has('policies', 100)->where('policies', fn ($rows) => collect($rows)->where('key', $this->f['policy']->record_key)->isEmpty())
->where('records.0.data.policy_key', $this->f['policy']->record_key));
expect($suggestion->fresh()->getAttributes())->toBe($before[0]);
});
test('supply pagination import source search preserves selection validates scope and never creates documents', function () {
$before = [SupplyRecord::query()->count(), DB::table('audit_logs')->count(), DB::table('requisitions')->count(), DB::table('stock_movements')->count()];
$number = mb_strtolower($this->f['order']->number);
$this->get('/compras/fornecimento?view=imports&order_search='.urlencode($number))->assertInertia(fn (Assert $page) => $page->has('orders', 1)->where('orders.0.id', $this->f['order']->id));
$this->get('/compras/fornecimento?view=imports&order_search=NO-MATCH-FICTICIO')->assertInertia(fn (Assert $page) => $page->has('orders', 0));
$this->get('/compras/fornecimento?view=imports&order_search=NO-MATCH-FICTICIO&selected_order_id='.$this->f['order']->id)->assertInertia(fn (Assert $page) => $page->has('orders', 1)->where('orders.0.id', $this->f['order']->id));
$this->getJson('/compras/fornecimento?view=imports&order_search='.str_repeat('A', 101))->assertUnprocessable();
$this->get('/compras/fornecimento?view=imports&order_search='.urlencode("' OR 1=1 --"))->assertInertia(fn (Assert $page) => $page->has('orders', 0));
expect([SupplyRecord::query()->count(), DB::table('audit_logs')->count(), DB::table('requisitions')->count(), DB::table('stock_movements')->count()])->toBe($before);
$foreign = receiptFixture();
$this->actingAs($this->f['issuer'])->getJson('/compras/fornecimento?view=imports&selected_order_id='.$foreign['order']->id)->assertUnprocessable();
supplyGrant($this->f['otherReceiver'], ['import_read']);
$this->actingAs($this->f['otherReceiver'])->get('/compras/fornecimento?view=imports&selected_order_id='.$this->f['order']->id)->assertInertia(fn (Assert $page) => $page->where('canPrepare', false)->has('orders', 0));
});