Files
Investidor-V2/tests/Feature/InvestmentPortfolioTest.php
nala 6ddafc95ba
tests / ci (push) Canceled after 0s
oportunidades 4
2026-10-10 07:03:59 +02:00

230 lines
15 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\InterestRequest;
use App\Models\InvestmentDossier;
use App\Models\InvestorJourney;
use App\Models\User;
use App\Services\ConvertInterestToLead;
use Database\Seeders\InvestmentPortfolioSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class InvestmentPortfolioTest extends TestCase
{
use RefreshDatabase;
public function test_document_import_is_idempotent_preserves_edits_and_links_all_anchors(): void
{
$this->seed(InvestmentPortfolioSeeder::class);
$dossier = InvestmentDossier::firstOrFail();
$dossier->update(['version_no' => $dossier->version_no, 'title' => 'Revisão humana preservada']);
$this->seed(InvestmentPortfolioSeeder::class);
$this->assertDatabaseCount('projects', 15);
$this->assertDatabaseCount('economic_clusters', 8);
$this->assertDatabaseCount('development_corridors', 6);
$this->assertDatabaseCount('investment_dossiers', 29);
$this->assertSame('Revisão humana preservada', $dossier->fresh()->title);
$this->assertSame(15, DB::table('corridor_projects')->distinct()->count('project_id'));
$this->assertDatabaseHas('cluster_projects', ['role' => 'anchor']);
$this->get('/carteira')->assertInertia(fn (Assert $page) => $page->component('portfolio/index')->has('records.data', 0));
$this->get('/carteira/'.$dossier->id)->assertNotFound();
}
public function test_stale_editor_cannot_overwrite_or_publish_a_newer_version(): void
{
$this->seed(InvestmentPortfolioSeeder::class);
$manager = User::factory()->create();
$reviewer = User::factory()->create();
config(['portal.managers' => [$manager->email, $reviewer->email]]);
$dossier = InvestmentDossier::firstOrFail();
$url = '/management/portfolio/'.$dossier->id;
$payload = ['version_no' => $dossier->version_no, 'title' => 'Revisao mais recente', 'source' => $dossier->source, 'content' => $dossier->content];
$this->actingAs($manager)->put($url, $payload)->assertRedirect()->assertSessionHasNoErrors();
$this->putJson($url, [...$payload, 'title' => 'Alteracao obsoleta'])->assertStatus(409);
$approval = ['version_no' => $dossier->version_no, 'approval_evidence' => 'Aprovacao sintetica da versao revista'];
$this->postJson($url.'/publish', $approval)->assertStatus(409);
$this->assertSame('Revisao mais recente', $dossier->fresh()->title);
$this->assertSame('draft', $dossier->fresh()->publication_status);
$this->assertSame(2, $dossier->fresh()->version_no);
$this->post($url.'/publish', [...$approval, 'version_no' => 2])->assertForbidden();
$this->actingAs($reviewer)->post($url.'/publish', [...$approval, 'version_no' => 2])->assertRedirect()->assertSessionHasNoErrors();
$this->assertSame(3, $dossier->fresh()->version_no);
$this->assertSame('published', $dossier->fresh()->publication_status);
$this->postJson($url.'/publish', [...$approval, 'version_no' => 2])->assertStatus(409);
$this->putJson($url, array_diff_key($payload, ['version_no' => true]))->assertUnprocessable()->assertJsonValidationErrors('version_no');
$this->get('/carteira/'.$dossier->id)->assertInertia(fn (Assert $page) => $page->missing('record.version_no')->missing('record.approval_evidence'));
$audit = DB::table('audit_events')->where('entity_id', $dossier->id)->orderByDesc('created_at')->get();
$this->assertCount(2, $audit);
$this->assertStringNotContainsString('Aprovacao sintetica', json_encode($audit));
}
private function publish(): InvestmentDossier
{
$this->seed(InvestmentPortfolioSeeder::class);
$editor = User::factory()->create();
$reviewer = User::factory()->create();
config(['portal.managers' => [$editor->email, $reviewer->email]]);
$dossier = InvestmentDossier::whereNotNull('project_id')->firstOrFail();
$this->actingAs($editor)->put('/management/portfolio/'.$dossier->id,
['version_no' => $dossier->version_no, 'title' => $dossier->title, 'source' => $dossier->source, 'content' => $dossier->content])->assertSessionHasNoErrors();
$evidence = ['approval_evidence' => 'Autorização fictícia apenas para teste'];
$this->post('/management/portfolio/'.$dossier->id.'/publish', [...$evidence, 'version_no' => $dossier->fresh()->version_no])->assertForbidden();
$this->actingAs($reviewer)->post('/management/portfolio/'.$dossier->id.'/publish', [...$evidence, 'version_no' => $dossier->fresh()->version_no])->assertSessionHasNoErrors()->assertRedirect();
$this->assertSame($reviewer->id, $dossier->fresh()->reviewer_id);
return $dossier->fresh();
}
public function test_authorized_manager_can_publish_and_internal_evidence_is_hidden(): void
{
$dossier = $this->publish();
$this->get('/carteira/'.$dossier->id)->assertInertia(fn (Assert $page) => $page->component('portfolio/show')
->where('record.title', $dossier->title)->missing('record.approval_evidence')->missing('record.editor_id')->has('related', 0));
$this->put('/management/portfolio/'.$dossier->id, ['version_no' => $dossier->version_no, 'title' => 'Conteúdo alterado', 'source' => $dossier->source, 'content' => $dossier->content])->assertSessionHasNoErrors();
$this->get('/carteira/'.$dossier->id)->assertNotFound();
$this->actingAs(User::factory()->create())->get('/management/portfolio')->assertForbidden();
$this->post('/management/portfolio/'.$dossier->id.'/publish', ['approval_evidence' => 'Tentativa sem privilégios de gestão'])->assertForbidden();
}
public function test_import_keeps_documentary_dossiers_private_and_preserves_later_edits(): void
{
$approval = 'Aprovação expressa do proprietário para executar a carteira v4.';
$this->artisan('portfolio:import')->assertSuccessful();
$this->assertSame(0, InvestmentDossier::where('publication_status', 'published')->count());
$this->assertSame(29, InvestmentDossier::where('publication_status', 'draft')->count());
$this->assertSame(0, DB::table('audit_events')->where('action', 'owner_approved')->count());
$dossier = InvestmentDossier::firstOrFail();
$manager = User::factory()->create();
config(['portal.managers' => [$manager->email]]);
$this->actingAs($manager)->post('/management/portfolio/'.$dossier->id.'/publish', [
'version_no' => $dossier->version_no, 'approval_evidence' => 'Synthetic owner approval is not an editor assignment',
])->assertStatus(422);
$this->get('/carteira/'.$dossier->id)->assertNotFound();
$this->artisan('portfolio:import')->assertSuccessful();
$this->assertSame(0, DB::table('audit_events')->where('action', 'owner_approved')->count());
$editor = User::factory()->create();
config(['portal.managers' => [$editor->email]]);
$this->actingAs($editor)->put('/management/portfolio/'.$dossier->id, [
'version_no' => $dossier->version_no, 'title' => 'Alteração posterior', 'source' => $dossier->source, 'content' => $dossier->content,
])->assertSessionHasNoErrors();
$this->artisan('portfolio:import')->assertSuccessful();
$this->assertSame('draft', $dossier->fresh()->publication_status);
$this->assertSame('Alteração posterior', $dossier->fresh()->title);
$this->get('/carteira/'.$dossier->id)->assertNotFound();
}
public function test_personal_journey_is_isolated_and_retries_do_not_duplicate_interest_or_event(): void
{
$dossier = $this->publish();
$owner = User::factory()->create();
$other = User::factory()->create();
$this->actingAs($owner)->post('/investidor/guardar/'.$dossier->id)->assertRedirect('/investidor');
$this->post('/investidor/guardar/'.$dossier->id)->assertRedirect();
$this->assertDatabaseCount('investor_journeys', 1);
$journey = InvestorJourney::firstOrFail();
$payload = ['message' => 'Pretendo investir neste projecto de teste.', 'country' => 'Moçambique', 'privacy_consent' => true, 'role' => 'investir', 'horizon' => '12 meses', 'declared_origin_country_code' => 'PT'];
$url = '/investidor/'.$journey->id.'/interesse';
$this->postJson($url, [...$payload, 'declared_origin_country_code' => 'ZZ'])->assertUnprocessable();
$this->assertDatabaseCount('interest_requests', 0);
$this->post($url, [...$payload, 'privacy_consent' => false])->assertSessionHasErrors('privacy_consent');
$this->actingAs($other)->post($url, $payload)->assertNotFound();
$this->get('/investidor')->assertInertia(fn (Assert $page) => $page->has('journeys', 0));
$this->actingAs($owner)->post($url, $payload)->assertSessionHasNoErrors()->assertRedirect();
$this->post($url, $payload)->assertSessionHasNoErrors();
$this->assertDatabaseCount('interest_requests', 1);
$this->assertDatabaseCount('outbox_events', 1);
$this->assertDatabaseHas('investor_journeys', ['id' => $journey->id, 'status' => 'active']);
$metadata = json_decode(DB::table('interest_requests')->value('metadata'), true);
$this->assertSame($journey->id, $metadata['journey_id']);
$this->assertSame('PT', $metadata['declared_origin_country_code']);
$this->assertSame($dossier->project_id, $metadata['project_id']);
$this->get('/investidor')->assertInertia(fn (Assert $page) => $page->has('journeys', 1)->where('journeys.0.request_status', 'received')
->where('journeys.0.country', 'Moçambique')->where('journeys.0.declared_origin.code', 'PT'));
$conversion = ['owner_user_id' => $owner->id, 'title' => 'Interesse da jornada', 'next_action' => 'Contactar investidor', 'due_at' => now()->addDay()->toIso8601String()];
$interest = InterestRequest::firstOrFail();
try {
app(ConvertInterestToLead::class)->convert($interest, $conversion, $owner->id);
$this->fail('A conversão deve preservar o investidor da jornada.');
} catch (ValidationException $exception) {
$this->assertArrayHasKey('investor_id', $exception->errors());
}
$lead = app(ConvertInterestToLead::class)->convert($interest, [...$conversion, 'investor_id' => $journey->investor_id], $owner->id);
$this->assertSame($journey->investor_id, $lead->investor_id);
$this->assertDatabaseCount('investors', 1);
config(['portal.managers' => [$owner->email]]);
$this->get('/management/interests?origin=PT')->assertInertia(fn (Assert $page) => $page
->where('requests.total', 1)->where('requests.data.0.declaredOrigin.code', 'PT')
->where('originStatistics.interests.0.code', 'PT')->where('originStatistics.interests.0.count', 1));
$this->get('/management/interests?origin=MZ')->assertInertia(fn (Assert $page) => $page->where('requests.total', 0)->has('originStatistics.interests', 0));
}
public function test_private_manifestation_link_requires_authentication_and_preserves_the_public_dossier(): void
{
$dossier = $this->publish();
$this->app['auth']->forgetGuards();
$dossierId = $dossier->id;
$owner = User::factory()->create();
$other = User::factory()->create();
$this->postJson('/api/v1/investment/submissions', ['dossier_id' => $dossierId])->assertUnauthorized();
$this->assertDatabaseCount('investment_submissions', 0);
$this->actingAs($owner)->get('/carteira/'.$dossierId)
->assertInertia(fn (Assert $page) => $page->component('portfolio/show')->has('existingSubmissions', 0));
$submissionId = $this->postJson('/api/v1/investment/submissions', ['dossier_id' => $dossierId])
->assertCreated()->json('id');
$this->assertDatabaseHas('investment_submissions', [
'id' => $submissionId,
'user_id' => $owner->id,
'dossier_id' => $dossierId,
'status' => 'draft',
]);
$this->assertSame('published', $dossier->fresh()->publication_status);
$this->actingAs($owner)->get('/carteira/'.$dossierId)
->assertInertia(fn (Assert $page) => $page->has('existingSubmissions', 1)->where('existingSubmissions.0.id', $submissionId));
$this->actingAs($other)->get('/investidor/manifestacoes/'.$submissionId)->assertNotFound();
$this->assertDatabaseCount('investment_submissions', 1);
$this->assertDatabaseCount('investment_dossiers', 29);
}
public function test_unpublished_and_deleted_entities_cannot_start_journeys(): void
{
$dossier = $this->publish();
DB::table('projects')->where('id', $dossier->project_id)->update(['deleted_at' => now()]);
$this->get('/carteira/'.$dossier->id)->assertNotFound();
$this->post('/investidor/guardar/'.$dossier->id)->assertNotFound();
$this->assertDatabaseCount('investor_journeys', 0);
}
public function test_origin_context_is_validated_and_preserved_in_the_interest(): void
{
$project = $this->publish();
$corridorId = DB::table('corridor_projects')->where('project_id', $project->project_id)->value('corridor_id');
$origin = InvestmentDossier::where('corridor_id', $corridorId)->firstOrFail();
$origin->update(['editor_id' => $project->editor_id, 'reviewer_id' => $project->reviewer_id,
'publication_status' => 'published', 'published_at' => now(), 'approval_evidence' => 'Teste de contexto autorizado']);
$this->actingAs(User::factory()->create())->post('/investidor/guardar/'.$project->id, ['origin_id' => $origin->id])->assertStatus(422);
DB::table('corridor_projects')->where('project_id', $project->project_id)->where('corridor_id', $corridorId)->update([
'validation_status' => 'validated', 'source_reference' => 'Fixture de teste com fonte identificada',
'evidence_reference' => 'fixture:test:association', 'valid_from' => today()->toDateString(),
]);
$other = InvestmentDossier::whereNotNull('project_id')->where('id', '!=', $project->id)->firstOrFail();
$other->update(['editor_id' => $project->editor_id, 'reviewer_id' => $project->reviewer_id,
'publication_status' => 'published', 'published_at' => now(), 'approval_evidence' => 'Teste de contexto autorizado']);
$this->actingAs(User::factory()->create())->post('/investidor/guardar/'.$project->id, ['origin_id' => $other->id])->assertStatus(422);
$this->post('/investidor/guardar/'.$project->id, ['origin_id' => $origin->id])->assertRedirect();
$journey = InvestorJourney::firstOrFail();
$this->post('/investidor/'.$journey->id.'/interesse', ['message' => 'Participar no corredor através deste projecto.',
'country' => 'Moçambique', 'privacy_consent' => true, 'role' => 'financiar', 'horizon' => '24 meses'])->assertSessionHasNoErrors();
$metadata = json_decode(DB::table('interest_requests')->value('metadata'), true);
$this->assertSame($origin->corridor_id, $metadata['corridor_id']);
$this->assertSame($project->project_id, $metadata['project_id']);
}
}