187 lines
12 KiB
PHP
187 lines
12 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature;
|
||
|
||
use App\Models\User;
|
||
use App\Services\InvestmentGuides;
|
||
use App\Services\InvestorContext;
|
||
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 InvestmentGuidesTest 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 capitalisation', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id');
|
||
}
|
||
|
||
private function url(string $id): string
|
||
{
|
||
return '/investimentos/'.$id.'/guias/capitalizacao';
|
||
}
|
||
|
||
/** @return array<string,mixed> */
|
||
private function input(string $id, array $extra = []): array
|
||
{
|
||
return [...['version' => DB::table('desk_cases')->where('id', $id)->value('version'), 'operation_key' => (string) Str::uuid(), 'sha256' => app(InvestmentGuides::class)->guide('capitalizacao')['sha256'], 'instruments' => ['private_equity', 'debt'], 'note' => 'SYNTHETIC private preparation'], ...$extra];
|
||
}
|
||
|
||
public function test_public_source_versioned_guide_requires_neither_login_nor_ia_and_does_not_capture_private_notes(): void
|
||
{
|
||
$this->getJson('/guias-investimento/capitalizacao?note=PRIVATE')->assertOk()->assertHeader('Cache-Control', 'no-store, private')
|
||
->assertJsonPath('note', '')->assertJsonPath('case', null)->assertJsonCount(4, 'guide.instruments')->assertJsonCount(5, 'guide.sources')
|
||
->assertJsonPath('guide.sources.bni.sha256', 'fbeaab84c4cad22ee63e5c1dfb61ea3206333885ddd80c8001d37a3f49d2820e')
|
||
->assertJsonPath('guide.sources.ifc_apply.locator', 'Eligibility; Investment Proposals, pontos 1–7');
|
||
$this->get('/guias-investimento/capitalizacao')->assertOk()->assertInertia(fn ($page) => $page->component('investment/guide'));
|
||
$this->assertDatabaseCount('desk_cases', 0);
|
||
$this->assertDatabaseCount('organizations', 0);
|
||
$this->getJson('/guias-investimento/invented')->assertNotFound();
|
||
}
|
||
|
||
public function test_instrument_choices_are_coded_and_source_pin_survives_normal_authentication_and_dossier_choice(): void
|
||
{
|
||
$guide = app(InvestmentGuides::class)->guide('capitalizacao');
|
||
$selection = ['slug' => 'capitalizacao', 'sha256' => $guide['sha256'], 'instruments' => ['private_equity', 'debt', 'debt']];
|
||
$this->getJson('/guias-investimento/capitalizacao?'.http_build_query($selection))->assertOk()->assertJsonPath('instruments', ['debt', 'private_equity']);
|
||
$this->getJson('/guias-investimento/capitalizacao?instruments[0]=invented')->assertUnprocessable();
|
||
$this->getJson('/guias-investimento/capitalizacao?instruments=debt')->assertUnprocessable();
|
||
$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.instruments', ['debt', 'private_equity']);
|
||
$this->getJson($this->url($id).'?'.http_build_query(['current' => 1, ...$selection]))->assertOk()->assertJsonPath('workingCopy', true)->assertJsonPath('saved', null);
|
||
$new = $this->postJson('/investimentos', ['title' => 'SYNTHETIC chosen guide destination', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true, 'investment_guide' => $selection])->assertCreated()->json();
|
||
$this->getJson($new['guide_href'])->assertOk()->assertJsonPath('instruments', ['debt', 'private_equity'])->assertJsonPath('saved', null);
|
||
$this->assertSame(0, DB::table('desk_events')->where('action', InvestmentGuides::EVENT)->count());
|
||
$before = DB::table('desk_cases')->count();
|
||
$this->postJson('/investimentos', ['title' => 'SYNTHETIC invalid guide', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true, 'investment_guide' => [...$selection, 'sha256' => str_repeat('0', 64)]])->assertConflict();
|
||
$this->assertSame($before, DB::table('desk_cases')->count());
|
||
}
|
||
|
||
public function test_private_reference_is_encrypted_scoped_versioned_idempotent_and_does_not_advance_any_finance_stage(): void
|
||
{
|
||
$id = $this->dossier();
|
||
$url = $this->url($id);
|
||
$steps = DB::table('journey_steps')->orderBy('id')->get()->toJson();
|
||
$before = app(VirtualDesk::class)->decode(DB::table('desk_cases')->where('id', $id)->value('payload'));
|
||
$input = $this->input($id, ['guide' => ['title' => 'FORGED guarantee'], 'instruments' => ['debt', 'private_equity']]);
|
||
$this->postJson($url, $input)->assertOk()->assertJsonPath('revision', 1);
|
||
$this->postJson($url, $input)->assertOk()->assertJsonPath('replayed', true);
|
||
$this->postJson($url, [...$input, 'note' => 'SYNTHETIC changed body'])->assertConflict();
|
||
$this->postJson($url, [...$input, 'operation_key' => (string) Str::uuid()])->assertConflict();
|
||
$this->postJson($url, $this->input($id))->assertOk()->assertJsonPath('unchanged', true);
|
||
$r = $this->getJson($url)->assertOk()->assertJsonPath('saved.scope', 'dossier_reference')->assertJsonPath('note', 'SYNTHETIC private preparation')->json();
|
||
$this->assertSame(app(InvestmentGuides::class)->guide('capitalizacao'), $r['guide']);
|
||
$stored = DB::table('desk_cases')->where('id', $id)->first();
|
||
$this->assertStringNotContainsString('SYNTHETIC private preparation', $stored->payload);
|
||
$payload = app(VirtualDesk::class)->decode($stored->payload);
|
||
unset($payload['investment_guides']);
|
||
$this->assertSame($before, $payload);
|
||
$event = DB::table('desk_events')->where('case_id', $id)->where('action', InvestmentGuides::EVENT)->first();
|
||
$this->assertStringNotContainsString('SYNTHETIC private preparation', $event->payload);
|
||
$this->assertStringNotContainsString('SYNTHETIC private preparation', DB::table('audit_events')->where('entity_id', $id)->get()->toJson());
|
||
$this->assertSame($steps, DB::table('journey_steps')->orderBy('id')->get()->toJson());
|
||
$this->assertDatabaseCount('desk_requests', 0);
|
||
$this->assertDatabaseCount('investment_company_declarations', 0);
|
||
$this->assertSame(1, DB::table('desk_events')->where('action', InvestmentGuides::EVENT)->count());
|
||
$this->getJson($url.'?'.http_build_query(['current' => 1, 'selection' => 1, 'sha256' => $r['guide']['sha256']]))->assertOk()->assertJsonPath('instruments', [])->assertJsonPath('workingCopy', true)->assertJsonPath('saved.instruments', ['debt', 'private_equity']);
|
||
DB::table('desk_cases')->where('id', $id)->update(['closed_at' => now()]);
|
||
$this->postJson($url, $input)->assertOk()->assertJsonPath('replayed', true);
|
||
$this->postJson($url, $this->input($id))->assertConflict();
|
||
}
|
||
|
||
public function test_source_editions_and_notes_remain_recoverable_after_updates_or_withdrawal_and_replays_use_original_choices(): void
|
||
{
|
||
$id = $this->dossier();
|
||
$url = $this->url($id);
|
||
$input = $this->input($id);
|
||
$this->postJson($url, $input)->assertOk();
|
||
$first = $this->getJson($url)->json();
|
||
config(['investment-guides.capitalizacao.version' => 'SYNTHETIC future', 'investment-guides.capitalizacao.instruments.debt' => null]);
|
||
$this->postJson($url, $input)->assertOk()->assertJsonPath('replayed', true);
|
||
$this->getJson($url)->assertJsonPath('olderEdition', true)->assertJsonPath('guide', $first['guide']);
|
||
$this->postJson($url, [...$input, 'version' => $first['case']['version'], 'operation_key' => (string) Str::uuid()])->assertConflict();
|
||
config(['investment-guides.capitalizacao.instruments' => ['private_equity' => $first['guide']['instruments']['private_equity']]]);
|
||
$this->getJson($url.'?current=1')->assertOk()->assertJsonPath('unavailableInstruments', [$first['guide']['instruments']['debt']['title']])->assertJsonPath('note', $first['note']);
|
||
$this->postJson($url, $this->input($id, ['instruments' => ['private_equity'], 'note' => 'SYNTHETIC new edition']))->assertOk()->assertJsonPath('revision', 2);
|
||
$this->getJson($url.'?revision='.$first['history'][0]['id'])->assertOk()->assertJsonPath('guide', $first['guide'])->assertJsonPath('note', $first['note']);
|
||
config(['investment-guides.capitalizacao' => null]);
|
||
$this->getJson('/guias-investimento/capitalizacao')->assertNotFound();
|
||
$this->getJson($url)->assertOk()->assertJsonPath('currentAvailable', false)->assertJsonPath('note', 'SYNTHETIC new edition');
|
||
$this->getJson($url.'?current=1')->assertConflict();
|
||
$this->postJson($url, $input)->assertOk()->assertJsonPath('replayed', true);
|
||
}
|
||
|
||
public function test_pagination_preserves_every_revision_and_rejects_another_dossiers_history_identifier(): void
|
||
{
|
||
$id = $this->dossier();
|
||
$url = $this->url($id);
|
||
for ($i = 1; $i <= 26; $i++) {
|
||
$this->postJson($url, $this->input($id, ['note' => 'SYNTHETIC note '.$i]))->assertOk();
|
||
}
|
||
$r = $this->getJson($url)->assertJsonCount(25, 'history')->json();
|
||
$old = $this->getJson($url.'?history_before='.$r['historyNext'])->assertJsonCount(1, 'history')->json();
|
||
$revision = $old['history'][0]['id'];
|
||
$this->getJson($url.'?revision='.$revision)->assertOk()->assertJsonPath('saved.revision', 1)->assertJsonPath('historical', true);
|
||
$this->getJson($url.'?revision='.$revision.'¤t=1')->assertUnprocessable();
|
||
$other = $this->dossier();
|
||
$this->getJson($this->url($other).'?revision='.$revision)->assertNotFound();
|
||
}
|
||
|
||
public function test_context_changes_are_explicit_without_rewriting_the_previous_reference(): void
|
||
{
|
||
$id = $this->dossier();
|
||
$url = $this->url($id);
|
||
$this->postJson($url, $this->input($id))->assertOk();
|
||
$r = $this->getJson($url)->assertJsonPath('contextChanged', false)->json();
|
||
$case = DB::table('desk_cases')->where('id', $id)->first();
|
||
$payload = app(VirtualDesk::class)->decode($case->payload);
|
||
$payload['investor_path']['context'] = app(InvestorContext::class)->state($case);
|
||
$payload['investor_path']['context']['revision'] = 99;
|
||
$payload['investor_path']['context']['fields']['project_title'] = 'SYNTHETIC changed scope';
|
||
DB::table('desk_cases')->where('id', $id)->update(['payload' => app(VirtualDesk::class)->encode($payload)]);
|
||
$this->getJson($url)->assertJsonPath('contextChanged', true)->assertJsonPath('saved.context', $r['saved']['context']);
|
||
}
|
||
|
||
public function test_revocation_and_read_only_membership_are_effective_before_replay_or_validation(): void
|
||
{
|
||
$id = $this->dossier();
|
||
$url = $this->url($id);
|
||
$org = (string) Str::uuid();
|
||
DB::table('organizations')->insert(['id' => $org, 'name' => 'SYNTHETIC financing organization', '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);
|
||
$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->assertSame(1, DB::table('desk_events')->where('action', InvestmentGuides::EVENT)->count());
|
||
}
|
||
}
|