Files
Investidor-V2/tests/Feature/InvestmentUnificationTest.php
T
nala 250ac35fd1
tests / ci (push) Canceled after 0s
first commit
2026-09-28 10:01:09 +02:00

199 lines
13 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\InterestRequest;
use App\Models\InvestmentDossier;
use App\Models\InvestmentSubmission;
use App\Models\Opportunity;
use App\Models\Sector;
use App\Models\User;
use Database\Seeders\InvestmentReferenceSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class InvestmentUnificationTest extends TestCase
{
use RefreshDatabase;
private function option(string $group, string $code): int
{
return DB::table('investment_reference_options')->where('group', $group)->where('code', $code)->value('id');
}
private function opportunity(): Opportunity
{
$sector = Sector::firstOrCreate(['slug' => 'test-sector'], ['name' => 'Sector sintético', 'is_active' => true]);
return Opportunity::create(['sector_id' => $sector->id, 'reference' => (string) Str::uuid(), 'slug' => (string) Str::uuid(),
'title' => 'Oportunidade sintética', 'status' => 'published', 'published_at' => now()->subMinute(), 'province' => 'Gaza',
'estimated_investment_amount' => 1000, 'estimated_investment_currency' => 'USD']);
}
private function payload(int $version = 1): array
{
return ['version' => $version, 'payload' => ['full_name' => 'Pessoa de teste', 'email' => 'test@example.com', 'country' => 'Moçambique',
'profile_id' => $this->option('investor_profile', 'individual'), 'sector' => 'Educação',
'objective' => 'Criar uma oficina de formação exclusivamente sintética.', 'participation_id' => $this->option('participation', 'invest'),
'horizon' => '12 meses', 'contact_consent' => true], 'needs' => [['category_id' => $this->option('need_category', 'education'),
'option_id' => $this->option('education', 'laboratory'), 'quantity' => 2, 'unit_id' => $this->option('unit', 'units'),
'priority_id' => $this->option('priority', 'normal'), 'details' => ['description' => 'Laboratórios sintéticos para formação']]]];
}
public function test_submission_is_versioned_encrypted_private_and_idempotently_integrated(): void
{
$this->seed(InvestmentReferenceSeeder::class);
$user = User::factory()->create();
$opportunity = $this->opportunity();
$id = $this->actingAs($user)->postJson('/api/v1/investment/submissions', ['opportunity_id' => $opportunity->id])->assertCreated()->json('id');
$this->getJson('/api/v1/investment/submissions/'.$id)->assertJsonPath('submission.payload.sector', 'Sector sintético')->assertJsonPath('submission.payload.location', 'Gaza');
$this->putJson('/api/v1/investment/submissions/'.$id, $this->payload())->assertOk()->assertJsonPath('version', 2);
$this->putJson('/api/v1/investment/submissions/'.$id, $this->payload())->assertConflict();
$this->assertStringNotContainsString('test@example.com', DB::table('investment_submissions')->where('id', $id)->value('payload'));
$this->assertStringNotContainsString('Laboratórios', DB::table('investment_specific_needs')->value('details'));
$this->actingAs(User::factory()->create())->getJson('/api/v1/investment/submissions/'.$id)->assertNotFound();
$this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $this->payload(2))->assertNotFound();
$this->actingAs($user)->postJson('/api/v1/investment/submissions/'.$id.'/submit', $this->payload(2))->assertOk();
$this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $this->payload(2))->assertOk();
$this->assertDatabaseCount('interest_requests', 1);
$this->assertDatabaseCount('outbox_events', 1);
$this->assertSame($id, InterestRequest::firstOrFail()->metadata['submission_id']);
$this->getJson('/api/v1/investment/submissions/'.$id)->assertHeader('Cache-Control', 'no-store, private');
InterestRequest::firstOrFail()->update(['status' => 'in_review']);
$this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $this->payload(2))->assertJsonPath('status', 'in_review');
$this->get('/balcao-virtual')->assertInertia(fn (Assert $page) => $page->has('requests', 1));
$this->assertSame(3, DB::table('investment_submission_events')->where('submission_id', $id)->count());
$this->putJson('/api/v1/investment/submissions/'.$id, $this->payload(3))->assertConflict();
}
public function test_invalid_reference_group_consent_and_unpublished_target_are_rejected(): void
{
$this->seed(InvestmentReferenceSeeder::class);
$user = User::factory()->create();
$opportunity = $this->opportunity();
$id = $this->actingAs($user)->postJson('/api/v1/investment/submissions', ['opportunity_id' => $opportunity->id])->json('id');
$data = $this->payload();
$data['needs'][0]['option_id'] = $this->option('infrastructure', 'energy');
$this->putJson('/api/v1/investment/submissions/'.$id, $data)->assertUnprocessable();
$data = $this->payload();
$data['payload']['contact_consent'] = false;
$this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $data)->assertUnprocessable();
$data = $this->payload();
$data['payload']['participation_id'] = $this->option('participation', 'other');
$this->putJson('/api/v1/investment/submissions/'.$id, $data)->assertUnprocessable();
$opportunity->update(['status' => 'draft']);
$this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $this->payload())->assertNotFound();
$this->assertDatabaseCount('interest_requests', 0);
}
public function test_attachments_are_private_and_content_changes_are_blocked(): void
{
Storage::fake('local');
$user = User::factory()->create();
$id = $this->actingAs($user)->postJson('/api/v1/investment/submissions')->assertCreated()->json('id');
$this->post('/investidor/manifestacoes/'.$id.'/attachments', ['version' => '1',
'file' => UploadedFile::fake()->createWithContent('nota.txt', 'Anexo privado de teste')])->assertSessionHasNoErrors()->assertRedirect();
$attachment = DB::table('investment_submission_attachments')->first();
$document = DB::table('documents')->where('id', $attachment->document_id)->first();
$url = '/investidor/manifestacoes/'.$id.'/attachments/'.$attachment->id;
$this->get($url)->assertDownload('nota.txt');
$this->assertSame('confidential', $document->data_classification);
$this->actingAs(User::factory()->create())->get($url)->assertNotFound();
Storage::disk('local')->put($document->storage_key, 'Alteração');
$this->actingAs($user)->get($url)->assertNotFound();
$this->assertSame(2, InvestmentSubmission::findOrFail($id)->version);
}
public function test_context_editing_combined_facets_and_education_discovery(): void
{
$this->seed(InvestmentReferenceSeeder::class);
$manager = User::factory()->create();
config(['portal.managers' => [$manager->email]]);
$opportunity = $this->opportunity();
$facet = $this->option('education', 'laboratory');
$data = ['version' => 0, 'is_education' => true, 'facets' => [$facet], 'source_reference' => 'Fonte exclusivamente sintética'];
$url = '/api/v1/investment/admin/opportunities/'.$opportunity->id.'/context';
$this->actingAs($manager)->putJson($url, $data)->assertOk()->assertJsonPath('version', 1);
$this->assertSame('draft', $opportunity->fresh()->status);
$this->putJson($url, $data)->assertConflict();
$this->getJson('/api/v1/investment/search?education=1')->assertJsonPath('total', 0);
$opportunity->refresh()->update(['status' => 'published', 'published_at' => now()]);
$this->getJson('/api/v1/investment/search?education=1&province=Gaza&facets[]='.$facet)->assertOk()->assertJsonPath('total', 1);
$this->getJson('/api/v1/investment/search?education=1&province=Maputo')->assertJsonPath('total', 0);
$this->get('/educacao')->assertInertia(fn (Assert $page) => $page->where('filters.education', true)->has('results.data', 1));
$this->actingAs(User::factory()->create())->putJson($url, [...$data, 'version' => 1])->assertForbidden();
}
public function test_map_discloses_only_public_coordinates_of_published_opportunities(): void
{
$opportunity = $this->opportunity();
$id = (string) Str::uuid();
DB::table('geo_locations')->insert(['id' => $id, 'code' => $id, 'name' => 'Local de teste', 'location_type' => 'site', 'data_classification' => 'public']);
DB::table('opportunity_locations')->insert(['opportunity_id' => $opportunity->id, 'geo_location_id' => $id]);
DB::statement("INSERT INTO geo_geometries(geo_location_id,geometry_type,geom,data_classification) VALUES(?, 'point', ST_SetSRID(ST_MakePoint(32,-25),4326), 'public')", [$id]);
$this->getJson('/api/v1/investment/map')->assertOk()->assertJsonCount(1, 'features');
DB::table('geo_geometries')->where('geo_location_id', $id)->update(['data_classification' => 'restricted']);
$this->getJson('/api/v1/investment/map')->assertJsonCount(0, 'features');
DB::table('geo_geometries')->where('geo_location_id', $id)->update(['data_classification' => 'public']);
$opportunity->update(['status' => 'draft']);
$this->getJson('/api/v1/investment/map')->assertJsonCount(0, 'features');
}
public function test_territorial_filters_intersect_and_hide_withdrawn_relations(): void
{
$this->artisan('portfolio:import', ['--approval' => 'Autorização sintética exclusivamente para teste.'])->assertSuccessful();
$clusters = InvestmentDossier::whereNotNull('cluster_id')->limit(2)->get();
$corridor = InvestmentDossier::whereNotNull('corridor_id')->firstOrFail();
$opportunity = $this->opportunity();
foreach ($clusters as $cluster) {
DB::table('opportunity_clusters')->insert(['opportunity_id' => $opportunity->id, 'cluster_id' => $cluster->cluster_id]);
}
DB::table('opportunity_corridors')->insert(['opportunity_id' => $opportunity->id, 'corridor_id' => $corridor->corridor_id]);
$url = '/api/v1/investment/search?cluster_id='.$clusters[0]->cluster_id.'&corridor_id='.$corridor->corridor_id;
$this->getJson($url)->assertOk()->assertJsonPath('total', 1);
$this->getJson($url.'&province=Maputo')->assertJsonPath('total', 0);
$this->getJson('/api/v1/investment/search?cluster_id='.$clusters[1]->cluster_id)->assertJsonPath('total', 1);
$clusters[0]->update(['publication_status' => 'draft']);
$this->getJson($url)->assertJsonPath('total', 0);
$this->getJson('/api/v1/investment/opportunities/'.$opportunity->slug)->assertJsonCount(1, 'context.clusters');
$this->getJson('/api/v1/investment/references')->assertJsonMissing(['id' => $clusters[0]->cluster_id]);
}
public function test_favourites_comparison_and_csv_respect_publication_and_ownership(): void
{
$user = User::factory()->create();
$opportunity = $this->opportunity();
$opportunity->update(['title' => '=FORMULA()']);
$this->actingAs($user)->postJson('/api/v1/investment/favourites/'.$opportunity->id, ['saved' => true])->assertOk();
$this->postJson('/api/v1/investment/favourites/'.$opportunity->id, ['saved' => true])->assertOk();
$this->assertDatabaseCount('opportunity_favourites', 1);
$this->getJson('/api/v1/investment/compare?ids[]='.$opportunity->id)->assertOk()->assertJsonCount(1, 'records');
$this->assertStringContainsString("'=FORMULA()", $this->get('/api/v1/investment/export')->assertOk()->streamedContent());
$this->actingAs(User::factory()->create())->getJson('/api/v1/investment/submissions')->assertJsonCount(0, 'favourites');
$opportunity->update(['status' => 'draft']);
$this->getJson('/api/v1/investment/compare?ids[]='.$opportunity->id)->assertNotFound();
$this->postJson('/api/v1/investment/favourites/'.$opportunity->id, ['saved' => true])->assertNotFound();
}
public function test_reference_updates_preserve_codes_and_other_option(): void
{
$this->seed(InvestmentReferenceSeeder::class);
$user = User::factory()->create();
config(['portal.managers' => [$user->email]]);
$id = $this->option('education', 'other');
$data = ['group' => 'education', 'code' => 'other', 'label' => 'Outro — detalhar', 'active' => true, 'requires_description' => true, 'sort_order' => 1];
$this->actingAs($user)->postJson('/api/v1/investment/admin/references/'.$id, $data)->assertOk();
$this->postJson('/api/v1/investment/admin/references/'.$id, [...$data, 'active' => false])->assertUnprocessable();
$this->postJson('/api/v1/investment/admin/references/'.$id, [...$data, 'code' => 'changed'])->assertUnprocessable();
$count = DB::table('investment_reference_options')->count();
$this->seed(InvestmentReferenceSeeder::class);
$this->assertSame($count, DB::table('investment_reference_options')->count());
$this->assertSame('Outro — detalhar', DB::table('investment_reference_options')->where('id', $id)->value('label'));
}
}