Files
Investidor-V2/tests/Feature/InvestmentComplementsTest.php

194 lines
15 KiB
PHP

<?php
namespace Tests\Feature;
use App\Http\Controllers\InvestmentPortfolioController;
use App\Models\Opportunity;
use App\Models\User;
use App\Services\InvestmentComplements;
use App\Services\PartnershipExchange;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
final class InvestmentComplementsTest extends TestCase
{
use DatabaseTransactions;
protected function setUp(): void
{
parent::setUp();
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->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]);
}
}