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)); });