assertSame('simulator_fiscal_delta_20261021_test', $target->name); $this->assertSame(55441, (int) $target->port); $this->withoutVite(); config(['investor-path.enabled' => true, 'investor-path.private_preparation' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]); } private function source(): string { return 'portfolio:'.InvestmentPortfolioController::visible()->where('content->code', 'COR-02')->value('id'); } public function test_public_complements_link_existing_identities_and_distinguish_unoffered_needs(): void { $source = $this->source(); $options = $this->getJson('/complementos-investimento?'.http_build_query(['origin' => $source]))->assertOk()->json(); $storage = collect($options)->firstWhere('code', 'storage'); $this->assertNotEmpty($storage['opportunities']); $offer = $storage['opportunities'][0]; $record = Opportunity::findOrFail(substr($offer['key'], 12)); $this->assertSame('/oportunidades/'.$record->slug, $offer['href']); $this->get($offer['href'])->assertOk(); $spinning = collect($options)->firstWhere('code', 'spinning'); $this->assertSame([], $spinning['opportunities']); $query = http_build_query(['complement_origin' => $source, 'complement_code' => 'spinning']); $this->get('/investimentos/iniciar?'.$query)->assertRedirect(route('login')); $this->assertStringContainsString('complement_code=spinning', session('url.intended')); $this->getJson('/complementos-investimento?origin=sector:1')->assertUnprocessable(); } public function test_complements_survive_draft_confirmation_and_consumer_snapshot_without_overwriting_title(): void { $actor = User::factory()->create(); $this->actingAs($actor); $origin = $this->source(); $option = collect(app(InvestmentComplements::class)->options($origin))->firstWhere('code', 'storage'); $target = $option['opportunities'][0]['key']; $need = ['origin' => $origin, 'code' => 'spinning', 'target' => null]; $offer = ['origin' => $origin, 'code' => 'storage', 'target' => $target]; $query = http_build_query(['complement_origin' => $origin, 'complement_code' => 'storage', 'complement_target' => $target]); $this->get('/investimentos/iniciar?'.$query)->assertOk()->assertInertia(fn(Assert $p) => $p->where('entryComplements.0.target.key', $target)->where('opportunityChoices.0.opportunity.key', $target)); $id = $this->postJson('/investimentos', ['title' => 'QA título deliberado dos complementos', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $version = $this->getJson('/investimentos/'.$id.'/contexto')->json('case.version'); $body = ['version' => $version, 'selection' => [], 'complements' => [$need, $offer], 'opportunity_choices' => [['source' => $target, 'portfolio_origins' => [substr($origin, 10)]]]]; $this->postJson('/investimentos/'.$id.'/contexto/rascunho', [...$body, 'draft_sequence' => 1])->assertOk(); $this->get('/investimentos/'.$id.'/contexto/editar')->assertOk()->assertInertia(fn(Assert $p) => $p->has('entryComplements', 2)->where('entryComplements.0.kind', 'need')->where('entryComplements.1.target.key', $target)); $plan = $this->postJson('/investimentos/'.$id.'/contexto/previa', $body)->assertOk()->json(); $this->assertSame('QA título deliberado dos complementos', $plan['fields']['project_title']); $control = [...$body, 'preview_hash' => $plan['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true]; $this->postJson('/investimentos/'.$id.'/contexto/confirmar', $control)->assertOk(); $this->postJson('/investimentos/'.$id.'/contexto/confirmar', $control)->assertOk()->assertJsonPath('replayed', true); $this->flushSession(); $this->actingAs($actor); $view = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json(); $this->assertSame($plan['complements'], $view['context']['complements']); $this->assertSame($plan['complements'], json_decode($view['prefill']['simulator']['context_applicability_snapshot'], true)['complements']); $this->get('/investimentos/'.$id)->assertOk()->assertInertia(fn(Assert $p) => $p->has('investment_context.complements', 2)); $this->actingAs(User::factory()->create())->getJson('/investimentos/'.$id.'/contexto')->assertForbidden(); } public function test_offer_cannot_be_forged_or_confirmed_after_withdrawal(): void { $this->actingAs(User::factory()->create()); $id = $this->postJson('/investimentos', ['title' => 'QA complemento', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $origin = $this->source(); $target = collect(app(InvestmentComplements::class)->options($origin))->firstWhere('code', 'storage')['opportunities'][0]['key']; $base = ['version' => $this->getJson('/investimentos/'.$id.'/contexto')->json('case.version'), 'selection' => []]; $this->postJson('/investimentos/'.$id.'/contexto/previa', [...$base, 'complements' => [['origin' => $origin, 'code' => 'spinning', 'target' => $target]]])->assertUnprocessable(); $body = [...$base, 'opportunity_choices' => [['source' => $target]], 'complements' => [['origin' => $origin, 'code' => 'storage', 'target' => $target]]]; $plan = $this->postJson('/investimentos/'.$id.'/contexto/previa', $body)->assertOk()->json(); Opportunity::whereKey(substr($target, 12))->update(['status' => 'draft']); $this->postJson('/investimentos/'.$id.'/contexto/confirmar', [...$body, 'preview_hash' => $plan['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true])->assertUnprocessable(); $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->assertJsonPath('context.revision', 0); } public function test_concrete_training_and_finance_services_are_reused_only_for_their_documented_origin(): void { $actor = User::factory()->create(); $this->actingAs($actor); $origin = $this->source(); foreach (['training' => 'Formação e certificação', 'finance' => 'Financiamento'] as $type => $title) { $service = app(PartnershipExchange::class)->save($actor, ['client_key' => (string) Str::uuid(), 'title' => $title, 'kind' => 'offering', 'partnership_type' => $type, 'payload' => ['summary' => 'QA oferta '.$title, 'consent' => true], 'origins' => [['type' => 'dossier', 'target_id' => substr($origin, 10)]]]); // Published fixture only in this rolled-back isolated QA transaction. DB::table('portal_partnerships')->where('id', $service->id)->update(['status' => 'published']); $code = $type === 'finance' ? 'finance' : 'training'; $option = collect(app(InvestmentComplements::class)->options($origin))->firstWhere('code', $code); $this->assertSame('partnership:'.$service->id, $option['services'][0]['key']); $this->get($option['services'][0]['href'])->assertOk(); $resolved = app(InvestmentComplements::class)->resolve([['origin' => $origin, 'code' => $code, 'target' => 'partnership:'.$service->id]]); $this->assertSame('service', $resolved[0]['kind']); } $other = InvestmentPortfolioController::visible()->where('content->code', 'COR-01')->firstOrFail(); $this->assertSame([], collect(app(InvestmentComplements::class)->options('portfolio:'.$other->id))->flatMap(fn($r) => $r['services'])->all()); } public function test_explicit_complementary_project_relationship_and_geography_control_the_available_fiche(): void { $records = Opportunity::published()->orderBy('id')->take(2)->get(); [$main, $target] = [$records[0], $records[1]]; $main->summary = 'QA investimento principal'; $main->save(); $target->title = 'Regadio QA relacionado'; $target->summary = 'Regadio para a cadeia de valor QA'; $target->save(); $projects = InvestmentPortfolioController::visible()->whereNotNull('project_id')->take(2)->pluck('project_id'); DB::table('opportunity_projects')->whereIn('opportunity_id', [$main->id, $target->id])->delete(); DB::table('opportunity_projects')->insert([['opportunity_id' => $main->id, 'project_id' => $projects[0]], ['opportunity_id' => $target->id, 'project_id' => $projects[1]]]); $link = (string) Str::uuid(); DB::table('complementary_project_links')->insert(['id' => $link, 'anchor_project_id' => $projects[0], 'complementary_project_id' => $projects[1], 'relation_type' => 'enabler', 'data_classification' => 'public']); $service = app(InvestmentComplements::class); $options = $service->options('opportunity:'.$main->id); $this->assertSame('opportunity:'.$target->id, collect($options)->firstWhere('code', 'irrigation')['opportunities'][0]['key']); $sofala = collect(app(\App\Services\InvestmentCorridorContext::class)->provinces('beira'))->firstWhere('source_code', '07')->id; $gaza = app(\App\Services\InvestmentCorridorContext::class)->provinces('limpopo')[0]->id; $target->primary_territory_id = $gaza; $target->save(); $filtered = $service->options('opportunity:'.$main->id, $sofala); $this->assertSame([], collect($filtered)->firstWhere('code', 'irrigation')['opportunities']); $batch = $this->postJson('/complementos-investimento', ['contexts' => [['origin' => 'opportunity:'.$main->id, 'territory_id' => $sofala]]])->assertOk()->json(); $this->assertSame([], collect($batch['items'])->firstWhere('code', 'irrigation')['opportunities']); DB::table('complementary_project_links')->where('id', $link)->update(['data_classification' => 'internal']); $this->assertSame([], $service->options('opportunity:'.$main->id)); } public function test_previous_relation_is_preserved_as_history_when_an_offer_is_withdrawn(): void { $service = app(InvestmentComplements::class); $origin = $this->source(); $target = collect($service->options($origin))->firstWhere('code', 'storage')['opportunities'][0]['key']; $input = [['origin' => $origin, 'code' => 'storage', 'target' => $target]]; $previous = $service->resolve($input); Opportunity::whereKey(substr($target, 12))->update(['status' => 'draft']); $history = $service->resolve($input, $previous); $this->assertSame($previous[0]['target'], $history[0]['target']); $this->assertSame('review_required', $history[0]['availability']); DB::table('investment_dossiers')->where('id', substr($origin, 10))->update(['publication_status' => 'draft']); $this->assertSame($history, $service->resolve($input, $previous)); $this->assertSame(['items' => [], 'issues' => []], $this->postJson('/complementos-investimento', ['contexts' => []])->assertOk()->json()); $this->postJson('/complementos-investimento', ['contexts' => [['origin' => $origin], ['origin' => 'portfolio:'.InvestmentPortfolioController::visible()->where('content->code', 'COR-01')->value('id')]]]) ->assertOk()->assertJsonCount(1, 'issues')->assertJsonPath('issues.0.origin', $origin); } public function test_removing_then_explicitly_selecting_a_need_keeps_the_draft_and_does_not_reapply_an_old_entry(): void { $this->actingAs(User::factory()->create()); $id = $this->postJson('/investimentos', ['title' => 'QA retirada de complemento', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $origin = $this->source(); $selection = (string) Str::uuid(); $entry = app(InvestmentComplements::class)->resolve([['origin' => $origin, 'code' => 'spinning']])[0]; $query = http_build_query(['complement_origin' => $origin, 'complement_code' => 'spinning', 'selection_key' => $selection]); $tag = 'complement:'.hash('sha256', $entry['key'].'@'.$selection); $this->get('/investimentos/'.$id.'/contexto/editar?'.$query)->assertOk()->assertInertia(fn(Assert $p) => $p->where('entrySources.0', $tag)->has('entryComplements', 1)); $this->postJson('/investimentos/'.$id.'/contexto/rascunho', ['version' => $this->getJson('/investimentos/'.$id.'/contexto')->json('case.version'), 'draft_sequence' => 1, 'selection' => [], 'complements' => [], 'entry_sources' => [$tag], 'declarations' => ['project_title' => 'QA edição a conservar']])->assertOk(); $this->get('/investimentos/'.$id.'/contexto/editar?'.$query)->assertOk()->assertInertia(fn(Assert $p) => $p->has('entryComplements', 0)->where('draft.declarations.project_title', 'QA edição a conservar')); $query = http_build_query(['complement_origin' => $origin, 'complement_code' => 'spinning', 'selection_key' => (string) Str::uuid()]); $this->get('/investimentos/'.$id.'/contexto/editar?'.$query)->assertOk()->assertInertia(fn(Assert $p) => $p->has('entryComplements', 1)->where('draft.declarations.project_title', 'QA edição a conservar')); } public function test_public_chain_contract_preserves_both_documentary_references_in_the_selected_relation(): void { $records = Opportunity::published()->orderBy('id')->take(2)->get(); [$main, $target] = [$records[0], $records[1]]; $main->summary = 'QA produção e processamento'; $main->save(); $target->summary = 'QA processamento da mesma cadeia'; $target->save(); $row = ['target_type' => 'chain', 'target_id' => (string) Str::uuid(), 'label' => 'Cadeia sintética QA', 'state' => 'preliminary', 'source_reference' => 'Referência documental sintética QA']; $classification = \Mockery::mock(\App\Services\OpportunityClassification::class); $classification->shouldReceive('publicRows')->andReturnUsing(fn($record) => in_array($record->id, [$main->id, $target->id], true) ? [$row] : []); $this->app->instance(\App\Services\OpportunityClassification::class, $classification); $service = app(InvestmentComplements::class); $options = $service->options('opportunity:'.$main->id); $offer = collect(collect($options)->firstWhere('code', 'processing')['opportunities'])->firstWhere('key', 'opportunity:'.$target->id); $this->assertNotNull($offer); $this->assertSame('value_chain', $offer['relationship']['type']); $selected = $service->resolve([['origin' => 'opportunity:'.$main->id, 'code' => 'processing', 'target' => 'opportunity:'.$target->id]]); $this->assertSame($row, $selected[0]['target']['relationship']['principal'][0]); $this->assertSame($row, $selected[0]['target']['relationship']['target'][0]); } }