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 */ 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 */ 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()); } }