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(); });