withoutVite(); config(['investor-path.enabled' => true, 'investmoz_ai.enabled' => false, 'suitecrm.enabled' => false, 'portal.notifications_enabled' => false]); Http::preventStrayRequests(); $this->owner = User::factory()->create(); } private function dossier(): string { return $this->actingAs($this->owner)->postJson('/investimentos', ['title' => 'SYNTHETIC BVM preparation', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); } private function url(string $id, string $slug = 'bolsa'): string { return '/investimentos/'.$id.'/guias/'.$slug; } /** @return array */ private function input(string $id, string $slug = 'bolsa', array $extra = []): array { return [...['version' => DB::table('desk_cases')->where('id', $id)->value('version'), 'operation_key' => (string) Str::uuid(), 'sha256' => app(InvestmentGuides::class)->guide($slug)['sha256'], 'instruments' => $slug === 'bolsa' ? ['third_shares'] : ['debt'], 'note' => 'SYNTHETIC private '.$slug], ...$extra]; } public function test_public_guide_has_five_real_paths_with_article_page_and_redaction_per_requirement(): void { $r = $this->getJson('/guias-investimento/bolsa')->assertOk()->assertHeader('Cache-Control', 'no-store, private') ->assertJsonCount(5, 'guide.instruments')->assertJsonCount(7, 'guide.sources')->assertJsonCount(6, 'guide.preparation')->assertJsonPath('case', null)->json(); $guide = $r['guide']; $this->assertSame(['official_shares', 'official_bonds', 'second_shares', 'second_bonds', 'third_shares'], array_keys($guide['instruments'])); foreach ([$guide['requirements'], ...array_column($guide['instruments'], 'requirements')] as $requirements) { foreach ($requirements as $requirement) { $this->assertNotEmpty($requirement['citations']); foreach ($requirement['citations'] as $cite) { $this->assertArrayHasKey($cite['source'], $guide['sources']); $this->assertNotEmpty($guide['sources'][$cite['source']]['edition']); $this->assertGreaterThan(0, $cite['page']); $this->assertMatchesRegularExpression('/art|âmbito/iu', $cite['reference']); } } } $this->get('/guias-investimento/bolsa')->assertOk()->assertInertia(fn ($page) => $page->component('investment/guide')); $this->assertDatabaseCount('desk_cases', 0); $this->assertDatabaseCount('desk_requests', 0); } public function test_current_minimums_remain_unknown_instead_of_reusing_the_expired_amounts_or_zero(): void { $guide = $this->getJson('/guias-investimento/bolsa')->assertOk()->assertJsonPath('guide.sources.minimums_historical.valid_until', '2025-03-31')->json('guide'); foreach ($guide['instruments'] as $path) { $gap = collect($path['requirements'])->firstWhere('kind', 'gap'); $this->assertNull($gap['value']); $this->assertSame('MZN', $gap['unit']); $this->assertStringContainsString('31/03/2025', $gap['text']); $this->assertStringContainsString('regulamento anual', $gap['text']); } $serialized = json_encode($guide, JSON_THROW_ON_ERROR); foreach (['16.000.000', '4.000.000', '1.000.000', '8.000.000', '2.000.000'] as $expired) { $this->assertStringNotContainsString($expired, $serialized); } $this->assertStringContainsString('um quarto', $guide['instruments']['second_shares']['requirements'][0]['text']); $this->assertStringContainsString('25%', $guide['instruments']['third_shares']['requirements'][0]['text']); } public function test_third_market_uses_2021_instead_of_the_old_nine_month_transition_and_keeps_initial_waivers_scoped(): void { $guide = app(InvestmentGuides::class)->guide('bolsa'); $third = collect($guide['instruments']['third_shares']['requirements'])->keyBy('id'); $this->assertStringContainsString('dois anos', $third['third_entry_dispersion']['text']); $this->assertStringContainsString('mais um ano', $third['third_entry_dispersion']['text']); $this->assertStringContainsString('sem extensão automática', $third['third_entry_dispersion']['text']); $this->assertStringNotContainsString('nove meses', json_encode($third, JSON_THROW_ON_ERROR)); $this->assertSame('exception', $third['third_entry_audit']['kind']); $this->assertStringContainsString('momento do pedido', $third['third_entry_audit']['text']); $this->assertStringContainsString('OCAM', $third['third_finances']['text']); $this->assertStringContainsString('semestrais', $third['third_reporting']['text']); $docs = collect($guide['documents'])->keyBy('id'); $this->assertNotContains('third_shares', $docs['audit']['paths']); $this->assertContains('third_shares', $docs['updated_information']['paths']); $this->assertStringContainsString('não reproduz uma dispensa geral', $docs['updated_information']['text']); $this->assertStringContainsString('um ano', $guide['instruments']['second_shares']['requirements'][1]['text']); $this->assertStringContainsString('nove meses', $guide['instruments']['second_shares']['requirements'][1]['text']); } public function test_sources_distinguish_supervision_sponsorship_registry_and_the_2022_nominative_and_sas_rules(): void { $guide = app(InvestmentGuides::class)->guide('bolsa'); $roles = implode(' ', array_column($guide['eligibility'], 'text')); foreach (['Banco de Moçambique', 'BVM', 'operador', 'Central de Valores Mobiliários', 'Não é um regulador separado'] as $expected) { $this->assertStringContainsString($expected, $roles); } $rules = collect($guide['requirements'])->keyBy('id'); $this->assertStringContainsString('simplificada', $rules['legal_form']['text']); $this->assertSame(63, $rules['legal_form']['citations'][1]['page']); $this->assertStringContainsString('2022', $rules['nominative']['text']); $this->assertStringContainsString('ao portador', $rules['nominative']['text']); foreach ($guide['handoffs'] as $link) { $this->assertStringStartsWith('https://www.bvm.co.mz/', $link['url']); $this->assertStringNotContainsString('?', $link['url']); } } public function test_dossier_choice_and_creation_pin_the_bvm_edition_without_saving_or_submitting_by_arrasto(): void { $guide = app(InvestmentGuides::class)->guide('bolsa'); $selection = ['slug' => 'bolsa', 'sha256' => $guide['sha256'], 'instruments' => ['second_bonds', 'third_shares']]; $target = '/investimentos?'.http_build_query(['investment_guide' => $selection]); $this->get($target)->assertRedirect('/login'); $this->assertSame(url($target), session('url.intended')); $id = $this->dossier(); $this->getJson($target)->assertOk()->assertJsonPath('guideSelection.slug', 'bolsa'); $record = $this->postJson('/investimentos', ['title' => 'SYNTHETIC selected destination', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true, 'investment_guide' => $selection])->assertCreated()->json(); $this->getJson($record['guide_href'])->assertOk()->assertJsonPath('guide.slug', 'bolsa')->assertJsonPath('instruments', ['second_bonds', 'third_shares'])->assertJsonPath('saved', null); $this->assertSame(0, DB::table('desk_events')->where('action', InvestmentGuides::EVENT)->count()); $this->getJson($this->url($id).'?instruments[0]=third_bonds')->assertUnprocessable(); $this->getJson($this->url($id).'?instruments[0]=debt')->assertUnprocessable(); } public function test_multiple_guides_reuse_one_dossier_and_cannot_replay_or_open_each_others_revisions(): void { $id = $this->dossier(); $before = DB::table('journey_steps')->orderBy('id')->get()->toJson(); $capital = $this->input($id, 'capitalizacao'); $this->postJson($this->url($id, 'capitalizacao'), $capital)->assertOk(); $first = $this->getJson($this->url($id, 'capitalizacao'))->json(); $bvm = $this->input($id); $this->postJson($this->url($id), $bvm)->assertOk()->assertJsonPath('revision', 1); $this->postJson($this->url($id), $bvm)->assertOk()->assertJsonPath('replayed', true); $this->postJson($this->url($id), [...$bvm, 'operation_key' => $capital['operation_key']])->assertConflict(); $this->getJson($this->url($id).'?revision='.$first['history'][0]['id'])->assertNotFound(); $this->getJson($this->url($id, 'capitalizacao'))->assertOk()->assertJsonPath('saved', $first['saved'])->assertJsonCount(1, 'history'); $this->getJson($this->url($id))->assertOk()->assertJsonCount(1, 'history')->assertJsonPath('guide.slug', 'bolsa'); $this->assertSame($before, DB::table('journey_steps')->orderBy('id')->get()->toJson()); $stored = DB::table('desk_cases')->where('id', $id)->first(); $this->assertStringNotContainsString('SYNTHETIC private bolsa', $stored->payload); $payload = app(VirtualDesk::class)->decode($stored->payload); $this->assertCount(2, $payload['investment_guides']); $this->assertDatabaseCount('desk_requests', 0); $this->assertDatabaseCount('organizations', 0); } public function test_pagination_filters_the_requested_guide_when_the_shared_event_stream_is_interleaved(): void { $id = $this->dossier(); for ($i = 1; $i <= 26; $i++) { $this->postJson($this->url($id), $this->input($id, 'bolsa', ['note' => 'SYNTHETIC BVM revision '.$i]))->assertOk(); $this->postJson($this->url($id, 'capitalizacao'), $this->input($id, 'capitalizacao', ['note' => 'SYNTHETIC capital revision '.$i]))->assertOk(); } $page = $this->getJson($this->url($id))->assertOk()->assertJsonCount(25, 'history')->assertJsonPath('saved.revision', 26)->json(); $old = $this->getJson($this->url($id).'?history_before='.$page['historyNext'])->assertOk()->assertJsonCount(1, 'history')->assertJsonPath('history.0.revision', 1)->json(); $this->getJson($this->url($id).'?revision='.$old['history'][0]['id'])->assertOk()->assertJsonPath('guide.slug', 'bolsa')->assertJsonPath('note', 'SYNTHETIC BVM revision 1'); $this->getJson($this->url($id, 'capitalizacao').'?revision='.$old['history'][0]['id'])->assertNotFound(); } }