f = receiptFixture(); $this->author = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'department_id' => $this->f['department']->id, 'purchasing_role' => PurchasingRole::Requester]); $this->actingAs($this->f['receiver']); }); function publishRequestArticle(object $test, bool $published = true, int $version = 0): object { $stock = $test->f['stock']->fresh(); $test->actingAs($test->f['receiver'])->postJson('/stock/catalogo/'.$stock->id, ['version' => $version, 'stock_basis' => app(RequisitionCatalogue::class)->basis($stock), 'category' => 'Material de produção', 'purchase_type' => 'consumable', 'published' => $published, 'reason' => 'Publicação fictícia para reutilizar dados.'])->assertRedirect(); return DB::table('requisition_catalogue')->where('stock_item_id', $stock->id)->sole(); } function requestCataloguePayload(object $test, object $entry): array { return ['cost_center_id' => $test->f['r']->cost_center_id, 'priority' => 'normal', 'needed_by' => today()->addWeek()->toDateString(), 'justification' => 'Pedido fictício com dados do catálogo.', 'items' => [['catalogue_entry_id' => $entry->id, 'catalogue_version' => $entry->version, 'quantity' => '2.000', 'unit_price' => '10.00', 'tax_rate' => '16']]]; } test('requesters see only published article facts without warehouse balances and the server owns copied descriptors', function () { $this->actingAs($this->author)->get('/requisicoes/criar')->assertInertia(fn (Assert $p) => $p->where('catalogue.items', [])); $entry = publishRequestArticle($this); $this->actingAs($this->author)->get('/requisicoes/criar')->assertInertia(fn (Assert $p) => $p->has('catalogue.items', 1) ->where('catalogue.items.0.stock_item_id', $this->f['stock']->id)->missing('catalogue.items.0.balance')->missing('catalogue.items.0.created_by')); $payload = requestCataloguePayload($this, $entry); $payload['items'][0] += ['description' => 'FORGED', 'unit' => 'kg', 'category' => 'FORGED', 'purchase_type' => 'other']; $this->postJson('/requisicoes', $payload)->assertRedirect(); $line = Requisition::query()->where('requester_id', $this->author->id)->sole()->items()->sole(); expect($line->description)->toBe($this->f['stock']->name)->and($line->unit)->toBe('cx')->and($line->category)->toBe('Material de produção') ->and($line->catalogue_snapshot['version'])->toBe(1)->and($line->total_minor)->toBe(2320); $this->get('/stock/catalogo')->assertForbidden(); }); test('a saved draft preserves its source when publication changes but new uses require the current version', function () { $entry = publishRequestArticle($this); $this->actingAs($this->author)->postJson('/requisicoes', requestCataloguePayload($this, $entry))->assertRedirect(); $request = Requisition::query()->where('requester_id', $this->author->id)->sole(); $old = $request->items()->sole(); $this->f['stock']->update(['name' => 'Nome corrigido do artigo']); $this->get('/requisicoes/criar')->assertInertia(fn (Assert $p) => $p->where('catalogue.items', [])); publishRequestArticle($this, false, 1); $payload = requestCataloguePayload($this, $entry); $payload['items'][0] += ['source_line_id' => $old->id]; $payload['items'][0]['quantity'] = '3'; $this->actingAs($this->author)->putJson('/requisicoes/'.$request->id, $payload)->assertRedirect(); $saved = $request->items()->sole(); expect($saved->description)->toBe($old->description)->and($saved->catalogue_snapshot)->toBe($old->catalogue_snapshot)->and($saved->quantity)->toBe('3.000'); $this->postJson('/requisicoes', requestCataloguePayload($this, $entry))->assertUnprocessable(); $new = publishRequestArticle($this, true, 2); $this->actingAs($this->author)->postJson('/requisicoes', requestCataloguePayload($this, $new))->assertRedirect(); expect(Requisition::query()->where('requester_id', $this->author->id)->latest('id')->first()->items()->sole()->description)->toBe('Nome corrigido do artigo'); }); test('catalogue selection never reuses another company entry or another requests frozen line', function () { $entry = publishRequestArticle($this); $foreign = receiptFixture(); $this->actingAs($foreign['receiver'])->postJson('/stock/catalogo/'.$foreign['stock']->id, ['version' => 0, 'stock_basis' => app(RequisitionCatalogue::class)->basis($foreign['stock']), 'category' => 'Material', 'purchase_type' => 'consumable', 'published' => true, 'reason' => 'Catálogo de outra empresa fictícia.'])->assertRedirect(); $other = DB::table('requisition_catalogue')->where('tenant_id', $foreign['tenant']->id)->sole(); $this->actingAs($this->author)->postJson('/requisicoes', requestCataloguePayload($this, $other))->assertNotFound(); $payload = requestCataloguePayload($this, $entry); $payload['items'][0]['source_line_id'] = $foreign['line']->requisition_item_id; publishRequestArticle($this, false, 1); $this->actingAs($this->author)->postJson('/requisicoes', $payload)->assertUnprocessable(); }); test('catalogue publication validates version source basis and write authority and rolls back on audit failure', function () { $entry = publishRequestArticle($this); $body = ['version' => 0, 'stock_basis' => app(RequisitionCatalogue::class)->basis($this->f['stock']), 'category' => 'Material', 'purchase_type' => 'consumable', 'published' => false, 'reason' => 'Mudança fictícia para conferência.']; $this->postJson('/stock/catalogo/'.$this->f['stock']->id, $body)->assertUnprocessable(); $this->actingAs($this->author)->postJson('/stock/catalogo/'.$this->f['stock']->id, [...$body, 'version' => 1])->assertForbidden(); $this->actingAs($this->f['receiver']); $this->mock(RequisitionWorkflow::class)->shouldReceive('audit')->once()->andThrow(new RuntimeException('Injected catalogue audit failure')); $this->withoutExceptionHandling(); expect(fn () => $this->postJson('/stock/catalogo/'.$this->f['stock']->id, [...$body, 'version' => 1]))->toThrow(RuntimeException::class); expect(DB::table('requisition_catalogue')->where('id', $entry->id)->value('version'))->toBe(1) ->and((bool) DB::table('requisition_catalogue')->where('id', $entry->id)->value('published'))->toBeTrue(); });