Files
compras/tests/Feature/InternalSupplyTraceTest.php
T
2026-11-02 20:27:11 +02:00

130 lines
8.7 KiB
PHP

<?php
use App\Models\Requisition;
use App\Models\SupplyRecord;
use App\Models\User;
use App\Services\AutomaticPurchases;
use App\Services\InternalSupplyTrace;
use App\Services\InventoryReversals;
use App\Services\RequisitionWorkflow;
use App\Services\SupplyPlanning;
use App\Services\WarehouseFulfilment;
use App\Services\WarehouseRequests;
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();
app(SupplyPlanning::class)->save($this->f['receiver'], 'policy', supplyInput([], 'suspend', $this->f['policy']));
supplyGrant($this->f['issuer'], ['warehouse_request_prepare']);
supplyGrant($this->f['receiver'], ['warehouse_request_review']);
supplyGrant($this->f['otherReceiver'], ['warehouse', 'warehouse_request_deliver']);
$this->actingAs($this->f['issuer']);
$policy = app(SupplyPlanning::class)->save($this->f['issuer'], 'policy', supplyInput([...supplyPolicy($this->f), 'purpose' => 'internal', 'agreement_key' => $this->f['agreement']->record_key]));
$this->actingAs($this->f['receiver']);
$policy = app(SupplyPlanning::class)->save($this->f['receiver'], 'policy', supplyInput([], 'approve', $policy));
$this->actingAs($this->f['issuer']);
$rule = app(AutomaticPurchases::class)->save($this->f['issuer'], ['operation_key' => (string) Str::uuid(), 'version' => 0, 'action' => 'save', 'reason' => 'Parametrização sintética para integrar os módulos internos.',
'data' => ['policy_key' => $policy->record_key, 'requester_id' => $this->f['issuer']->id, 'cost_center_id' => $this->f['r']->cost_center_id, 'unit_price' => null, 'tax_rate' => null, 'needed_days' => 5, 'valid_until' => today()->addMonth()->toDateString()]]);
$this->actingAs($this->f['receiver']);
app(AutomaticPurchases::class)->save($this->f['receiver'], ['operation_key' => (string) Str::uuid(), 'record_key' => $rule->record_key, 'version' => $rule->version, 'action' => 'approve', 'reason' => 'Conferência independente da regra no ensaio.', 'data' => []]);
$this->actingAs($this->f['issuer']);
$this->request = app(WarehouseRequests::class)->draft($this->f['issuer'], ['operation_key' => (string) Str::uuid(), 'warehouse_id' => $this->f['warehouse']->id,
'cost_center_id' => $this->f['r']->cost_center_id, 'priority' => 'normal', 'destination' => 'Oficina do ensaio', 'reason' => 'Necessidade interna de manutenção para o ensaio.',
'lines' => [['stock_item_id' => $this->f['stock']->id, 'quantity' => '125.000', 'use' => 'internal']]]);
app(WarehouseFulfilment::class)->command($this->f['issuer'], $this->request->id, ['operation_key' => (string) Str::uuid(), 'version' => 0, 'action' => 'submit', 'partial_allowed' => true, 'reason' => 'Submissão efectiva do pedido para o ensaio.']);
app(AutomaticPurchases::class)->evaluate($this->f['issuer'], $this->f['stock']->id, $this->f['warehouse']->id);
$this->need = app(SupplyPlanning::class)->latest('auto_need')->sole();
$this->purchase = Requisition::query()->findOrFail($this->need->data['requisition_ids'][0]);
});
function internalTrace($test, ?User $actor = null): array
{
$actor ??= $test->f['issuer'];
$test->actingAs($actor);
return app(InternalSupplyTrace::class)->forRequest($actor, $test->request, app(WarehouseFulfilment::class)->progress($test->request));
}
/** A documentary fixture version; not a new business approval or manual-link UI. */
function internalTraceVersion($test, array $data, ?array $snapshot = null): SupplyRecord
{
$old = app(SupplyPlanning::class)->latest('auto_need')->sole();
$copy = $old->replicate();
$copy->fill(['version' => $old->version + 1, 'data' => $data, 'snapshot' => $snapshot ?? $old->snapshot,
'operation_key' => (string) Str::uuid(), 'input_hash' => hash('sha256', (string) Str::uuid()), 'created_at' => now()])->save();
return $copy;
}
test('native modules link warehouse shortage to the canonical purchase and its pending human approval', function () {
$trace = internalTrace($this);
expect($trace['groups'])->toHaveCount(1)->and($trace['groups'][0]['purchases'][0]['id'])->toBe($this->purchase->id)
->and($trace['groups'][0]['purchases'][0]['status'])->toBe('draft')->and($trace['groups'][0]['unreserved_milli'])->toBe(125000);
app(RequisitionWorkflow::class)->submit($this->f['issuer'], $this->purchase);
$trace = internalTrace($this);
expect($trace['groups'][0]['purchases'][0]['status'])->toBe('pending')->and($trace['groups'][0]['purchases'][0]['next_task'])->not->toBeNull()
->and($this->purchase->fresh()->steps->where('decision', 'approved'))->toHaveCount(0);
$this->get('/stock/requisicoes/'.$this->request->id)->assertOk()->assertInertia(fn (Assert $page) => $page->where('supplyTrace.groups.0.purchases.0.id', $this->purchase->id));
$this->get('/requisicoes/'.$this->purchase->id)->assertOk()->assertInertia(fn (Assert $page) => $page->where('automaticSource.origins.0.href', '/stock/requisicoes/'.$this->request->id));
});
test('trace queries reuse documents and do not create purchases decisions movements or reservations', function () {
$tables = ['requisitions', 'approval_steps', 'warehouse_request_events', 'warehouse_reservations', 'stock_movements', 'supply_records', 'audit_logs', 'invoices', 'budget_entries'];
$before = [];
foreach ($tables as $table) {
$before[$table] = DB::table($table)->get()->toArray();
}
internalTrace($this);
internalTrace($this);
foreach ($tables as $table) {
expect(DB::table($table)->get()->toArray())->toEqual($before[$table]);
}
});
test('warehouse responsibilities do not acquire access to other peoples purchase processes or financial values', function () {
$trace = internalTrace($this, $this->f['otherReceiver']);
expect($trace['groups'][0]['purchases'])->toBe([])->and($trace['groups'][0]['restricted'])->toBeTrue()
->and(json_encode($trace))->not->toContain($this->purchase->number)->not->toContain('unit_price')->not->toContain('total_minor');
$this->actingAs($this->f['otherReceiver'])->get('/stock/requisicoes/'.$this->request->id)->assertOk()
->assertInertia(fn (Assert $page) => $page->where('supplyTrace.groups.0.restricted', true)->has('supplyTrace.groups.0.purchases', 0));
});
test('later shared needs do not retrospectively assign a future purchase to the old warehouse request', function () {
$new = internalTraceVersion($this, [...$this->need->data, 'requisition_ids' => [$this->purchase->id, $this->f['r']->id]], [...$this->need->snapshot, 'origins' => []]);
$trace = internalTrace($this);
expect($trace['groups'][0]['current_need_id'])->toBe($new->id)->and($trace['groups'][0]['purchases'])->toHaveCount(1)
->and($trace['groups'][0]['purchases'][0]['id'])->toBe($this->purchase->id);
});
test('documentary receipt projection follows the linked issued order and respects the reversal engine', function () {
// Already issued order/receipt from the fixture, not the new automatic draft.
internalTraceVersion($this, [...$this->need->data, 'requisition_ids' => [$this->f['r']->id]]);
$trace = internalTrace($this);
$row = $trace['groups'][0]['purchases'][0]['orders'][0];
expect($row['id'])->toBe($this->f['order']->id)->and($row['effective_received_milli'])->toBe(120000)
->and($row['receipts'][0]['reversed'])->toBeFalse();
$corrections = Mockery::mock(InventoryReversals::class)->makePartial();
$corrections->shouldReceive('corrections')->with('receipt')->andReturn([$row['receipts'][0]['item_id'] => $this->need]);
app()->instance(InventoryReversals::class, $corrections);
$trace = internalTrace($this);
expect($trace['groups'][0]['purchases'][0]['orders'][0]['effective_received_milli'])->toBe(0)
->and($trace['groups'][0]['purchases'][0]['orders'][0]['receipts'][0]['reversed'])->toBeTrue()
->and((int) DB::table('goods_receipt_items')->sum('accepted_milli'))->toBe(120000);
});
test('unrelated people and companies cannot read or link warehouse requests through a purchase', function () {
$outsider = User::factory()->create(['tenant_id' => $this->f['issuer']->tenant_id, 'department_id' => $this->f['issuer']->department_id]);
supplyGrant($outsider, ['procurement']);
$this->actingAs($outsider);
$origin = app(InternalSupplyTrace::class)->purchaseOrigin($outsider, $this->purchase);
expect($origin['origins'][0]['href'])->toBeNull();
$this->get('/stock/requisicoes/'.$this->request->id)->assertForbidden();
$foreign = receiptFixture();
supplyGrant($foreign['issuer'], ['admin']);
$this->actingAs($foreign['issuer'])->get('/stock/requisicoes/'.$this->request->id)->assertNotFound();
});