Files
Investidor-V2/tests/Feature/BvmAdmissionGuideTest.php
T

175 lines
11 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\InvestmentGuides;
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 BvmAdmissionGuideTest 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();
}
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<string,mixed> */
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();
}
}