199 lines
13 KiB
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());
|
|
}
|
|
}
|