Files
Investidor-V2/tests/Feature/InvestorPathTest.php
nala 20971d523f
tests / ci (push) Canceled after 0s
oportunidades 6
2026-10-10 21:17:46 +02:00

175 lines
15 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class InvestorPathTest extends TestCase
{
use RefreshDatabase;
private User $owner;
private User $manager;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
$this->owner = User::factory()->create();
$this->manager = User::factory()->create();
config(['investor-path.enabled' => true, 'portal.managers' => [$this->manager->email], 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]);
$identity = DB::selectOne('select current_database() as name, inet_server_port() as port');
$this->assertStringEndsWith('_test', $identity->name);
$this->assertSame(55441, (int) $identity->port);
Http::fake();
}
private function context(string $title = 'Ideia sintética QA'): string
{
return $this->actingAs($this->owner)->postJson('/investimentos', ['title' => $title, 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id');
}
private function pathView(string $id): array
{
return $this->actingAs($this->owner)->getJson('/investimentos/'.$id)->assertOk()->json();
}
private function snapshot(string $title): array
{
return ['personal_snapshot' => [], 'economic_snapshot' => ['purpose' => 'simulate_only', 'project_title' => $title, 'project_summary' => 'Descrição guardada do investimento '.$title,
'financial_tools_snapshot' => json_encode(['version' => 1, 'data' => ['budgetPeriod' => 'Mensal',
'loan' => ['principal' => '100000', 'rate' => '0', 'months' => '24', 'rateType' => 'nominal', 'periodMonths' => '1', 'gracePeriods' => '0', 'upfrontFee' => '0', 'recurringFee' => '0'],
'breakEven' => ['fixed' => '1000', 'price' => '100', 'variable' => '50'], 'funding' => ['total' => '100000', 'secured' => '30000'],
'incomes' => [['label' => 'Receitas QA', 'amount' => '100']], 'expenses' => [['label' => 'Custos QA', 'amount' => '50']]]])]];
}
public function test_existing_empty_desk_draft_can_explicitly_reuse_current_contextual_summary_without_overwriting_data(): void
{
$service = $this->actingAs($this->manager)->postJson('/management/balcao/servicos', ['code' => 'project-preparation', 'confirm' => true])->assertCreated()->json('id');
$id = $this->context('Dossier anterior à ferramenta');
$support = $this->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->assertCreated()->json('id');
$data = [...$this->snapshot('Resumo posterior'), 'context' => $id];
$draft = $this->postJson('/api/v1/simulation-drafts', $data, ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
$initial = $this->getJson('/balcao-virtual/pedidos/'.$support)->assertOk()->assertJsonPath('request.payload.description', '')->json();
$preview = $initial['toolPrefill'] ?? null;
$this->assertNotNull($preview, 'The existing empty draft needs an explicit contextual reuse action.');
$this->assertSame($draft, $preview['draft_id']);
$this->assertSame($data['economic_snapshot']['project_summary'], $preview['description']);
$this->actingAs($this->manager)->getJson('/balcao-virtual/pedidos/'.$support)->assertJsonPath('toolPrefill', null);
$this->actingAs($this->owner);
$payload = [...$initial['request']['payload'], 'description' => $preview['description'], 'country' => 'PT', 'declared_origin_country_code' => 'ZA'];
$source = ['draft_id' => $preview['draft_id'], 'draft_version' => $preview['draft_version'], 'snapshot_sha256' => $preview['snapshot_sha256']];
$this->putJson('/balcao-virtual/pedidos/'.$support, ['version' => $initial['request']['version'], 'payload' => $payload, 'tool_prefill' => [...$source, 'draft_id' => (string) Str::uuid()]])->assertUnprocessable();
$this->putJson('/balcao-virtual/pedidos/'.$support, ['version' => $initial['request']['version'], 'payload' => $payload, 'tool_prefill' => [...$source, 'draft_version' => $source['draft_version'] + 1]])->assertUnprocessable();
$this->getJson('/balcao-virtual/pedidos/'.$support)->assertJsonPath('request.version', $initial['request']['version'])->assertJsonPath('request.payload.description', '');
$this->assertDatabaseCount('desk_revisions', 0);
$this->putJson('/balcao-virtual/pedidos/'.$support, ['version' => $initial['request']['version'], 'payload' => $payload, 'tool_prefill' => $source])->assertOk()->assertJsonPath('status', 'draft');
$recovered = $this->getJson('/balcao-virtual/pedidos/'.$support)->assertJsonPath('request.payload.description', $preview['description'])->assertJsonPath('request.payload.country', 'PT')->assertJsonPath('request.payload.declared_origin_country_code', 'ZA')->assertJsonPath('toolPrefill', null)->json();
$this->assertSame('Dossier anterior à ferramenta', $recovered['request']['payload']['title']);
$this->assertDatabaseCount('desk_requests', 1);
$this->assertDatabaseCount('desk_revisions', 1);
$this->assertDatabaseHas('desk_events', ['request_id' => $support, 'action' => 'request_prefill_reused']);
$this->putJson('/balcao-virtual/pedidos/'.$support, ['version' => $recovered['request']['version'], 'payload' => [...$payload, 'description' => 'Tentativa de substituir texto existente'], 'tool_prefill' => $source])->assertUnprocessable();
$this->actingAs(User::factory()->create())->putJson('/balcao-virtual/pedidos/'.$support, ['version' => $recovered['request']['version'], 'payload' => $payload, 'tool_prefill' => $source])->assertForbidden();
}
public function test_two_independent_investments_recover_tool_evidence_support_and_internal_tasks_without_external_calls(): void
{
$service = $this->actingAs($this->manager)->postJson('/management/balcao/servicos', ['code' => 'project-preparation', 'confirm' => true])->assertCreated()->json('id');
$ids = [$this->context('A sem terra nem financiamento externo'), $this->context('B com terra e financiamento externo')];
$draftIds = [];
foreach ($ids as $index => $id) {
$view = $this->pathView($id);
$this->assertCount(19, $view['steps']);
$this->assertCount(5, $view['phases']);
$this->postJson('/investimentos/'.$id.'/aplicabilidade', ['version' => $view['case']['version'], 'land_needed' => $index ? 'yes' : 'no', 'external_funding' => $index ? 'yes' : 'no', 'viability' => 'proceed', 'reason' => 'Cenário sintético declarado pelo promotor'])->assertOk();
$data = [...$this->snapshot($index ? 'B' : 'A'), 'context' => $id];
$key = (string) Str::uuid();
$created = $this->postJson('/api/v1/simulation-drafts', $data, ['Idempotency-Key' => $key])->assertCreated();
$draft = $created->json('data.id');
$draftIds[] = $draft;
$this->postJson('/api/v1/simulation-drafts', $data, ['Idempotency-Key' => $key])->assertOk()->assertJsonPath('data.replayed', true);
$this->postJson('/api/v1/simulation-drafts', $data, ['Idempotency-Key' => (string) Str::uuid()])->assertConflict();
$this->getJson('/api/v1/simulation-drafts/current?context='.$id)->assertJsonPath('data.id', $draft)->assertJsonPath('data.economic_snapshot.project_title', $index ? 'B' : 'A');
$returned = $this->postJson('/investimentos/'.$id.'/resultados', ['version' => $this->pathView($id)['case']['version'], 'draft_id' => $draft, 'draft_version' => 1])->assertOk()->json('result');
$this->postJson('/investimentos/'.$id.'/resultados', ['version' => 1, 'draft_id' => $draft, 'draft_version' => 1])->assertOk()->assertJsonPath('result.id', $returned['id']);
$support = $this->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->assertCreated()->json('id');
$this->getJson('/balcao-virtual/pedidos/'.$support)->assertJsonPath('request.status', 'draft')->assertJsonPath('request.payload.description', $data['economic_snapshot']['project_summary']);
$v = $this->pathView($id);
$this->assertSame($index ? 'applicable' : 'not_needed_for_preparation', collect($v['steps'])->firstWhere('code', 'S10')['applicability']);
$this->postJson('/investimentos/'.$id.'/etapas/S10', ['version' => $v['case']['version'], 'state' => 'waiting_external', 'action' => 'prepare', 'reason' => 'Aguarda localização e informação do terreno'])->assertStatus($index ? 200 : 422);
$task = $this->postJson('/investimentos/'.$id.'/tarefas', ['version' => $this->pathView($id)['case']['version'], 'code' => 'S02', 'title' => 'Análise de mercado independente', 'due_at' => now()->addWeek()->toIso8601String(), 'consent' => true])->assertCreated()->json('id');
$this->actingAs($this->manager)->postJson('/balcao-virtual/dossiers/'.$id.'/tarefas/'.$task, ['version' => 1, 'status' => 'in_progress', 'assignee_user_id' => $this->manager->id, 'reason' => 'Acompanhamento interno atribuído'])->assertOk();
$this->postJson('/balcao-virtual/dossiers/'.$id.'/tarefas/'.$task, ['version' => 2, 'status' => 'done', 'reason' => 'Evidência de mercado preparada enquanto a terra aguarda'])->assertOk();
$this->getJson('/investimentos/'.$id)->assertJsonPath('results', [])->assertJsonPath('tasks.0.status', 'done')->assertJsonPath('canWrite', false)->assertJsonPath('canReview', true);
$this->getJson('/balcao-virtual/dossiers/'.$id)->assertJsonMissingPath('case.payload.investor_path');
// A fresh HTTP session and a newly resolved account recover only this context's data.
$this->flushSession();
$this->actingAs(User::findOrFail($this->owner->id))->getJson('/investimentos/'.$id)->assertJsonPath('results.0.id', $returned['id'])->assertJsonPath('results.0.draft_id', $draft)->assertJsonPath('tasks.0.status', 'done');
$this->get('/investimentos/'.$id)->assertInertia(fn (Assert $page) => $page->component('investor-path/show')->has('steps', 19)->has('phases', 5)->where('case.id', $id));
}
$this->assertNotSame($draftIds[0], $draftIds[1]);
$this->actingAs($this->owner)->getJson('/api/v1/simulation-drafts/'.$draftIds[0].'?context='.$ids[1])->assertForbidden();
$this->putJson('/api/v1/simulation-drafts/'.$draftIds[0], [...$this->snapshot('Outra ideia'), 'context' => $ids[1]], ['If-Match' => '"sfd-'.$draftIds[0].'-v1"'])->assertForbidden();
$this->postJson('/investimentos/'.$ids[1].'/resultados', ['version' => $this->pathView($ids[1])['case']['version'], 'draft_id' => $draftIds[0], 'draft_version' => 1])->assertForbidden();
$this->actingAs($this->manager)->getJson('/api/v1/simulation-drafts/'.$draftIds[0])->assertNotFound();
$this->actingAs($this->owner)->getJson('/api/v1/simulation-drafts/current')->assertJsonPath('data', null);
$this->assertDatabaseCount('simulation_form_drafts', 2);
$this->assertDatabaseCount('journey_steps', 38);
$this->assertDatabaseCount('desk_requests', 2);
$this->assertDatabaseCount('tasks', 2);
Http::assertNothingSent();
}
public function test_permissions_stale_versions_and_real_dependencies_do_not_block_independent_preparation(): void
{
$id = $this->context();
$v = $this->pathView($id);
$this->assertSame('undetermined', collect($v['steps'])->firstWhere('code', 'S10')['applicability']);
$payload = ['version' => $v['case']['version'], 'state' => 'preparing', 'action' => 'prepare', 'reason' => 'Preparação independente'];
$this->postJson('/investimentos/'.$id.'/etapas/S18', $payload)->assertOk();
$this->postJson('/investimentos/'.$id.'/etapas/S02', $payload)->assertConflict();
$payload['version'] = $this->pathView($id)['case']['version'];
$this->postJson('/investimentos/'.$id.'/etapas/S18', [...$payload, 'action' => 'operate'])->assertUnprocessable();
$this->postJson('/investimentos/'.$id.'/etapas/S18', [...$payload, 'state' => 'completed'])->assertUnprocessable();
$this->postJson('/investimentos/'.$id.'/etapas/S02', [...$payload, 'state' => 'approved', 'action' => 'record_external'])->assertForbidden();
$this->actingAs($this->manager)->postJson('/investimentos/'.$id.'/etapas/S02', [...$payload, 'state' => 'approved', 'action' => 'record_external'])->assertUnprocessable();
$stranger = User::factory()->create();
$this->actingAs($stranger)->getJson('/investimentos/'.$id)->assertForbidden();
$this->postJson('/api/v1/simulation-drafts', [...$this->snapshot('Invasão'), 'context' => $id], ['Idempotency-Key' => (string) Str::uuid()])->assertForbidden();
$this->postJson('/investimentos/'.$id.'/etapas/S02', $payload)->assertForbidden();
$this->assertDatabaseCount('simulation_form_drafts', 0);
$this->assertDatabaseCount('journey_steps', 19);
$this->actingAs($this->owner)->get('/dashboard')->assertInertia(fn (Assert $page) => $page->where('summary', null)->where('investorPath.contexts.0.id', $id));
$this->actingAs($this->manager)->get('/dashboard')->assertInertia(fn (Assert $page) => $page->has('summary')->missing('investorPath'));
}
public function test_applicability_changes_preserve_old_branch_evidence_and_legacy_workflow(): void
{
$id = $this->context();
$workflow = DB::table('desk_cases')->where('id', $id)->value('workflow');
$facts = ['land_needed' => 'yes', 'external_funding' => 'unknown', 'viability' => 'reformulate', 'reason' => 'Preparação provisória'];
$this->postJson('/investimentos/'.$id.'/aplicabilidade', ['version' => $this->pathView($id)['case']['version'], ...$facts])->assertOk();
$this->postJson('/investimentos/'.$id.'/etapas/S10', ['version' => $this->pathView($id)['case']['version'], 'state' => 'preparing', 'action' => 'prepare', 'reason' => 'Evidência anterior do terreno'])->assertOk();
$this->postJson('/investimentos/'.$id.'/aplicabilidade', ['version' => $this->pathView($id)['case']['version'], ...$facts, 'land_needed' => 'no'])->assertOk();
$step = collect($this->pathView($id)['steps'])->firstWhere('code', 'S10');
$this->assertSame('dispensed', $step['state']);
$this->assertSame('preparing', $step['saved_state']);
$this->assertSame('Evidência anterior do terreno', $step['detail']['reason']);
$this->postJson('/investimentos/'.$id.'/aplicabilidade', ['version' => $this->pathView($id)['case']['version'], ...$facts])->assertOk();
$this->assertSame('preparing', collect($this->pathView($id)['steps'])->firstWhere('code', 'S10')['state']);
$this->assertSame($workflow, DB::table('desk_cases')->where('id', $id)->value('workflow'));
config(['investor-path.enabled' => false]);
$this->getJson('/investimentos')->assertNotFound();
$this->getJson('/investimentos/'.$id)->assertNotFound();
$this->getJson('/balcao-virtual/dossiers/'.$id)->assertOk();
}
}