288 lines
22 KiB
PHP
288 lines
22 KiB
PHP
<?php
|
|
|
|
use App\Models\StockMovement;
|
|
use App\Models\StockOperation;
|
|
use App\Models\SupplyRecord;
|
|
use App\Models\User;
|
|
use App\Models\Warehouse;
|
|
use App\Services\InventoryCounts;
|
|
use App\Services\InventoryDispositions;
|
|
use App\Services\InventoryLots;
|
|
use App\Services\InventoryReversals;
|
|
use App\Services\InvoiceWorkflow;
|
|
use App\Services\ReceiptWorkflow;
|
|
use App\Services\RequisitionWorkflow;
|
|
use App\Services\StockWorkflow;
|
|
use App\Services\SupplyCoordination;
|
|
use App\Services\SupplyHistory;
|
|
use App\Services\SupplyPlanning;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
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_reversal_prepare', 'inventory_disposition_prepare', 'inventory_lot_review', 'inventory_lot_release', 'supply_reserve']);
|
|
supplyGrant($this->f['issuer'], ['inventory_reversal_review', 'inventory_disposition_review', 'inventory_lot_prepare', 'inventory_adjust_review']);
|
|
supplyGrant($this->f['otherReceiver'], ['warehouse', 'inventory_lot_release']);
|
|
$this->actingAs($this->f['receiver']);
|
|
$this->receiptPayload = receiptPayload($this->f, '10.000');
|
|
$this->receipt = app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], $this->receiptPayload);
|
|
$this->receiptItem = $this->receipt->items->first();
|
|
});
|
|
function reversalStock($test, string $quantity = '3.000', array $extra = []): StockOperation
|
|
{
|
|
$test->actingAs($test->f['receiver']);
|
|
|
|
return app(StockWorkflow::class)->record($test->f['receiver'], ['kind' => 'issue', 'reference' => 'MOV-'.Str::uuid(), 'idempotency_key' => (string) Str::uuid(),
|
|
'warehouse_id' => $test->f['warehouse']->id, 'stock_item_id' => $test->f['stock']->id, 'responsible_id' => $test->f['receiver']->id,
|
|
'quantity' => $quantity, 'reason' => 'Movimento fictício para conferir correcção rastreável.', ...$extra]);
|
|
}
|
|
function reversalSetup($test, array $extra = []): SupplyRecord
|
|
{
|
|
$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' => [['code' => 'BASE', 'expires_on' => today()->addDays(30)->toDateString()]], ...$extra], 'prepare'));
|
|
$test->actingAs($test->f['receiver']);
|
|
|
|
return app(InventoryLots::class)->save($test->f['receiver'], supplyInput([], 'activate', $draft));
|
|
}
|
|
function reversalContext($test, ?int $warehouse = null): array
|
|
{
|
|
return app(InventoryLots::class)->context($test->f['stock']->id, $warehouse ?? $test->f['warehouse']->id);
|
|
}
|
|
function reversalRelease($test, int $warehouse, array $lot, $actor = null): SupplyRecord
|
|
{
|
|
$actor ??= $test->f['receiver'];
|
|
$test->actingAs($actor);
|
|
|
|
return app(InventoryLots::class)->save($actor, supplyInput(['lot_id' => $lot['id'], 'warehouse_id' => $warehouse, 'state_version' => $lot['state_version']], 'release', SupplyRecord::query()->findOrFail($lot['metadata_id'])));
|
|
}
|
|
function approvedReversal($test, array $source): SupplyRecord
|
|
{
|
|
$test->actingAs($test->f['receiver']);
|
|
$draft = app(InventoryReversals::class)->save($test->f['receiver'], supplyInput(['reference' => 'REV-'.Str::uuid(), ...$source]));
|
|
$test->actingAs($test->f['issuer']);
|
|
|
|
return app(InventoryReversals::class)->save($test->f['issuer'], supplyInput([], 'approve', $draft));
|
|
}
|
|
function executeReversal($test, SupplyRecord $approved, ?array $payload = null): SupplyRecord
|
|
{
|
|
$test->actingAs($test->f['receiver']);
|
|
|
|
return app(InventoryReversals::class)->save($test->f['receiver'], $payload ?? supplyInput(['event_on' => today()->toDateString(), 'evidence' => 'Comprovativo da correcção e existência física no ensaio.'], 'execute', $approved));
|
|
}
|
|
function reverseSourceOperation($test, StockOperation $operation): SupplyRecord
|
|
{
|
|
return executeReversal($test, approvedReversal($test, ['source_kind' => 'operation', 'source_operation_id' => $operation->id]));
|
|
}
|
|
|
|
test('full issue reversal preserves the original and is exactly once even on an execution retry', function () {
|
|
$original = reversalStock($this);
|
|
$approved = approvedReversal($this, ['source_kind' => 'operation', 'source_operation_id' => $original->id]);
|
|
$payload = supplyInput(['event_on' => today()->toDateString(), 'evidence' => 'Comprovativo fictício para testar repetição do estorno.'], 'execute', $approved);
|
|
$executed = executeReversal($this, $approved, $payload);
|
|
expect(executeReversal($this, $approved, $payload)->id)->toBe($executed->id)
|
|
->and(app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['quantity'])->toBe(10000)
|
|
->and(StockMovement::query()->where('stock_operation_id', $original->id)->sole()->quantity_milli)->toBe(-3000)
|
|
->and(StockMovement::query()->count())->toBe(3);
|
|
expect(fn () => approvedReversal($this, ['source_kind' => 'operation', 'source_operation_id' => $original->id]))->toThrow(ValidationException::class);
|
|
});
|
|
|
|
test('a corrected issue no longer satisfies a reservation and the same need can use a new actual issue', function () {
|
|
$reservation = app(SupplyCoordination::class)->save($this->f['receiver'], 'reservation', supplyInput(['stock_item_id' => $this->f['stock']->id, 'warehouse_id' => $this->f['warehouse']->id,
|
|
'quantity_milli' => 3000, 'needed_on' => today()->toDateString(), 'additional' => false, 'reference' => 'NEED-REV']));
|
|
$issue = reversalStock($this);
|
|
$fulfilled = app(SupplyCoordination::class)->save($this->f['receiver'], 'reservation', supplyInput(['stock_operation_id' => $issue->id, 'quantity_milli' => 3000], 'fulfil', $reservation));
|
|
reverseSourceOperation($this, $issue);
|
|
$events = app(SupplyCoordination::class)->events($this->f['stock']->id, $this->f['warehouse']->id, today()->toDateString());
|
|
expect($events['reservations'][0]['quantity'])->toBe(3000)->and($events['fulfilled_needs'])->toBe([])->and($events['consumed_today'])->toBe(0)
|
|
->and($fulfilled->fresh()->state)->toBe('fulfilled')->and($fulfilled->data['applications'])->toHaveCount(1);
|
|
$this->actingAs($this->f['receiver']);
|
|
expect(fn () => app(SupplyCoordination::class)->save($this->f['receiver'], 'reservation', supplyInput(['stock_operation_id' => $issue->id, 'quantity_milli' => 1], 'fulfil', $fulfilled)))->toThrow(ValidationException::class);
|
|
$nextIssue = reversalStock($this);
|
|
$again = app(SupplyCoordination::class)->save($this->f['receiver'], 'reservation', supplyInput(['stock_operation_id' => $nextIssue->id, 'quantity_milli' => 3000], 'fulfil', $fulfilled));
|
|
expect(app(SupplyCoordination::class)->remainingReservation($again))->toBe(0)->and($again->data['applications'])->toHaveCount(2);
|
|
expect($again->snapshot['effective_allocated_milli'])->toBe(3000)->and($again->snapshot['reversed_operations'])->toContain($issue->id);
|
|
});
|
|
|
|
test('a reversal outside a closed history period invalidates its signature and corrects effective consumption', function () {
|
|
$day = today()->toDateString();
|
|
$issue = reversalStock($this);
|
|
$before = app(SupplyHistory::class)->analyse($this->f['stock']->id, $this->f['warehouse']->id, $day, $day);
|
|
$this->travel(2)->days();
|
|
reverseSourceOperation($this, $issue);
|
|
$after = app(SupplyHistory::class)->analyse($this->f['stock']->id, $this->f['warehouse']->id, $day, $day);
|
|
expect($before['total_milli'])->toBe(3000)->and($after['total_milli'])->toBe(0)->and($after['source_hash'])->not->toBe($before['source_hash']);
|
|
});
|
|
|
|
test('receipt reversal restores authorised quantities without deleting acceptance and a new correction receipt is not a duplicate', function () {
|
|
$executed = executeReversal($this, approvedReversal($this, ['source_kind' => 'receipt', 'receipt_item_id' => $this->receiptItem->id]));
|
|
expect(app(ReceiptWorkflow::class)->recordedQuantity($this->f['line']->id, 'received_milli'))->toBe(0)
|
|
->and(app(ReceiptWorkflow::class)->recordedQuantity($this->f['line']->id, 'accepted_milli'))->toBe(0)
|
|
->and($this->receiptItem->fresh()->accepted_milli)->toBe(10000)
|
|
->and(app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['quantity'])->toBe(0);
|
|
$this->actingAs($this->f['receiver']);
|
|
app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], $this->receiptPayload);
|
|
expect(StockMovement::query()->count())->toBe(2);
|
|
app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], receiptPayload($this->f, '10.000'));
|
|
expect(app(ReceiptWorkflow::class)->recordedQuantity($this->f['line']->id, 'accepted_milli'))->toBe(10000)
|
|
->and(app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['quantity'])->toBe(10000)
|
|
->and($executed->snapshot['source']['receipt']['accepted_milli'])->toBe(10000);
|
|
});
|
|
|
|
test('receipt line reversal removes the full documentary quantities but only the accepted physical entry', function () {
|
|
$payload = receiptPayload($this->f, '2.000');
|
|
$payload['items'][0] = [...$payload['items'][0], 'accepted_quantity' => '1.000', 'quality_status' => 'reservations', 'notes' => 'Divergência fictícia da aceitação parcial.'];
|
|
$receipt = app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], $payload);
|
|
executeReversal($this, approvedReversal($this, ['source_kind' => 'receipt', 'receipt_item_id' => $receipt->items->first()->id]));
|
|
expect(app(ReceiptWorkflow::class)->recordedQuantity($this->f['line']->id, 'received_milli'))->toBe(10000)
|
|
->and(app(ReceiptWorkflow::class)->recordedQuantity($this->f['line']->id, 'accepted_milli'))->toBe(10000)
|
|
->and(app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['quantity'])->toBe(10000);
|
|
});
|
|
|
|
test('tracked transfer reversal removes original destination lots and quarantines the restored source identity', function () {
|
|
reversalSetup($this);
|
|
reversalRelease($this, $this->f['warehouse']->id, reversalContext($this)['lots'][0]);
|
|
$destination = Warehouse::query()->create(['tenant_id' => $this->f['tenant']->id, 'created_by' => $this->f['receiver']->id, 'code' => 'DEST', 'name' => 'Destino']);
|
|
reversalSetup($this, ['warehouse_id' => $destination->id, 'lots' => []]);
|
|
$original = reversalStock($this, '2.000', ['kind' => 'transfer', 'destination_warehouse_id' => $destination->id]);
|
|
$lot = reversalContext($this)['lots'][0];
|
|
$reversal = reverseSourceOperation($this, $original);
|
|
expect(reversalContext($this)['physical']['quantity'])->toBe(10000)->and(reversalContext($this)['usable_milli'])->toBe(0)
|
|
->and(reversalContext($this, $destination->id)['physical']['quantity'])->toBe(0)
|
|
->and(reversalContext($this)['reconciled'])->toBeTrue()->and(reversalContext($this, $destination->id)['reconciled'])->toBeTrue()
|
|
->and(StockMovement::query()->where('stock_operation_id', $reversal->data['stock_operation_id'])->count())->toBe(2);
|
|
reversalRelease($this, $this->f['warehouse']->id, reversalContext($this)['lots'][0], $this->f['otherReceiver']);
|
|
expect(reversalContext($this)['usable_milli'])->toBe(10000)->and(reversalContext($this)['lots'][0]['id'])->toBe($lot['id']);
|
|
});
|
|
|
|
test('mixed lot adjustments can be reversed without turning corrections into consumption', function () {
|
|
reversalSetup($this, ['lots' => [['code' => 'A', 'expires_on' => today()->addDays(10)->toDateString(), 'quantity' => '6.000'], ['code' => 'B', 'expires_on' => today()->addDays(20)->toDateString(), 'quantity' => '4.000']]]);
|
|
$lots = reversalContext($this)['lots'];
|
|
$count = reversalStock($this, '10.000', ['kind' => 'count', 'ledger_version' => reversalContext($this)['physical']['version'],
|
|
'lot_version' => app(InventoryCounts::class)->source($this->f['stock']->id, $this->f['warehouse']->id)['hash'], 'lot_counts' => [['lot_id' => $lots[0]['id'], 'quantity' => '5.000'], ['lot_id' => $lots[1]['id'], 'quantity' => '5.000']]]);
|
|
$this->actingAs($this->f['issuer']);
|
|
$adjustment = app(StockWorkflow::class)->record($this->f['issuer'], ['kind' => 'adjustment', 'reference' => 'ADJ', 'idempotency_key' => (string) Str::uuid(), 'warehouse_id' => $count->warehouse_id,
|
|
'stock_item_id' => $count->stock_item_id, 'responsible_id' => $this->f['issuer']->id, 'quantity' => '0.000', 'count_id' => $count->id, 'reason' => 'Conferência independente da contagem mista.']);
|
|
reverseSourceOperation($this, $adjustment);
|
|
expect(array_column(reversalContext($this)['lots'], 'quantity_milli'))->toBe([6000, 4000])->and(reversalContext($this)['physical']['quantity'])->toBe(10000)
|
|
->and(reversalContext($this)['reconciled'])->toBeTrue()->and(StockMovement::query()->where('movement_type', 'issue')->count())->toBe(0);
|
|
});
|
|
|
|
test('restoring an old untracked issue after cutover creates an unidentified held lot without fabricating receipt origin', function () {
|
|
$original = reversalStock($this);
|
|
reversalSetup($this);
|
|
reverseSourceOperation($this, $original);
|
|
$context = reversalContext($this);
|
|
$unknown = collect($context['lots'])->firstWhere('data.code', null);
|
|
expect($context['physical']['quantity'])->toBe(10000)->and($context['usable_milli'])->toBe(0)->and($context['reconciled'])->toBeTrue()
|
|
->and($unknown['quantity_milli'])->toBe(3000)->and($unknown['origin'])->toHaveKey('reversal_key')->and($unknown['origin'])->not->toHaveKey('receipt_item_id');
|
|
});
|
|
|
|
test('receipt reversal cannot silently associate an old entry with a baseline lot', function () {
|
|
reversalSetup($this);
|
|
expect(fn () => approvedReversal($this, ['source_kind' => 'receipt', 'receipt_item_id' => $this->receiptItem->id]))->toThrow(ValidationException::class);
|
|
expect(reversalContext($this)['physical']['quantity'])->toBe(10000);
|
|
});
|
|
|
|
test('a supplier return reversal restores net receipt entitlement but cannot itself be reversed', function () {
|
|
$this->actingAs($this->f['receiver']);
|
|
$draft = app(InventoryDispositions::class)->save($this->f['receiver'], supplyInput(['kind' => 'supplier_return', 'reference' => 'RET', 'stock_item_id' => $this->f['stock']->id,
|
|
'warehouse_id' => $this->f['warehouse']->id, 'receipt_item_id' => $this->receiptItem->id, 'quantity' => '3.000']));
|
|
$this->actingAs($this->f['issuer']);
|
|
$approved = app(InventoryDispositions::class)->save($this->f['issuer'], supplyInput([], 'approve', $draft));
|
|
$this->actingAs($this->f['receiver']);
|
|
$returned = app(InventoryDispositions::class)->save($this->f['receiver'], supplyInput(['event_on' => today()->toDateString(), 'evidence' => 'Guia assinada da devolução fictícia.'], 'execute', $approved));
|
|
expect(app(InventoryDispositions::class)->returnedForReceipt($this->receiptItem->id))->toBe(3000);
|
|
expect(fn () => approvedReversal($this, ['source_kind' => 'receipt', 'receipt_item_id' => $this->receiptItem->id]))->toThrow(ValidationException::class);
|
|
$corrected = reverseSourceOperation($this, StockOperation::query()->findOrFail($returned->data['stock_operation_id']));
|
|
expect(app(InventoryDispositions::class)->returnedForReceipt($this->receiptItem->id))->toBe(0)->and($returned->fresh()->state)->toBe('executed')
|
|
->and(app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['quantity'])->toBe(10000);
|
|
expect(fn () => approvedReversal($this, ['source_kind' => 'operation', 'source_operation_id' => $corrected->data['stock_operation_id']]))->toThrow(ValidationException::class);
|
|
});
|
|
|
|
test('an insufficient original lot or warehouse blocks an approved reversal without compensating from another lot', function () {
|
|
reversalSetup($this);
|
|
$payload = receiptPayload($this->f, '5.000');
|
|
$payload['items'][0]['lots'] = [['code' => 'EXACT', 'expires_on' => today()->addDays(30)->toDateString()]];
|
|
$this->actingAs($this->f['receiver']);
|
|
$receipt = app(ReceiptWorkflow::class)->confirm($this->f['receiver'], $this->f['order'], $payload);
|
|
$lot = collect(reversalContext($this)['lots'])->firstWhere('data.code', 'EXACT');
|
|
reversalRelease($this, $this->f['warehouse']->id, $lot, $this->f['otherReceiver']);
|
|
$approved = approvedReversal($this, ['source_kind' => 'receipt', 'receipt_item_id' => $receipt->items->first()->id]);
|
|
reversalStock($this, '3.000');
|
|
expect(fn () => executeReversal($this, $approved))->toThrow(ValidationException::class);
|
|
expect(reversalContext($this)['physical']['quantity'])->toBe(12000)->and(reversalContext($this)['reconciled'])->toBeTrue()
|
|
->and(app(InventoryReversals::class)->receiptReversed($receipt->items->first()->id))->toBeFalse();
|
|
});
|
|
|
|
test('approval independence and source isolation apply even with accumulated roles', function () {
|
|
$original = reversalStock($this);
|
|
$this->actingAs($this->f['receiver']);
|
|
$draft = app(InventoryReversals::class)->save($this->f['receiver'], supplyInput(['reference' => 'REV', 'source_kind' => 'operation', 'source_operation_id' => $original->id]));
|
|
supplyGrant($this->f['receiver'], ['inventory_reversal_review']);
|
|
expect(fn () => app(InventoryReversals::class)->save($this->f['receiver'], supplyInput([], 'approve', $draft)))->toThrow(ValidationException::class);
|
|
$foreign = receiptFixture();
|
|
$this->actingAs($foreign['receiver']);
|
|
$foreignReceipt = app(ReceiptWorkflow::class)->confirm($foreign['receiver'], $foreign['order'], receiptPayload($foreign, '1.000'));
|
|
$this->actingAs($this->f['receiver'])->postJson('/stock/estornos', supplyInput(['reference' => 'FOREIGN', 'source_kind' => 'receipt', 'receipt_item_id' => $foreignReceipt->items->first()->id]))->assertNotFound();
|
|
});
|
|
|
|
test('a reviewer cannot execute their own decision and duplicate approved plans cannot reverse the original twice', function () {
|
|
$original = reversalStock($this);
|
|
$first = approvedReversal($this, ['source_kind' => 'operation', 'source_operation_id' => $original->id]);
|
|
$second = approvedReversal($this, ['source_kind' => 'operation', 'source_operation_id' => $original->id]);
|
|
supplyGrant($this->f['issuer'], ['warehouse']);
|
|
$this->actingAs($this->f['issuer']);
|
|
expect(fn () => app(InventoryReversals::class)->save($this->f['issuer'], supplyInput(['event_on' => today()->toDateString(), 'evidence' => 'Tentativa de executar a própria conferência.'], 'execute', $first)))->toThrow(ValidationException::class);
|
|
executeReversal($this, $first);
|
|
expect(fn () => executeReversal($this, $second))->toThrow(ValidationException::class);
|
|
expect(app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['quantity'])->toBe(10000);
|
|
});
|
|
|
|
test('a final audit failure rolls back all reversal movements and leaves the original approval intact', function () {
|
|
$original = reversalStock($this);
|
|
$approved = approvedReversal($this, ['source_kind' => 'operation', 'source_operation_id' => $original->id]);
|
|
$before = StockMovement::query()->count();
|
|
$this->mock(RequisitionWorkflow::class)->shouldReceive('audit')->andThrow(new RuntimeException('Audit unavailable'));
|
|
expect(fn () => executeReversal($this, $approved))->toThrow(RuntimeException::class);
|
|
expect(StockMovement::query()->count())->toBe($before)->and($approved->fresh()->state)->toBe('approved')
|
|
->and(app(InventoryReversals::class)->operationReversed($original->id))->toBeFalse();
|
|
});
|
|
|
|
test('HTTP permissions and source views expose the correction without changing original documents', function () {
|
|
$original = reversalStock($this);
|
|
$this->postJson('/stock/estornos', supplyInput(['reference' => 'HTTP-REV', 'source_kind' => 'operation', 'source_operation_id' => $original->id]))->assertRedirect();
|
|
$draft = app(SupplyPlanning::class)->latest('stock_reversal')->sole();
|
|
$this->actingAs($this->f['issuer'])->postJson('/stock/estornos', supplyInput([], 'approve', $draft))->assertRedirect();
|
|
$approved = app(SupplyPlanning::class)->latest('stock_reversal')->sole();
|
|
$this->actingAs($this->f['receiver'])->postJson('/stock/estornos', supplyInput(['event_on' => today()->toDateString(), 'evidence' => 'Comprovativo para o percurso HTTP do estorno.'], 'execute', $approved))->assertRedirect();
|
|
$this->get('/stock/estornos')->assertOk();
|
|
$this->get('/stock/operacoes')->assertOk();
|
|
$this->actingAs($this->f['issuer'])->get('/stock/ocorrencias')->assertOk();
|
|
$admin = User::factory()->create(['tenant_id' => $this->f['tenant']->id]);
|
|
supplyGrant($admin, ['admin']);
|
|
$this->actingAs($admin)->get('/stock/estornos')->assertOk();
|
|
$this->postJson('/stock/estornos', supplyInput(['reference' => 'NO-WRITE', 'source_kind' => 'operation', 'source_operation_id' => $original->id]))->assertForbidden();
|
|
});
|
|
|
|
test('voided receipt matching blocks new invoices while preserving invoice facts already registered', function () {
|
|
$finance = User::factory()->create(['tenant_id' => $this->f['tenant']->id]);
|
|
supplyGrant($finance, ['finance']);
|
|
$this->actingAs($finance);
|
|
$data = ['number' => 'OLD-INVOICE', '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'], $data);
|
|
$snapshot = $invoice->fresh(['items'])->toArray();
|
|
executeReversal($this, approvedReversal($this, ['source_kind' => 'receipt', 'receipt_item_id' => $this->receiptItem->id]));
|
|
expect($invoice->fresh(['items'])->toArray())->toBe($snapshot)->and(app(InventoryReversals::class)->financialNotices())->toHaveCount(1);
|
|
$this->actingAs($finance);
|
|
$data['number'] = 'NEW-INVOICE';
|
|
expect(fn () => app(InvoiceWorkflow::class)->register($finance, $this->f['order'], $data))->toThrow(ValidationException::class);
|
|
$this->get('/facturas')->assertOk();
|
|
$this->actingAs($this->f['receiver'])->get('/recepcoes/'.$this->receipt->id)->assertOk();
|
|
});
|