130 lines
8.7 KiB
PHP
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();
|
|
});
|