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

153 lines
12 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\PrivateInvestmentPreparation;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
class PrivateInvestmentPreparationTest extends TestCase
{
use DatabaseTransactions;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
$db = DB::selectOne('select current_database() as name, inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $db->name);
$this->assertSame(55441, (int) $db->port);
config(['investor-path.enabled' => true, 'investor-path.private_preparation' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]);
}
private function declarations(): array
{
return ['investment_form' => 'direct', 'investment_preparation' => 'yes', 'investment_activity_scope' => 'general', 'implementation_mode' => 'domestic_entity', 'capital_origin' => 'national', 'site_tenure' => 'lease', 'project_concession' => 'no', 'significant_implications' => 'no', 'land_extension' => 'no', 'forest_concession' => 'no', 'total_investment' => '1000000', 'currency' => 'MZN', 'expected_start_date' => '2026-01-01', 'entity_stage' => 'being_formed'];
}
private function dossier(User $user): string
{
return $this->actingAs($user)->postJson('/investimentos', ['title' => 'SINTÉTICO C — percurso adaptativo', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id');
}
private function save(string $id, array $declarations, array $links = []): array
{
$view = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json();
$body = ['version' => $view['case']['version'], 'selection' => [], 'declarations' => $declarations, 'document_links' => $links];
$plan = $this->postJson('/investimentos/'.$id.'/contexto/previa', $body)->assertOk()->json();
// The preview is read-only.
$this->getJson('/investimentos/'.$id.'/contexto')->assertJsonPath('context.revision', $view['context']['revision']);
$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);
return $plan;
}
public function test_boundaries_unknown_conditions_and_scope_are_not_silently_presumed(): void
{
$service = app(PrivateInvestmentPreparation::class);
$assess = fn (array $changes = []) => $service->assess(['fields' => [...$this->declarations(), ...$changes]]);
$this->assertSame('registration', $assess(['total_investment' => '32000000000'])['mode']);
$this->assertSame('authorization', $assess(['total_investment' => '32000000000.0001'])['mode']);
$this->assertSame('authorization', $assess(['land_extension' => 'yes', 'area_required' => '10000', 'area_unit' => 'ha'])['mode']);
$this->assertSame('registration', $assess(['forest_concession' => 'yes', 'forest_concession_hectares' => '100000'])['mode']);
$this->assertSame('authorization', $assess(['forest_concession' => 'yes', 'forest_concession_hectares' => '100000.01'])['mode']);
$this->assertSame('indeterminate', $assess(['significant_implications' => 'unknown'])['mode']);
$this->assertSame('indeterminate', $assess(['currency' => 'USD'])['mode']);
$this->assertSame('indeterminate', $assess(['investment_form' => 'unknown'])['mode']);
$this->assertSame('indeterminate', $assess(['investment_form' => 'indirect'])['mode']);
$this->assertSame('authorization', $assess(['investment_form' => 'indirect', 'project_concession' => 'yes'])['mode']);
foreach ([['investment_activity_scope' => 'extractive'], ['expected_start_date' => '2027-01-01']] as $changes) {
$result = $assess($changes);
$this->assertSame('indeterminate', $result['mode']);
$this->assertFalse(collect($result['requirements'])->contains('required', true));
}
$this->assertSame('registration', $assess(['investor_origin' => 'foreign', 'tax_residency_country' => 'ZA'])['mode']);
$this->assertFalse(collect($assess(['investor_origin' => 'foreign'])['requirements'])->firstWhere('key', 'representation_license')['required']);
foreach (['zee', 'zfi'] as $regime) {
$zone = $service->assess(['fields' => $this->declarations(), 'selection' => ['park' => ['fields' => ['investment_regime' => $regime]]]]);
$this->assertStringContainsString('APIEX', $zone['authority']);
$this->assertStringContainsString('condicionado', $zone['authority']);
$this->assertArrayNotHasKey('benefit_rate', $zone);
}
$park = $service->assess(['fields' => $this->declarations(), 'selection' => ['park' => ['fields' => ['investment_regime' => 'industrial_park']]]]);
$this->assertSame('Governador da Província', $park['authority']);
}
public function test_context_changes_adapt_requirements_and_preserve_association_history_and_results(): void
{
$owner = User::factory()->create();
$id = $this->dossier($owner);
$doc = (string) Str::uuid();
DB::table('documents')->insert(['id' => $doc, 'file_name' => 'SINTETICO-C-QA.txt', 'media_type' => 'text/plain', 'storage_key' => 'synthetic-test-no-download', 'sha256' => hash('sha256', 'synthetic'), 'status' => 'quarantined']);
DB::table('desk_documents')->insert(['id' => (string) Str::uuid(), 'case_id' => $id, 'document_id' => $doc, 'category' => 'support', 'source_reference' => 'Synthetic QA only', 'linked_by' => $owner->id]);
$initial = $this->save($id, $this->declarations(), ['lease' => $doc]);
$this->assertSame('registration', $initial['private_investment']['after']['mode']);
$view = $this->getJson('/investimentos/'.$id.'/contexto')->json();
$this->assertSame('associated', collect($view['private_investment']['requirements'])->firstWhere('key', 'lease')['state']);
$this->assertLessThan(20000, mb_strlen($view['prefill']['simulator']['context_applicability_snapshot']));
$this->assertArrayNotHasKey('investment_preparation', $view['prefill']['simulator']);
$personalKeys = ['email', 'phone', 'person_type', 'investor_origin', 'tax_residency_country', 'fiscal_residency_country', 'entity_stage'];
// The browser stores presentation labels in selection_labels, never as snapshot fields.
foreach (array_keys($view['prefill']['simulator']) as $field) {
if (str_ends_with($field, '_label')) {
$view['prefill']['simulator']['selection_labels'][substr($field, 0, -6)] = $view['prefill']['simulator'][$field];
unset($view['prefill']['simulator'][$field]);
}
}
$this->postJson('/api/v1/simulation-drafts', ['context' => $id, 'personal_snapshot' => Arr::only($view['prefill']['simulator'], $personalKeys), 'economic_snapshot' => ['purpose' => 'simulate_only', ...Arr::except($view['prefill']['simulator'], $personalKeys)]], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated();
$plan = $this->save($id, ['project_concession' => 'yes', 'implementation_mode' => 'foreign_representation', 'site_tenure' => 'duat']);
$this->assertFalse($plan['calculation_inputs_changed'], 'Procedural changes do not change the inputs of the existing IVA calculation.');
$this->assertContains('S06', $plan['review_steps']);
$after = $plan['private_investment']['after'];
$this->assertSame('authorization', $after['mode']);
$this->assertSame('Conselho de Ministros', $after['authority']);
foreach (['feasibility', 'financial_capacity', 'representation_license', 'name_reservation', 'duat'] as $key) {
$this->assertTrue(collect($after['requirements'])->firstWhere('key', $key)['required']);
}
$lease = collect($after['requirements'])->firstWhere('key', 'lease');
$this->assertSame('not_applicable', $lease['state']);
$this->assertSame($doc, $lease['document_id']);
$this->assertSame(1, DB::table('desk_documents')->where('case_id', $id)->count());
$back = $this->save($id, ['project_concession' => 'no', 'implementation_mode' => 'domestic_entity', 'site_tenure' => 'lease', 'entity_stage' => 'existing']);
$this->assertTrue(collect($back['private_investment']['after']['requirements'])->firstWhere('key', 'registration')['required']);
$this->assertFalse(collect($back['private_investment']['after']['requirements'])->firstWhere('key', 'name_reservation')['required']);
$this->assertSame('associated', collect($back['private_investment']['after']['requirements'])->firstWhere('key', 'lease')['state']);
$this->assertSame('not_applicable', collect($back['private_investment']['after']['requirements'])->firstWhere('key', 'feasibility')['state']);
$events = DB::table('desk_events')->where('case_id', $id)->where('action', 'investment_context_changed')->count();
$this->assertSame(3, $events);
$this->getJson('/investimentos/'.$id)->assertOk()->assertJsonPath('private_investment.mode', 'registration');
$this->actingAs(User::factory()->create())->getJson('/investimentos/'.$id.'/contexto')->assertForbidden();
$this->actingAs($owner)->getJson('/investimentos/'.$id.'/contexto')->assertOk()->assertJsonPath('private_investment.mode', 'registration');
config(['investor-path.private_preparation' => false]);
$this->save($id, ['project_title' => 'Edição independente durante suspensão']);
$this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->assertJsonPath('private_investment', null)->assertJsonPath('context.preparation_snapshot.mode', 'registration')->assertJsonPath('context.preparation_documents.lease', $doc);
}
public function test_foreign_document_and_tampered_preview_cannot_be_committed_and_disabled_feature_does_not_accept_writes(): void
{
$owner = User::factory()->create();
$id = $this->dossier($owner);
$view = $this->getJson('/investimentos/'.$id.'/contexto')->json();
$body = ['version' => $view['case']['version'], 'selection' => [], 'declarations' => $this->declarations()];
$this->postJson('/investimentos/'.$id.'/contexto/previa', [...$body, 'document_links' => ['identity' => (string) Str::uuid()]])->assertUnprocessable();
$other = $this->dossier($owner);
$foreignDocument = (string) Str::uuid();
DB::table('documents')->insert(['id' => $foreignDocument, 'file_name' => 'OUTRO-DOSSIER-SINTETICO-QA.txt', 'media_type' => 'text/plain', 'storage_key' => 'synthetic-no-download', 'sha256' => hash('sha256', 'other synthetic'), 'status' => 'quarantined']);
DB::table('desk_documents')->insert(['id' => (string) Str::uuid(), 'case_id' => $other, 'document_id' => $foreignDocument, 'category' => 'support', 'source_reference' => 'Synthetic isolated QA', 'linked_by' => $owner->id]);
$this->postJson('/investimentos/'.$id.'/contexto/previa', [...$body, 'document_links' => ['identity' => $foreignDocument]])->assertUnprocessable();
$plan = $this->postJson('/investimentos/'.$id.'/contexto/previa', $body)->assertOk()->json();
$this->postJson('/investimentos/'.$id.'/contexto/confirmar', [...$body, 'declarations' => [...$body['declarations'], 'currency' => 'USD'], 'preview_hash' => $plan['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true])->assertConflict();
$this->getJson('/investimentos/'.$id.'/contexto')->assertJsonPath('context.revision', 0);
config(['investor-path.private_preparation' => false]);
$this->postJson('/investimentos/'.$id.'/contexto/previa', $body)->assertConflict();
$this->postJson('/investimentos/'.$id.'/contexto/previa', [...$body, 'declarations' => 'invalid'])->assertUnprocessable();
$this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->assertJsonPath('private_investment', null);
}
}