This commit is contained in:
2026-11-03 11:51:44 +02:00
parent badaada043
commit e753f7a0a4
309 changed files with 14537 additions and 529 deletions
+81 -1
View File
@@ -16,8 +16,11 @@ use App\Services\StockWorkflow;
use App\Services\SupplyCoordination;
use App\Services\SupplyHistory;
use App\Services\SupplyPlanning;
use App\Services\WorkflowTaskInbox;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Inertia\Testing\AssertableInertia as Assert;
require_once __DIR__.'/../Support/ReceiptFixture.php';
require_once __DIR__.'/../Support/SupplyFixture.php';
@@ -91,12 +94,17 @@ test('full issue reversal preserves the original and is exactly once even on an
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 () {
test('coordination task returns when 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));
$pending = fn () => collect(app(WorkflowTaskInbox::class)->build($this->f['receiver'], 'procurement'))->where('task', 'reservation_followup')->count();
expect($pending())->toBe(0);
reverseSourceOperation($this, $issue);
expect($pending())->toBe(1)->and(app(SupplyCoordination::class)->canManage($this->f['receiver'], $fulfilled, 'fulfil'))->toBeTrue();
$this->get('/compras/fornecimento?view=reservations&issue_record_id='.$fulfilled->id.'&selected_issue_id='.$issue->id)
->assertInertia(fn (Assert $page) => $page->has('operations', 0));
$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);
@@ -106,6 +114,7 @@ test('a corrected issue no longer satisfies a reservation and the same need can
$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);
expect($pending())->toBe(0);
});
test('a reversal outside a closed history period invalidates its signature and corrects effective consumption', function () {
@@ -285,3 +294,74 @@ test('voided receipt matching blocks new invoices while preserving invoice facts
$this->get('/facturas')->assertOk();
$this->actingAs($this->f['receiver'])->get('/recepcoes/'.$this->receipt->id)->assertOk();
});
test('reversal task links independent approval and exactly one physical correction while preserving originals', function (string $kind) {
$operation = $kind === 'operation' ? reversalStock($this) : null;
$source = $kind === 'operation' ? ['source_kind' => 'operation', 'source_operation_id' => $operation->id] : ['source_kind' => 'receipt', 'receipt_item_id' => $this->receiptItem->id];
$original = $operation?->fresh()->getAttributes() ?? $this->receiptItem->fresh()->getAttributes();
$draft = app(InventoryReversals::class)->save($this->f['receiver'], supplyInput(['reference' => 'TASK-'.Str::uuid(), ...$source]));
$path = '/stock/estornos?record_id='.$draft->id;
supplyGrant($this->f['receiver'], ['inventory_reversal_review']);
supplyGrant($this->f['issuer'], ['warehouse']);
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->actingAs($this->f['issuer'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)
->where('tasks.data.0.task', 'reversal_approve')->where('tasks.data.0.href', $path.'#reversal-'.$draft->id)->missing('tasks.data.0.snapshot'));
$before = [SupplyRecord::query()->count(), StockMovement::query()->count(), DB::table('audit_logs')->count(), DB::table('invoices')->count()];
$this->get($path)->assertHeader('Cache-Control', 'max-age=0, no-store, private')->assertInertia(fn (Assert $page) => $page->has('records', 1)->where('records.0.canApprove', true));
expect([SupplyRecord::query()->count(), StockMovement::query()->count(), DB::table('audit_logs')->count(), DB::table('invoices')->count()])->toBe($before);
$approved = app(InventoryReversals::class)->save($this->f['issuer'], supplyInput([], 'approve', $draft));
$this->get($path)->assertInertia(fn (Assert $page) => $page->where('focusChanged', true)->where('records.0.id', $approved->id)->where('records.0.canExecute', false));
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->actingAs($this->f['receiver'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'reversal_execute'));
$command = supplyInput(['event_on' => today()->toDateString(), 'evidence' => 'Correcção integral confirmada no ensaio descartável.'], 'execute', $approved);
$this->postJson('/stock/estornos', $command)->assertRedirect();
$this->postJson('/stock/estornos', $command)->assertRedirect();
expect(app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['quantity'])->toBe($kind === 'operation' ? 10000 : 0)
->and(StockMovement::query()->count())->toBe($before[1] + 1)->and(DB::table('invoices')->count())->toBe($before[3])
->and($operation?->fresh()->getAttributes() ?? $this->receiptItem->fresh()->getAttributes())->toBe($original);
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
})->with(['operation', 'receipt']);
test('reversal task does not withdraw unavailable receipt stock and still permits an authorised return', function () {
$approved = approvedReversal($this, ['source_kind' => 'receipt', 'receipt_item_id' => $this->receiptItem->id]);
reversalStock($this, '1.000');
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.blocked', true));
$this->postJson('/stock/estornos', supplyInput(['event_on' => today()->toDateString(), 'evidence' => 'O consumo posterior deve impedir uma retirada sem saldo.'], 'execute', $approved))->assertUnprocessable();
expect(app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['quantity'])->toBe(9000);
$this->actingAs($this->f['issuer']);
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'reversal_return')->where('tasks.data.0.blocked', true));
$returned = app(InventoryReversals::class)->save($this->f['issuer'], supplyInput([], 'return', $approved));
expect($returned->state)->toBe('returned')->and($returned->snapshot['source'])->toBe($approved->snapshot['source']);
});
test('reversal task preserves older pending sources beyond the hundred row overview', function () {
$operation = reversalStock($this);
$first = app(InventoryReversals::class)->save($this->f['receiver'], supplyInput(['reference' => 'OLD-'.Str::uuid(), 'source_kind' => 'operation', 'source_operation_id' => $operation->id]));
for ($index = 1; $index < 105; $index++) {
app(InventoryReversals::class)->save($this->f['receiver'], supplyInput(['reference' => 'PENDING-'.Str::uuid(), 'source_kind' => 'operation', 'source_operation_id' => $operation->id]));
}
$this->actingAs($this->f['issuer'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 105)->has('tasks.data', 25)
->where('tasks.data.0.id', 'reversal:'.$first->id.':approve'));
$this->get('/requisicoes/tarefas?module=warehouse&page=5')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 105)->has('tasks.data', 5));
$this->get('/stock/estornos')->assertInertia(fn (Assert $page) => $page->has('records', 25)->where('recordPagination.total', 105));
$this->get('/stock/estornos?record_id='.$first->id)->assertInertia(fn (Assert $page) => $page->has('records', 1)->where('records.0.id', $first->id));
});
test('reversal task preserves record kind company maintenance and assignment expiry boundaries', function () {
$operation = reversalStock($this);
$draft = app(InventoryReversals::class)->save($this->f['receiver'], supplyInput(['reference' => 'SCOPE-'.Str::uuid(), 'source_kind' => 'operation', 'source_operation_id' => $operation->id]));
$path = '/stock/estornos?record_id='.$draft->id;
$other = receiptFixture();
supplyGrant($other['receiver'], ['warehouse']);
$this->actingAs($other['receiver'])->getJson($path)->assertUnprocessable();
$this->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->actingAs($this->f['receiver']);
$disposition = app(InventoryDispositions::class)->save($this->f['receiver'], supplyInput(['kind' => 'write_off', 'reference' => 'OTHER-'.Str::uuid(),
'stock_item_id' => $this->f['stock']->id, 'warehouse_id' => $this->f['warehouse']->id, 'quantity' => '1.000']));
$this->getJson('/stock/estornos?record_id='.$disposition->id)->assertUnprocessable();
DB::table('organization_memberships')->where('user_id', $this->f['issuer']->id)->update(['maintenance' => true, 'expires_at' => now()->addHour()]);
$this->actingAs($this->f['issuer'])->get('/requisicoes/tarefas?module=warehouse')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0));
$this->get($path)->assertForbidden();
expect(app(InventoryReversals::class)->canAct($this->f['issuer'], $draft, 'approve'))->toBeFalse();
DB::table('organization_memberships')->where('user_id', $this->f['issuer']->id)->update(['expires_at' => now()->subMinute()]);
$this->get('/requisicoes/tarefas?module=warehouse')->assertForbidden();
});