f = receiptFixture('100.000'); supplyGrant($this->f['receiver'], ['warehouse', 'inventory_disposition_prepare', 'inventory_lot_review']); supplyGrant($this->f['issuer'], ['procurement', 'inventory_disposition_review', 'inventory_lot_prepare']); supplyGrant($this->f['otherReceiver'], ['warehouse']); $this->actingAs($this->f['receiver']); $this->receipt = app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], receiptPayload($this->f, '10.000')); $this->source = $this->receipt->items->first(); }); function dispositionData($test, array $extra = []): array { return ['kind' => 'supplier_return', 'reference' => 'RET-'.Str::uuid(), 'stock_item_id' => $test->f['stock']->id, 'warehouse_id' => $test->f['warehouse']->id, 'receipt_item_id' => $test->source->id, 'quantity' => '3.000', ...$extra]; } function approvedDisposition($test, array $extra = []): SupplyRecord { $test->actingAs($test->f['receiver']); $draft = app(InventoryDispositions::class)->save($test->f['receiver'], supplyInput(dispositionData($test, $extra))); $test->actingAs($test->f['issuer']); return app(InventoryDispositions::class)->save($test->f['issuer'], supplyInput([], 'approve', $draft)); } function executeDisposition($test, SupplyRecord $record, ?array $payload = null): SupplyRecord { $test->actingAs($test->f['receiver']); return app(InventoryDispositions::class)->save($test->f['receiver'], $payload ?? supplyInput(['event_on' => today()->toDateString(), 'evidence' => 'Guia assinada de saída física no ensaio.'], 'execute', $record)); } function trackedReturnSource($test): array { $test->actingAs($test->f['issuer']); $draft = app(InventoryLots::class)->save($test->f['issuer'], supplyInput(['stock_item_id' => $test->f['stock']->id, 'warehouse_id' => $test->f['warehouse']->id, 'requires_expiry' => true, 'minimum_remaining_days' => 0, 'lots' => []], 'prepare')); $test->actingAs($test->f['receiver']); app(InventoryLots::class)->save($test->f['receiver'], supplyInput([], 'activate', $draft)); $payload = receiptPayload($test->f, '5.000'); $payload['items'][0]['lots'] = [['code' => 'RETURNABLE', 'expires_on' => today()->addDay()->toDateString()]]; $receipt = app(ReceiptWorkflow::class)->confirm($test->f['receiver'], $test->f['order'], $payload); $test->source = $receipt->items->first(); return collect(app(InventoryLots::class)->context($test->f['stock']->id, $test->f['warehouse']->id)['lots'])->firstWhere('data.code', 'RETURNABLE'); } test('return reuses accepted receipt and records one physical exit without rewriting acceptance or money', function () { $approved = approvedDisposition($this); expect(StockMovement::query()->count())->toBe(1); $executed = executeDisposition($this, $approved); expect(app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['quantity'])->toBe(7000) ->and($this->source->fresh()->accepted_milli)->toBe(10000)->and(StockOperation::query()->sole()->kind)->toBe('supplier_return') ->and($executed->snapshot['source']['receipt']['supplier_id'])->toBe($this->f['order']->supplier_id) ->and(app(InventoryDispositions::class)->returnedForReceipt($this->source->id))->toBe(3000) ->and(Invoice::query()->count())->toBe(0)->and(BudgetEntry::query()->count())->toBe(0); }); test('tracked return permits removing expired quarantined material with exact source allocation', function () { $lot = trackedReturnSource($this); $approved = approvedDisposition($this, ['lot_id' => $lot['id']]); $this->travel(2)->days(); $executed = executeDisposition($this, $approved); $context = app(InventoryLots::class)->context($this->f['stock']->id, $this->f['warehouse']->id); expect($context['reconciled'])->toBeTrue()->and($context['physical']['quantity'])->toBe(12000)->and($context['usable_milli'])->toBe(0) ->and(collect($context['lots'])->firstWhere('id', $lot['id'])['quantity_milli'])->toBe(2000) ->and(StockMovement::query()->where('stock_operation_id', $executed->data['stock_operation_id'])->sole()->movement_type)->toBe('supplier_return'); }); test('baseline identities cannot pretend to originate from a supplier receipt and foreign sources stay isolated', function () { trackedReturnSource($this); $baseline = collect(app(InventoryLots::class)->context($this->f['stock']->id, $this->f['warehouse']->id)['lots'])->firstWhere('data.code', null); $this->actingAs($this->f['receiver']); expect(fn () => app(InventoryDispositions::class)->save($this->f['receiver'], supplyInput(dispositionData($this, ['lot_id' => $baseline['id']]))))->toThrow(ValidationException::class); $foreign = receiptFixture(); $this->actingAs($this->f['receiver'])->postJson('/stock/ocorrencias', supplyInput(dispositionData($this, ['stock_item_id' => $foreign['stock']->id])))->assertNotFound(); }); test('self approval and execution by the reviewer remain forbidden despite accumulated functions', function () { $this->actingAs($this->f['receiver']); $draft = app(InventoryDispositions::class)->save($this->f['receiver'], supplyInput(dispositionData($this))); supplyGrant($this->f['receiver'], ['inventory_disposition_review']); expect(fn () => app(InventoryDispositions::class)->save($this->f['receiver'], supplyInput([], 'approve', $draft)))->toThrow(ValidationException::class); $this->actingAs($this->f['issuer']); $approved = app(InventoryDispositions::class)->save($this->f['issuer'], supplyInput([], 'approve', $draft)); supplyGrant($this->f['issuer'], ['warehouse']); expect(fn () => app(InventoryDispositions::class)->save($this->f['issuer'], supplyInput(['event_on' => today()->toDateString(), 'evidence' => 'Comprovativo fictício para testar independência.'], 'execute', $approved)))->toThrow(ValidationException::class); expect(StockMovement::query()->count())->toBe(1); }); test('execution retries cannot repeat the exit and cumulative supplier returns respect the original acceptance', function () { $approved = approvedDisposition($this, ['quantity' => '7.000']); $payload = supplyInput(['event_on' => today()->toDateString(), 'evidence' => 'Guia assinada fictícia para testar repetição.'], 'execute', $approved); $executed = executeDisposition($this, $approved, $payload); expect(executeDisposition($this, $approved, $payload)->id)->toBe($executed->id)->and(StockMovement::query()->count())->toBe(2); app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], receiptPayload($this->f, '10.000')); expect(fn () => approvedDisposition($this, ['quantity' => '4.000']))->toThrow(ValidationException::class); $second = executeDisposition($this, approvedDisposition($this, ['quantity' => '3.000'])); expect($second->state)->toBe('executed')->and(app(InventoryDispositions::class)->returnedForReceipt($this->source->id))->toBe(10000); }); test('a later stock shortage blocks an approved disposition without a partial exit', function () { $approved = approvedDisposition($this, ['quantity' => '8.000']); $this->actingAs($this->f['receiver']); app(StockWorkflow::class)->record($this->f['receiver'], ['kind' => 'issue', 'reference' => 'USE', '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' => '3.000', 'reason' => 'Consumo real posterior à preparação.']); expect(fn () => executeDisposition($this, $approved))->toThrow(ValidationException::class); expect(app(SupplyPlanning::class)->latest('stock_event')->sole()->state)->toBe('approved')->and(app(InventoryDispositions::class)->returnedForReceipt($this->source->id))->toBe(0); }); test('write off is a separate physical event and does not count as consumption or supplier return', function () { $lot = trackedReturnSource($this); $approved = approvedDisposition($this, ['kind' => 'write_off', 'lot_id' => $lot['id'], 'quantity' => '2.000']); executeDisposition($this, $approved); expect(StockOperation::query()->sole()->kind)->toBe('write_off')->and(app(InventoryDispositions::class)->returnedForReceipt($this->source->id))->toBe(0) ->and(StockMovement::query()->where('movement_type', 'issue')->count())->toBe(0); }); test('an audit failure rolls back stock, partitions and disposition execution together', function () { $lot = trackedReturnSource($this); $approved = approvedDisposition($this, ['lot_id' => $lot['id']]); $this->mock(RequisitionWorkflow::class)->shouldReceive('audit')->andThrow(new RuntimeException('Audit unavailable')); expect(fn () => executeDisposition($this, $approved))->toThrow(RuntimeException::class); expect(app(SupplyPlanning::class)->latest('stock_event')->sole()->state)->toBe('approved')->and(StockOperation::query()->count())->toBe(0) ->and(app(InventoryLots::class)->context($this->f['stock']->id, $this->f['warehouse']->id)['reconciled'])->toBeTrue(); }); test('new invoice matching subtracts executed supplier returns without changing existing documents', function () { $finance = User::factory()->create(['tenant_id' => $this->f['tenant']->id]); supplyGrant($finance, ['finance']); $this->actingAs($finance); $payload = ['number' => 'FT-BEFORE', 'issued_on' => today()->toDateString(), 'subtotal' => '1000.00', 'tax' => '0.00', 'total' => '1000.00', 'items' => [['purchase_order_item_id' => $this->f['line']->id, 'quantity' => '10.000', 'unit' => 'cx', 'unit_price' => '100.00', 'tax_rate' => '0']]]; $invoice = app(InvoiceWorkflow::class)->register($finance, $this->f['order'], $payload); executeDisposition($this, approvedDisposition($this)); expect($invoice->fresh()->total_minor)->toBe(100000)->and($invoice->fresh()->status)->toBe('pending'); $this->actingAs($finance); $payload['number'] = 'FT-AFTER'; expect(fn () => app(InvoiceWorkflow::class)->register($finance, $this->f['order'], $payload))->toThrow(ValidationException::class); $payload['subtotal'] = $payload['total'] = '700.00'; $payload['items'][0]['quantity'] = '7.000'; expect(app(InvoiceWorkflow::class)->register($finance, $this->f['order'], $payload)->total_minor)->toBe(70000); }); test('metadata changes after approval and unsupported execution dates require a fresh decision', function () { $lot = trackedReturnSource($this); $approved = approvedDisposition($this, ['lot_id' => $lot['id']]); $this->actingAs($this->f['issuer']); app(InventoryLots::class)->save($this->f['issuer'], supplyInput(['code' => 'CORRECTED', 'expires_on' => today()->addDays(3)->toDateString()], 'identify', SupplyRecord::query()->findOrFail($lot['metadata_id']))); expect(fn () => executeDisposition($this, $approved))->toThrow(ValidationException::class); $this->actingAs($this->f['issuer']); $returned = app(InventoryDispositions::class)->save($this->f['issuer'], supplyInput([], 'return', $approved)); $this->actingAs($this->f['receiver']); $fresh = app(InventoryDispositions::class)->save($this->f['receiver'], supplyInput(dispositionData($this, ['lot_id' => $lot['id'], 'reference' => $approved->data['reference']]), 'save', $returned)); $this->actingAs($this->f['issuer']); $approved = app(InventoryDispositions::class)->save($this->f['issuer'], supplyInput([], 'approve', $fresh)); expect(fn () => executeDisposition($this, $approved, supplyInput(['event_on' => today()->addDay()->toDateString(), 'evidence' => 'Comprovativo fictício com data futura.'], 'execute', $approved)))->toThrow(ValidationException::class); expect(StockOperation::query()->count())->toBe(0); }); test('HTTP menu and commands use the same functional permissions without automatic administration writes', function () { $this->actingAs($this->f['receiver'])->get('/stock/ocorrencias')->assertOk(); $this->postJson('/stock/ocorrencias', supplyInput(dispositionData($this)))->assertRedirect(); $draft = app(SupplyPlanning::class)->latest('stock_event')->sole(); $this->actingAs($this->f['issuer'])->postJson('/stock/ocorrencias', supplyInput([], 'approve', $draft))->assertRedirect(); $approved = app(SupplyPlanning::class)->latest('stock_event')->sole(); $this->actingAs($this->f['otherReceiver'])->postJson('/stock/ocorrencias', supplyInput(['event_on' => today()->toDateString(), 'evidence' => 'Guia assinada para o percurso HTTP real.'], 'execute', $approved))->assertRedirect(); $admin = User::factory()->create(['tenant_id' => $this->f['tenant']->id]); supplyGrant($admin, ['admin']); $this->actingAs($admin)->get('/stock/ocorrencias')->assertOk(); $this->postJson('/stock/ocorrencias', supplyInput(dispositionData($this)))->assertForbidden(); expect(app(SupplyPlanning::class)->latest('stock_event')->sole()->state)->toBe('executed'); });