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

199 lines
13 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\CompanyPreparation;
use App\Services\VirtualDesk;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Tests\TestCase;
final class CompanyPreparationTest extends TestCase
{
use RefreshDatabase;
private User $owner;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
config(['investor-path.enabled' => true, 'investmoz_ai.enabled' => false, 'suitecrm.enabled' => false, 'portal.notifications_enabled' => false]);
Http::preventStrayRequests();
$this->owner = User::factory()->create(['name' => 'SYNTHETIC Proponent']);
}
private function dossier(): string
{
return $this->actingAs($this->owner)->postJson('/investimentos', ['title' => 'SYNTHETIC corporate preparation', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id');
}
/** @return array<string,mixed> */
private function party(array $changes = []): array
{
return [...['id' => (string) Str::uuid(), 'name' => 'SYNTHETIC Participant', 'kind' => 'person', 'source' => null, 'roles' => ['subscriber'], 'cash' => '0', 'in_kind' => '0', 'paid' => '0'], ...$changes];
}
/** @return array<string,mixed> */
private function input(string $id, array $facts): array
{
return ['version' => DB::table('desk_cases')->where('id', $id)->value('version'), 'operation_key' => (string) Str::uuid(), 'facts' => $facts];
}
public function test_private_empty_preparation_needs_no_templates_ia_or_master_writes(): void
{
$id = $this->dossier();
$url = '/investimentos/'.$id.'/preparar-empresa';
$this->getJson($url)->assertOk()->assertHeader('Cache-Control', 'no-store, private')->assertJsonPath('definition.template_status', 'source_pending')->assertJsonPath('review.totals', null)->assertJsonPath('saved', null);
$this->get($url)->assertOk()->assertInertia(fn ($page) => $page->component('investment/company-preparation'));
$this->postJson($url, $this->input($id, []))->assertOk()->assertJsonPath('revision', 1);
$this->getJson($url)->assertOk()->assertJsonPath('saved.revision', 1)->assertJsonPath('review.status', 'preparation');
$this->assertDatabaseCount('organizations', 0);
$this->assertDatabaseCount('investment_company_declarations', 0);
$this->assertDatabaseCount('desk_requests', 0);
}
public function test_exact_capital_and_shares_do_not_round_large_or_fractional_values(): void
{
$id = $this->dossier();
$url = '/investimentos/'.$id.'/preparar-empresa';
$facts = ['form' => 'anonymous', 'currency' => 'MZN', 'capital' => '999999999999999999.000003', 'nominal_value' => '0,000001', 'parties' => [
$this->party(['cash' => '999999999999999999', 'in_kind' => '0', 'paid' => '999999999999999999', 'share_count' => '999999999999999999']),
$this->party(['cash' => '0.000001', 'in_kind' => '0.000002', 'paid' => '0.000001', 'share_count' => '3']),
]];
$this->postJson($url, $this->input($id, $facts))->assertOk();
$r = $this->getJson($url)->assertOk()->assertJsonPath('review.totals.subscribed', '999999999999999999.000003')
->assertJsonPath('review.totals.paid', '999999999999999999.000001')->assertJsonPath('review.totals.unpaid', '0.000002')
->assertJsonPath('review.totals.shares_nominal', '1000000000000.000002')->json();
$this->assertStringContainsString('valor nominal', implode(' ', $r['review']['issues']));
$this->assertStringNotContainsString('soma das subscrições difere', implode(' ', $r['review']['issues']));
}
public function test_unknown_amounts_never_become_zero_and_inconsistencies_can_be_saved_as_draft(): void
{
$id = $this->dossier();
$url = '/investimentos/'.$id.'/preparar-empresa';
$facts = ['form' => 'quotas', 'capital' => '100', 'currency' => 'MZN', 'parties' => [$this->party(['cash' => null, 'paid' => null])]];
$this->postJson($url, $this->input($id, $facts))->assertOk();
$this->getJson($url)->assertOk()->assertJsonPath('review.totals.subscribed', null)->assertJsonPath('review.totals.paid', null)->assertJsonPath('review.totals.unpaid', null);
$facts['parties'][0]['cash'] = '40';
$facts['parties'][0]['paid'] = '41';
$this->postJson($url, $this->input($id, $facts))->assertOk();
$r = $this->getJson($url)->assertOk()->assertJsonPath('review.totals.unpaid', '-1')->json();
$issues = implode(' ', $r['review']['issues']);
$this->assertStringContainsString('excede', $issues);
$this->assertStringContainsString('difere do capital', $issues);
foreach (['-1', '1e3', '1.1234567', '1 000', '1000000000000000000'] as $invalid) {
$this->postJson($url, $this->input($id, ['capital' => $invalid]))->assertUnprocessable()->assertJsonValidationErrors('facts.capital');
}
$this->postJson($url, $this->input($id, ['parties' => [$this->party(['share_count' => '1.5'])]]))->assertUnprocessable();
}
public function test_role_accumulation_distinct_people_and_work_are_not_merged_or_counted_as_capital(): void
{
$id = $this->dossier();
$url = '/investimentos/'.$id.'/preparar-empresa';
$a = $this->party(['cash' => '10', 'work' => 'SYNTHETIC labour', 'roles' => ['subscriber', 'administrator']]);
$b = $this->party(['cash' => '20', 'roles' => ['subscriber', 'representative'], 'represented_id' => $a['id']]);
$this->postJson($url, $this->input($id, ['form' => 'collective', 'capital' => '30', 'currency' => 'MZN', 'parties' => [$a, $b]]))->assertOk();
$r = $this->getJson($url)->assertOk()->assertJsonCount(2, 'facts.parties')->assertJsonPath('review.totals.subscribed', '30')->json();
$this->assertStringContainsString('artigo 266', $r['review']['note']);
$this->assertStringNotContainsString('representante', implode(' ', $r['review']['issues']));
$this->assertNotContains('beneficial_owner', $r['facts']['parties'][0]['roles']);
$b['represented_id'] = $b['id'];
$this->postJson($url, $this->input($id, ['parties' => [$a, $b]]))->assertOk();
$this->assertStringContainsString('representante', implode(' ', $this->getJson($url)->json('review.issues')));
$this->postJson($url, $this->input($id, ['parties' => [$a, $a]]))->assertUnprocessable();
}
public function test_reused_dossier_identity_is_server_resolved_and_historical_values_remain_frozen(): void
{
$id = $this->dossier();
$url = '/investimentos/'.$id.'/preparar-empresa';
$party = $this->party(['source' => 'case_proponent', 'name' => 'FORGED identity', 'kind' => 'entity']);
$this->postJson($url, $this->input($id, ['parties' => [$party]]))->assertOk();
$r = $this->getJson($url)->assertOk()->assertJsonPath('facts.parties.0.name', 'SYNTHETIC Proponent')->assertJsonPath('facts.parties.0.kind', 'person')->json();
$this->assertSame($id, $r['facts']['parties'][0]['source_snapshot']['reference']);
$case = DB::table('desk_cases')->where('id', $id)->first();
$payload = app(VirtualDesk::class)->decode($case->payload);
$payload['name'] = 'SYNTHETIC future context';
DB::table('desk_cases')->where('id', $id)->update(['payload' => app(VirtualDesk::class)->encode($payload)]);
$this->postJson($url, $this->input($id, ['parties' => [$party]]))->assertOk()->assertJsonPath('unchanged', true);
$this->getJson($url)->assertJsonPath('facts.parties.0.name', 'SYNTHETIC Proponent');
$this->postJson($url, $this->input($id, ['parties' => [$this->party(['source' => 'case_company'])]]))->assertUnprocessable();
$forged = $this->party(['source_snapshot' => ['reference' => 'invented']]);
$this->postJson($url, $this->input($id, ['parties' => [$forged]]))->assertUnprocessable();
$stored = DB::table('desk_cases')->where('id', $id)->first();
$this->assertStringNotContainsString('SYNTHETIC Proponent', $stored->payload);
$event = DB::table('desk_events')->where('case_id', $id)->where('action', CompanyPreparation::EVENT)->first();
$this->assertStringNotContainsString('SYNTHETIC Proponent', $event->payload);
$this->assertStringNotContainsString('SYNTHETIC Proponent', DB::table('audit_events')->where('entity_id', $id)->get()->toJson());
}
public function test_correct_draft_history_and_dossier_version_are_preserved_without_advancing_workflow(): void
{
$id = $this->dossier();
$url = '/investimentos/'.$id.'/preparar-empresa';
$before = DB::table('journey_steps')->orderBy('id')->get()->toJson();
$input = $this->input($id, ['form' => 'quotas', 'capital' => '10']);
$this->postJson($url, $input)->assertOk()->assertJsonPath('revision', 1);
$this->postJson($url, $input)->assertOk()->assertJsonPath('replayed', true);
$this->postJson($url, [...$input, 'facts' => ['capital' => '11']])->assertConflict();
$this->postJson($url, [...$input, 'operation_key' => (string) Str::uuid()])->assertConflict();
$first = $this->getJson($url)->json();
$this->postJson($url, $this->input($id, ['form' => 'quotas', 'capital' => '10.000']))->assertOk()->assertJsonPath('unchanged', true);
$this->postJson($url, $this->input($id, ['form' => 'individual', 'capital' => '10']))->assertOk()->assertJsonPath('revision', 2);
$this->getJson($url)->assertJsonPath('facts.capital', '10')->assertJsonPath('review.totals', null);
config(['company-comparison.source.edition' => 'SYNTHETIC future edition']);
$this->getJson($url.'?revision='.$first['history'][0]['id'])->assertOk()->assertJsonPath('historical', true)->assertJsonPath('facts.form', 'quotas')->assertJsonPath('definition.source', $first['definition']['source']);
$this->getJson($url)->assertJsonPath('saved.revision', 2);
$this->assertSame($before, DB::table('journey_steps')->orderBy('id')->get()->toJson());
DB::table('desk_cases')->where('id', $id)->update(['closed_at' => now()]);
config(['company-comparison.forms' => ['individual' => ['title' => 'SYNTHETIC future catalogue']]]);
$this->postJson($url, $input)->assertOk()->assertJsonPath('replayed', true);
$this->postJson($url, $this->input($id, []))->assertConflict();
}
public function test_all_history_pages_and_cross_dossier_revision_access(): void
{
$id = $this->dossier();
$url = '/investimentos/'.$id.'/preparar-empresa';
for ($i = 1; $i <= 26; $i++) {
$this->postJson($url, $this->input($id, ['notes' => 'SYNTHETIC revision '.$i]))->assertOk();
}
$r = $this->getJson($url)->assertJsonCount(25, 'history')->json();
$old = $this->getJson($url.'?history_before='.$r['historyNext'])->assertJsonCount(1, 'history')->json();
$this->getJson($url.'?revision='.$old['history'][0]['id'])->assertOk()->assertJsonPath('saved.revision', 1);
$other = $this->dossier();
$this->getJson('/investimentos/'.$other.'/preparar-empresa?revision='.$old['history'][0]['id'])->assertNotFound();
$this->getJson($url.'?history_before=invalid')->assertUnprocessable();
}
public function test_current_permission_is_required_before_read_replay_or_validation(): void
{
$id = $this->dossier();
$url = '/investimentos/'.$id.'/preparar-empresa';
$org = (string) Str::uuid();
DB::table('organizations')->insert(['id' => $org, 'name' => 'SYNTHETIC Org', 'organization_type' => 'private']);
DB::table('desk_cases')->where('id', $id)->update(['organization_id' => $org]);
$editor = User::factory()->create();
$viewer = User::factory()->create();
foreach ([[$this->owner, 'officer'], [$editor, 'officer'], [$viewer, 'viewer']] as [$actor, $role]) {
DB::table('memberships')->insert(['id' => (string) Str::uuid(), 'organization_id' => $org, 'user_id' => $actor->id, 'role' => $role]);
}
$input = $this->input($id, ['notes' => 'SYNTHETIC private']);
$this->actingAs($editor)->postJson($url, $input)->assertOk();
$this->actingAs($viewer)->getJson($url)->assertOk()->assertJsonPath('canWrite', false);
$this->postJson($url, $input)->assertForbidden();
DB::table('memberships')->where('organization_id', $org)->where('user_id', $editor->id)->update(['deleted_at' => now()]);
$this->actingAs($editor)->getJson($url)->assertForbidden();
$this->postJson($url, $input)->assertForbidden();
$this->postJson($url, [])->assertForbidden();
$this->actingAs(User::factory()->create())->getJson($url)->assertForbidden();
$this->assertSame(1, DB::table('desk_events')->where('case_id', $id)->where('action', CompanyPreparation::EVENT)->count());
}
}