makePartial(); $queue->shouldReceive('interactive')->andReturn(false); app()->instance(ReplenishmentQueue::class, $queue); automaticIssue($test, '120.000'); return DB::table('replenishment_work')->where('tenant_id', $test->f['tenant']->id)->where('stock_item_id', $test->f['stock']->id)->value('id'); } beforeEach(function () { $this->f = suggestionFixture(); app(SupplyPlanning::class)->save($this->f['receiver'], 'policy', supplyInput([], 'suspend', $this->f['policy'])); $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']); $this->f['policy'] = app(SupplyPlanning::class)->save($this->f['receiver'], 'policy', supplyInput([], 'approve', $policy)); $this->actingAs($this->f['issuer']); $this->input = ['operation_key' => (string) Str::uuid(), 'version' => 0, 'action' => 'save', 'reason' => 'Parametrização autorizada para o ensaio isolado.', 'data' => ['policy_key' => $this->f['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()]]; }); function activateAutomaticRule($test) { $test->actingAs($test->f['issuer']); $rule = app(AutomaticPurchases::class)->save($test->f['issuer'], $test->input); $test->actingAs($test->f['receiver']); return app(AutomaticPurchases::class)->save($test->f['receiver'], ['operation_key' => (string) Str::uuid(), 'record_key' => $rule->record_key, 'version' => $rule->version, 'action' => 'approve', 'reason' => 'Activação independente da parametrização verificada.', 'data' => []]); } function automaticIssue($test, string $quantity, array $extra = []) { $test->actingAs($test->f['receiver']); return app(StockWorkflow::class)->record($test->f['receiver'], ['kind' => 'issue', 'reference' => 'TEST-'.Str::uuid(), 'idempotency_key' => (string) Str::uuid(), 'responsible_id' => $test->f['issuer']->id, 'warehouse_id' => $test->f['warehouse']->id, 'stock_item_id' => $test->f['stock']->id, 'quantity' => $quantity, 'destination' => 'Manutenção interna', 'reason' => 'Consumo físico confirmado no ensaio isolado.', ...$extra]); } test('automatic purchase reuses approved policy catalogue agreement and tax while preserving human submission and approvals', function () { $rule = activateAutomaticRule($this); expect($rule->data['unit_price'])->toBe('10.00'); $count = Requisition::query()->count(); automaticIssue($this, '100.000'); app(AutomaticPurchases::class)->evaluate($this->f['receiver'], $this->f['stock']->id, $this->f['warehouse']->id); $need = app(SupplyPlanning::class)->latest('auto_need')->sole(); $r = Requisition::query()->findOrFail($need->data['purchases'][0]['id']); expect(Requisition::query()->count())->toBe($count + 1)->and($r->status)->toBe('draft')->and($r->requester_id)->toBe($this->f['issuer']->id)->and($r->cost_center_id)->toBe($this->f['r']->cost_center_id) ->and($r->items()->sole()->quantity)->toBe('84.000')->and($r->steps()->count())->toBe(0)->and(DB::table('budget_entries')->count())->toBe(0) ->and(DB::table('audit_logs')->where('action', 'requisition.automatically_created')->value('user_id'))->toBe($this->f['receiver']->id); app(AutomaticPurchases::class)->evaluate($this->f['receiver'], $this->f['stock']->id, $this->f['warehouse']->id); expect(Requisition::query()->count())->toBe($count + 1)->and(app(SupplyPlanning::class)->latest('auto_need')->sole()->state)->toBe('covered'); $this->actingAs($this->f['issuer']); app(RequisitionWorkflow::class)->submit($this->f['issuer'], $r); expect($r->fresh()->status)->toBe('pending')->and($r->steps()->first()->role)->toBe('department_head'); }); test('missing configuration records a pending need and never invents a requester or budget', function () { $before = Requisition::query()->count(); automaticIssue($this, '120.000'); app(AutomaticPurchases::class)->evaluate($this->f['receiver'], $this->f['stock']->id, $this->f['warehouse']->id); $need = app(SupplyPlanning::class)->latest('auto_need')->sole(); expect($need->state)->toBe('pending')->and($need->data['purchases'])->toBe([])->and($need->data['message'])->toContain('Parametrize')->and(Requisition::query()->count())->toBe($before); }); test('supply task auto rule approval connects the reviewer to one requester draft while preserving human approvals', function () { $planning = app(SupplyPlanning::class); $this->actingAs($this->f['receiver']); $planning->save($this->f['receiver'], 'policy', supplyInput([], 'suspend', $this->f['policy'])); $this->actingAs($this->f['issuer']); $draft = $planning->save($this->f['issuer'], 'policy', supplyInput([...supplyPolicy($this->f), 'purpose' => 'internal', 'agreement_key' => $this->f['agreement']->record_key, 'safety_milli' => 120000, 'minimum_stock_milli' => 120000, 'target_milli' => 140000, 'maximum_milli' => 160000, 'daily_milli' => 0, 'minimum_milli' => 0, 'pack_milli' => 1000])); $this->actingAs($this->f['receiver']); $policy = $planning->save($this->f['receiver'], 'policy', supplyInput([], 'approve', $draft)); $this->actingAs($this->f['issuer']); $input = $this->input; $input['data']['policy_key'] = $policy->record_key; $rule = app(AutomaticPurchases::class)->save($this->f['issuer'], $input); supplyGrant($this->f['issuer'], ['supply_review']); $this->get('/stock/reposicao?rule_id='.$rule->id)->assertInertia(fn (Assert $page) => $page->where('rules.data.0.canApprove', false)); $this->get('/requisicoes/tarefas?module=supply')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); $this->actingAs($this->f['receiver'])->get('/requisicoes/tarefas?module=supply')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1) ->where('tasks.data.0.href', '/stock/reposicao?rule_id='.$rule->id.'#supply-rule-'.$rule->id)); $this->get('/stock/reposicao?rule_id='.$rule->id)->assertInertia(fn (Assert $page) => $page->where('rules.total', 1)->where('rules.data.0.canApprove', true)); $count = Requisition::query()->count(); $approval = ['operation_key' => (string) Str::uuid(), 'record_key' => $rule->record_key, 'version' => $rule->version, 'action' => 'approve', 'data' => [], 'reason' => 'Conferência independente apenas no ensaio descartável.']; $this->post('/stock/reposicao', $approval)->assertSessionHasNoErrors(); $this->post('/stock/reposicao', $approval)->assertSessionHasNoErrors(); $requisition = Requisition::query()->where('status', 'draft')->sole(); expect(Requisition::query()->count())->toBe($count + 1)->and($requisition->requester_id)->toBe($this->f['issuer']->id) ->and($requisition->items()->sole()->quantity)->toBe('20.000')->and($requisition->steps()->count())->toBe(0)->and(DB::table('budget_entries')->count())->toBe(0); $this->get('/requisicoes/tarefas?module=supply')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); $this->get('/stock/reposicao?rule_id='.$rule->id)->assertInertia(fn (Assert $page) => $page->where('focusChanged', true)->where('rules.data.0.state', 'approved')->where('rules.data.0.canApprove', false)); $this->actingAs($this->f['issuer'])->get('/requisicoes/tarefas?module=purchases')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.task', 'prepare')->where('tasks.data.0.href', '/requisicoes/'.$requisition->id)); expect($requisition->fresh()->status)->toBe('draft'); }); test('supply task auto rule preserves the existing ability of an authorised preparer to suspend an active rule', function () { $approved = activateAutomaticRule($this); supplyGrant($this->f['issuer'], ['supply_review']); $this->actingAs($this->f['issuer'])->get('/stock/reposicao?rule_id='.$approved->id)->assertInertia(fn (Assert $page) => $page->where('rules.data.0.canApprove', false)->where('rules.data.0.canSuspend', true)); expect(app(AutomaticPurchases::class)->canReview($this->f['issuer'], $approved, 'suspend'))->toBeTrue(); $this->post('/stock/reposicao', ['operation_key' => (string) Str::uuid(), 'record_key' => $approved->record_key, 'version' => $approved->version, 'action' => 'suspend', 'data' => [], 'reason' => 'Suspender a regra pelo percurso já existente.'])->assertSessionHasNoErrors(); expect(app(SupplyPlanning::class)->latest('auto_rule')->sole()->state)->toBe('suspended'); }); test('durable signals survive the missing immediate callback and scheduled recovery never impersonates or duplicates drafts', function () { $id = pilotWork($this); $before = Requisition::query()->count(); $row = DB::table('replenishment_work')->find($id); expect($row->generation)->toBeGreaterThan($row->processed_generation); auth()->guard()->logout(); expect(app(ReplenishmentQueue::class)->scheduled($id))->toBeTrue()->and(auth()->check())->toBeFalse(); $r = Requisition::withoutGlobalScopes()->where('tenant_id', $this->f['tenant']->id)->latest('id')->firstOrFail(); expect(Requisition::withoutGlobalScopes()->where('tenant_id', $this->f['tenant']->id)->count())->toBe($before + 1)->and($r->status)->toBe('draft'); $audit = DB::table('audit_logs')->where('requisition_id', $r->id)->where('action', 'requisition.automatically_created')->sole(); expect(json_decode($audit->after, true)['execution']['mode'])->toBe('scheduled_replenishment')->and($audit->ip_address)->toBeNull(); DB::table('replenishment_work')->where('id', $id)->update(['due_at' => now()->subMinute()]); expect(app(ReplenishmentQueue::class)->scheduled($id))->toBeTrue(); expect(Requisition::withoutGlobalScopes()->where('tenant_id', $this->f['tenant']->id)->count())->toBe($before + 1)->and(app(ReplenishmentRun::class)->tenantId())->toBeNull(); }); test('failure after draft creation rolls back the draft and checkpoint before a successful retry', function () { $id = pilotWork($this); $before = Requisition::query()->count(); $checkpoint = DB::table('replenishment_work')->where('id', $id)->value('processed_generation'); auth()->guard()->logout(); $injected = false; DB::listen(function ($query) use (&$injected) { if (! $injected && str_contains($query->sql, 'replenishment_work') && str_contains($query->sql, 'processed_generation') && str_starts_with(strtolower($query->sql), 'update')) { $injected = true; throw new RuntimeException('Injected checkpoint interruption'); } }); expect(app(ReplenishmentQueue::class)->scheduled($id))->toBeFalse(); expect(Requisition::withoutGlobalScopes()->where('tenant_id', $this->f['tenant']->id)->count())->toBe($before); $row = DB::table('replenishment_work')->find($id); expect($row->processed_generation)->toBe($checkpoint)->and($row->failures)->toBe(1); DB::table('replenishment_work')->where('id', $id)->update(['due_at' => now()->subMinute()]); expect(app(ReplenishmentQueue::class)->scheduled($id))->toBeTrue(); expect(Requisition::withoutGlobalScopes()->where('tenant_id', $this->f['tenant']->id)->count())->toBe($before + 1); }); test('periodic discovery rechecks configured minimum and expired rules without stock movements', function () { activateAutomaticRule($this); automaticIssue($this, '100.000'); auth()->guard()->logout(); $queue = app(ReplenishmentQueue::class); $queue->discover(); expect($queue->drain(100)['failed'])->toBe(0); $need = SupplyRecord::withoutGlobalScopes()->where('tenant_id', $this->f['tenant']->id)->where('kind', 'auto_need')->latest('id')->firstOrFail(); expect($need->data['purchases'])->toHaveCount(1); $count = DB::table('stock_movements')->where('tenant_id', $this->f['tenant']->id)->count(); $this->travel(32)->days(); expect($queue->drain(100)['failed'])->toBe(0); $latest = SupplyRecord::withoutGlobalScopes()->where('tenant_id', $this->f['tenant']->id)->where('kind', 'auto_need')->latest('id')->firstOrFail(); expect($latest->state)->toBe('pending')->and($latest->data['message'])->toContain('expirou')->and(DB::table('stock_movements')->where('tenant_id', $this->f['tenant']->id)->count())->toBe($count); }); test('pending replenishment pagination retains old needs and never exposes another tenant', function () { $tenant = $this->f['tenant']->id; for ($i = 0; $i < 105; $i++) { SupplyRecord::query()->create(['tenant_id' => $tenant, 'kind' => 'auto_need', 'record_key' => (string) Str::uuid(), 'version' => 1, 'state' => 'pending', 'stock_item_id' => $this->f['stock']->id, 'warehouse_id' => $this->f['warehouse']->id, 'created_by' => $this->f['issuer']->id, 'data' => ['message' => 'Necessidade pendente de teste.'], 'snapshot' => ['item' => ['id' => $this->f['stock']->id], 'warehouse' => ['id' => $this->f['warehouse']->id], 'origins' => []], 'reason' => 'Registo isolado para conferir paginação.', 'operation_key' => (string) Str::uuid(), 'input_hash' => hash('sha256', (string) $i), 'created_at' => now()->subDays(106 - $i)]); } $first = SupplyRecord::query()->where('kind', 'auto_need')->oldest('id')->value('id'); $foreign = receiptFixture(); SupplyRecord::query()->create(['tenant_id' => $foreign['tenant']->id, 'kind' => 'auto_need', 'record_key' => (string) Str::uuid(), 'version' => 1, 'state' => 'pending', 'stock_item_id' => $foreign['stock']->id, 'warehouse_id' => $foreign['warehouse']->id, 'created_by' => $foreign['issuer']->id, 'data' => ['message' => 'Outra empresa'], 'snapshot' => [], 'reason' => 'Origem de outra empresa no ensaio isolado.', 'operation_key' => (string) Str::uuid(), 'input_hash' => str_repeat('a', 64), 'created_at' => now()->subYear()]); $this->actingAs($this->f['issuer']); $this->get('/stock/reposicao?need_state=pending')->assertOk()->assertInertia(fn (Assert $p) => $p->where('needs.total', 105)->has('needs.data', 20)->where('needs.data.0.id', $first)->where('needs.current_page', 1)); $this->get('/stock/reposicao?need_state=pending&needs_page=6')->assertOk()->assertInertia(fn (Assert $p) => $p->where('needs.total', 105)->has('needs.data', 5)->where('needs.current_page', 6)); }); test('a rolled back physical command also rolls back its durable replenishment signal', function () { $this->actingAs($this->f['receiver']); $before = DB::table('replenishment_work')->where('tenant_id', $this->f['tenant']->id)->value('generation'); expect(fn () => DB::transaction(function () { automaticIssue($this, '1.000'); throw new RuntimeException('Interrupted physical transaction'); }))->toThrow(RuntimeException::class); expect(DB::table('replenishment_work')->where('tenant_id', $this->f['tenant']->id)->value('generation'))->toBe($before) ->and(app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['quantity'])->toBe(120000); }); test('background execution cannot be borrowed by an authenticated session', function () { $id = pilotWork($this); expect(fn () => app(ReplenishmentQueue::class)->scheduled($id))->toThrow(HttpException::class); expect(app(ReplenishmentRun::class)->tenantId())->toBeNull(); }); test('the pending work list exposes interrupted articles but recovery still requires an explicit operational task', function () { $id = pilotWork($this); DB::table('replenishment_work')->where('id', $id)->update(['failures' => 1, 'last_error' => 'Recuperação automática pendente.']); $admin = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'department_id' => $this->f['issuer']->department_id]); supplyGrant($admin, ['admin']); $this->actingAs($admin)->get('/stock/reposicao')->assertOk()->assertInertia(fn (Assert $p) => $p->where('canPrepare', false)->where('canReview', false)->where('workItems.total', 1)->where('workItems.data.0.id', $id)); $this->post('/stock/reposicao/trabalhos/'.$id.'/avaliar')->assertForbidden(); $this->actingAs($this->f['issuer'])->post('/stock/reposicao/trabalhos/'.$id.'/avaliar')->assertRedirect(); }); test('replenishment workers do not process the SGF composition', function () { $id = pilotWork($this); auth()->guard()->logout(); config(['work_context.composition' => 'sgf']); expect(app(ReplenishmentQueue::class)->ready())->toBeFalse(); expect(fn () => app(ReplenishmentQueue::class)->scheduled($id))->toThrow(HttpException::class); expect(app(ReplenishmentRun::class)->tenantId())->toBeNull(); }); test('periodic evaluation detects lot expiry without physical movements and uses the existing draft workflow', function () { supplyGrant($this->f['issuer'], ['inventory_lot_prepare']); supplyGrant($this->f['receiver'], ['inventory_lot_review', 'inventory_lot_release']); $this->actingAs($this->f['issuer']); $setup = app(InventoryLots::class)->save($this->f['issuer'], supplyInput(['stock_item_id' => $this->f['stock']->id, 'warehouse_id' => $this->f['warehouse']->id, 'requires_expiry' => true, 'minimum_remaining_days' => 0, 'lots' => [['code' => 'EXP-PILOT', 'expires_on' => today()->addDays(2)->toDateString()]]], 'prepare')); $this->actingAs($this->f['receiver']); app(InventoryLots::class)->save($this->f['receiver'], supplyInput([], 'activate', $setup)); $lot = app(InventoryLots::class)->context($this->f['stock']->id, $this->f['warehouse']->id)['lots'][0]; app(InventoryLots::class)->save($this->f['receiver'], supplyInput(['lot_id' => $lot['id'], 'warehouse_id' => $this->f['warehouse']->id, 'state_version' => $lot['state_version']], 'release', SupplyRecord::query()->findOrFail($lot['metadata_id']))); activateAutomaticRule($this); $before = Requisition::query()->count(); $moves = DB::table('stock_movements')->count(); auth()->guard()->logout(); $this->travel(3)->days(); $queue = app(ReplenishmentQueue::class); $queue->discover(); expect($queue->drain(100)['failed'])->toBe(0)->and(DB::table('stock_movements')->count())->toBe($moves) ->and(Requisition::withoutGlobalScopes()->where('tenant_id', $this->f['tenant']->id)->count())->toBe($before + 1); }); test('a rejected automatic purchase does not reopen itself or repeat the rejected purchase', function () { activateAutomaticRule($this); automaticIssue($this, '120.000'); app(AutomaticPurchases::class)->evaluate($this->f['receiver'], $this->f['stock']->id, $this->f['warehouse']->id); $need = app(SupplyPlanning::class)->latest('auto_need')->sole(); $r = Requisition::query()->findOrFail($need->data['purchases'][0]['id']); $this->actingAs($this->f['issuer']); app(RequisitionWorkflow::class)->submit($this->f['issuer'], $r); $reviewer = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'department_id' => $this->f['issuer']->department_id]); supplyGrant($reviewer, ['department_head']); $this->actingAs($reviewer); app(RequisitionWorkflow::class)->decide($reviewer, $r->fresh(), 'rejected', 'Compra não autorizada para esta necessidade.'); app(AutomaticPurchases::class)->evaluate($reviewer, $this->f['stock']->id, $this->f['warehouse']->id); $current = app(SupplyPlanning::class)->latest('auto_need')->sole(); expect($r->fresh()->status)->toBe('rejected')->and($current->state)->toBe('pending')->and($current->data['message'])->toContain('rejeitada')->and($current->data['purchases'])->toHaveCount(1); }); test('a preparer cannot activate their own rule and stale or changed commands do not create duplicate rules', function () { supplyGrant($this->f['issuer'], ['supply_review']); $rule = app(AutomaticPurchases::class)->save($this->f['issuer'], $this->input); expect(app(AutomaticPurchases::class)->save($this->f['issuer'], $this->input)->id)->toBe($rule->id); $decision = ['operation_key' => (string) Str::uuid(), 'record_key' => $rule->record_key, 'version' => 1, 'action' => 'approve', 'reason' => 'Tentativa de activar a própria regra.', 'data' => []]; expect(fn () => app(AutomaticPurchases::class)->save($this->f['issuer'], $decision))->toThrow(ValidationException::class); $this->input['data']['needed_days'] = 9; expect(fn () => app(AutomaticPurchases::class)->save($this->f['issuer'], $this->input))->toThrow(ValidationException::class); $this->actingAs($this->f['receiver']); $decision['version'] = 0; expect(fn () => app(AutomaticPurchases::class)->save($this->f['receiver'], $decision))->toThrow(ValidationException::class); }); test('expired or changed sources keep the need pending without preventing the physical operation', function () { activateAutomaticRule($this); $before = Requisition::query()->count(); $this->travel(32)->days(); automaticIssue($this, '120.000'); app(AutomaticPurchases::class)->evaluate($this->f['receiver'], $this->f['stock']->id, $this->f['warehouse']->id); expect(app(SupplyPlanning::class)->latest('auto_need')->sole()->state)->toBe('pending')->and(Requisition::query()->count())->toBe($before) ->and(app(StockWorkflow::class)->balance($this->f['stock']->id, $this->f['warehouse']->id)['quantity'])->toBe(0); }); test('configuration UI separates preparation review and read and does not generate purchases on GET', function () { $before = Requisition::query()->count(); $this->get('/stock/reposicao')->assertOk()->assertInertia(fn (Assert $p) => $p->component('stock/replenishment')->where('canPrepare', true)->where('canReview', false)->has('requesters', 1)); $this->actingAs($this->f['receiver'])->get('/stock/reposicao')->assertOk()->assertInertia(fn (Assert $p) => $p->where('canPrepare', false)->where('canReview', true)->has('requesters', 0)); expect(Requisition::query()->count())->toBe($before); }); test('warehouse shortage carries the original destination and equipment and is not counted twice after reservation', function () { activateAutomaticRule($this); supplyGrant($this->f['issuer'], ['warehouse_request_prepare']); supplyGrant($this->f['receiver'], ['warehouse_request_review']); $this->actingAs($this->f['issuer']); $level = CriticalityLevel::query()->create(['tenant_id' => $this->f['tenant']->id, 'created_by' => $this->f['issuer']->id, 'code' => 'NORMAL', 'name' => 'Normal', 'criteria' => 'Critério do ensaio.', 'is_critical' => false, 'created_at' => now()]); $equipment = Equipment::query()->create(['tenant_id' => $this->f['tenant']->id, 'created_by' => $this->f['issuer']->id, 'department_id' => $this->f['issuer']->department_id, 'criticality_level_id' => $level->id, 'code' => 'EQ-OF', 'description' => 'Equipamento da oficina central', 'area' => 'Oficina', 'status' => 'active']); $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, 'destination' => 'Oficina central, bancada de montagem', 'reason' => 'Componentes para reposição e manutenção interna.', 'priority' => 'normal', 'lines' => [['stock_item_id' => $this->f['stock']->id, 'quantity' => '125.000', 'use' => 'equipment', 'equipment_id' => $equipment->id]]]); app(WarehouseFulfilment::class)->command($this->f['issuer'], $request->id, ['operation_key' => (string) Str::uuid(), 'version' => 0, 'action' => 'submit', 'reason' => 'Submissão do material para utilização interna.', 'partial_allowed' => true]); $this->actingAs($this->f['receiver']); app(WarehouseFulfilment::class)->command($this->f['receiver'], $request->id, ['operation_key' => (string) Str::uuid(), 'version' => 1, 'action' => 'authorize', 'reason' => 'Autorização parcial com saldo em falta.', 'partial_allowed' => true]); app(AutomaticPurchases::class)->evaluate($this->f['receiver'], $this->f['stock']->id, $this->f['warehouse']->id); $need = app(SupplyPlanning::class)->latest('auto_need')->sole(); $r = Requisition::query()->findOrFail($need->data['purchases'][0]['id']); expect($r->items()->sole()->quantity)->toBe('108.000')->and($need->snapshot['origins'][0]['destination'])->toBe('Oficina central, bancada de montagem') ->and($need->snapshot['origins'][0]['reserved_milli'])->toBe(120000)->and($need->snapshot['unreserved_milli'])->toBe(5000); $origin = app(AutomaticPurchases::class)->origin($r); expect($origin['origins'][0]['warehouse_request_id'])->toBe($request->id); expect($origin['origins'][0]['equipment']['code'])->toBe('EQ-OF'); $this->actingAs($this->f['issuer'])->get('/requisicoes/'.$r->id)->assertOk()->assertInertia(fn (Assert $p) => $p->where('automaticSource.origins.0.destination', 'Oficina central, bancada de montagem')); }); test('revoked requester authority or expired work context cannot be borrowed for automatic purchases', function () { activateAutomaticRule($this); DB::table('organization_memberships')->where('user_id', $this->f['issuer']->id)->update(['expires_at' => now()->subMinute()]); $before = Requisition::query()->count(); automaticIssue($this, '120.000'); app(AutomaticPurchases::class)->evaluate($this->f['receiver'], $this->f['stock']->id, $this->f['warehouse']->id); expect(app(SupplyPlanning::class)->latest('auto_need')->sole()->data['message'])->toContain('competência')->and(Requisition::query()->count())->toBe($before); }); test('automatic spare part purchases reuse the registered compatible equipment and reject a missing technical destination', function () { $level = CriticalityLevel::query()->create(['tenant_id' => $this->f['tenant']->id, 'created_by' => $this->f['issuer']->id, 'code' => 'PART', 'name' => 'Normal', 'criteria' => 'Critério do ensaio.', 'is_critical' => false, 'created_at' => now()]); $equipment = Equipment::query()->create(['tenant_id' => $this->f['tenant']->id, 'created_by' => $this->f['issuer']->id, 'department_id' => $this->f['issuer']->department_id, 'criticality_level_id' => $level->id, 'code' => 'EQ-P', 'description' => 'Máquina de teste', 'area' => 'Oficina', 'status' => 'active']); $part = SparePart::query()->create(['tenant_id' => $this->f['tenant']->id, 'created_by' => $this->f['issuer']->id, 'stock_item_id' => $this->f['stock']->id, 'criticality_level_id' => $level->id, 'minimum_milli' => 20000]); EquipmentSparePart::query()->create(['tenant_id' => $this->f['tenant']->id, 'created_by' => $this->f['issuer']->id, 'equipment_id' => $equipment->id, 'spare_part_id' => $part->id, 'created_at' => now()]); $this->actingAs($this->f['receiver']); app(RequisitionCatalogue::class)->save($this->f['receiver'], $this->f['stock']->id, ['version' => 1, 'stock_basis' => app(RequisitionCatalogue::class)->basis($this->f['stock']), 'category' => 'Peças', 'purchase_type' => 'spare_part', 'published' => true, 'reason' => 'Peça publicada para aquisição com destino técnico.']); $this->actingAs($this->f['issuer']); expect(fn () => app(AutomaticPurchases::class)->save($this->f['issuer'], $this->input))->toThrow(ValidationException::class); $this->input['data']['equipment_id'] = $equipment->id; activateAutomaticRule($this); automaticIssue($this, '120.000', ['equipment_id' => $equipment->id]); app(AutomaticPurchases::class)->evaluate($this->f['receiver'], $this->f['stock']->id, $this->f['warehouse']->id); $need = app(SupplyPlanning::class)->latest('auto_need')->sole(); $r = Requisition::query()->findOrFail($need->data['purchases'][0]['id']); expect($r->items()->sole()->equipment_id)->toBe($equipment->id)->and($r->items()->sole()->spare_part_id)->toBe($part->id)->and($r->status)->toBe('draft'); });