199 lines
13 KiB
PHP
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'));
|
|
}
|
|
}
|