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

94 lines
7.5 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Opportunity;
use App\Models\Project;
use App\Models\Sector;
use App\Models\User;
use App\Services\OpportunityManagement;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
class OpportunityManagementTest extends TestCase
{
use RefreshDatabase;
public function test_installation_requires_management_permission_and_current_proofs(): void
{
$this->actingAs(User::factory()->create())->getJson('/management/opportunity-management-install')->assertForbidden();
$manager = User::factory()->create();
config(['portal.managers' => [$manager->email]]);
$migration = require database_path('migrations/2026_10_09_020000_extend_opportunity_management.php');
$migration->down();
$preview = $this->actingAs($manager)->getJson('/management/opportunity-management-install')->assertOk()->assertJsonPath('installed', false)->assertJsonPath('ready', false)->json();
$this->postJson('/management/opportunity-management-install', ['hash' => $preview['hash'], 'confirm' => true])->assertUnprocessable();
$this->assertFalse(app(OpportunityManagement::class)->installed());
}
public function test_incomplete_draft_recovers_and_soft_delete_restore_preserves_identity(): void
{
$manager = User::factory()->create();
config(['portal.managers' => [$manager->email]]);
$payload = ['creation_key' => (string) Str::uuid(), 'reason' => 'Criação de rascunho incompleto em QA'];
$result = $this->actingAs($manager)->postJson('/management/opportunities/drafts', $payload)->assertCreated()->json();
$id = $result['id'];
$this->assertDatabaseHas('opportunities', ['id' => $id, 'title' => null, 'sector_id' => null, 'estimated_investment_currency' => null, 'status' => 'draft']);
$this->postJson('/management/opportunities/drafts', $payload)->assertCreated()->assertJsonPath('id', $id);
$this->assertSame(1, Opportunity::count());
$this->postJson('/management/opportunities/drafts', [...$payload, 'title' => 'Different data'])->assertUnprocessable();
$this->get('/management/opportunities/'.$id.'/edit')->assertOk();
$update = $this->putJson('/management/opportunities/'.$id.'/draft', ['etag' => $result['etag'], 'title' => 'QA incomplete saved title', 'reason' => 'Edição de identificação incompleta QA'])->assertOk()->json();
$this->putJson('/management/opportunities/'.$id.'/draft', ['etag' => $result['etag'], 'title' => 'Lost update', 'reason' => 'Tentativa concorrente para teste QA'])->assertConflict()->assertJsonValidationErrors('etag');
$this->assertDatabaseHas('opportunities', ['id' => $id, 'title' => 'QA incomplete saved title']);
$request = ['etag' => $update['etag'], 'action' => 'delete', 'reason' => 'Exclusão lógica autorizada apenas em QA', 'confirm' => true];
$this->postJson('/management/opportunities/'.$id.'/lifecycle', [...$request, 'confirm' => false])->assertUnprocessable();
$this->postJson('/management/opportunities/'.$id.'/lifecycle', $request)->assertOk();
$this->assertSoftDeleted('opportunities', ['id' => $id]);
$this->get('/management/opportunities/'.$id.'/edit')->assertOk();
$record = Opportunity::withTrashed()->findOrFail($id);
$this->postJson('/management/opportunities/'.$id.'/lifecycle', [...$request, 'action' => 'restore', 'etag' => app(OpportunityManagement::class)->etag($record)])->assertOk();
$this->assertDatabaseHas('opportunities', ['id' => $id, 'status' => 'draft', 'deleted_at' => null]);
$this->assertSame(4, DB::table('audit_events')->where('source_system', 'opportunity-management')->count());
$this->actingAs(User::factory()->create())->get('/management/opportunities')->assertForbidden();
$this->postJson('/management/opportunities/drafts', $payload)->assertForbidden();
}
public function test_closed_version_edits_do_not_change_public_values_classification_or_relations(): void
{
$manager = User::factory()->create();
config(['portal.managers' => [$manager->email]]);
$sector = Sector::create(['name' => 'QA sector', 'slug' => 'qa-management', 'is_active' => true]);
$opportunity = Opportunity::create(['sector_id' => $sector->id, 'reference' => 'INV-MZ-QA-001', 'title' => 'Published QA original', 'promoter_type' => 'privado', 'slug' => 'qa-management', 'status' => 'published', 'published_at' => now()->subMinute()]);
$project = Project::factory()->create();
DB::table('opportunity_projects')->insert(['opportunity_id' => $opportunity->id, 'project_id' => $project->id]);
DB::table('audit_events')->insert(['source_system' => 'opportunity-origin-review', 'entity_type' => 'opportunity_origin', 'action' => 'origin_classification_decided', 'actor_user_id' => $manager->id, 'after_data' => json_encode(['opportunity_id' => $opportunity->id, 'classification' => 'real', 'reason' => 'QA classification retained'])]);
$service = app(OpportunityManagement::class);
$update = $this->actingAs($manager)->putJson('/management/opportunities/'.$opportunity->id.'/draft', ['etag' => $service->etag($opportunity->fresh()), 'promoter_type' => 'privado', 'title' => 'QA new unpublished version', 'reason' => 'Nova versão em rascunho para teste QA'])->assertOk()->json();
$this->assertSame('Published QA original', $opportunity->fresh()->title);
$this->assertSame('QA new unpublished version', json_decode($opportunity->fresh()->management_draft, true)['title']);
$this->getJson('/api/v1/opportunities/qa-management')->assertOk()->assertJsonMissingPath('data.management_draft')->assertJsonPath('data.title', 'Published QA original');
$this->postJson('/management/opportunities/'.$opportunity->id.'/lifecycle', ['etag' => $update['etag'], 'action' => 'delete', 'reason' => 'Arquivo explícito de versão submetida QA', 'confirm' => true])->assertOk();
$this->assertDatabaseHas('opportunities', ['id' => $opportunity->id, 'status' => 'archived', 'deleted_at' => null]);
$this->assertTrue((bool) $opportunity->fresh()->management_closed_once);
$this->assertNull($opportunity->fresh()->first_submitted_at);
$this->assertDatabaseHas('opportunity_projects', ['opportunity_id' => $opportunity->id, 'project_id' => $project->id]);
$this->assertSame(1, DB::table('audit_events')->where('source_system', 'opportunity-origin-review')->count());
$this->putJson('/management/opportunities/'.$opportunity->id, ['status' => 'published'])->assertUnprocessable();
$this->assertSame(0, Opportunity::published()->count());
$this->postJson('/management/opportunities/'.$opportunity->id.'/lifecycle', ['etag' => $service->etag($opportunity->fresh()), 'action' => 'restore', 'reason' => 'Reabertura autorizada com revisão somente QA', 'confirm' => true])->assertOk();
$this->assertSame('draft', $opportunity->fresh()->status);
$this->assertSame(0, Opportunity::published()->count());
try {
DB::transaction(fn () => DB::table('opportunities')->where('id', $opportunity->id)->delete());
$this->fail('Physical deletion after a closed version was accepted');
} catch (QueryException $error) {
$this->assertStringContainsString('Physical deletion forbidden', $error->getMessage());
}
$this->assertDatabaseHas('opportunity_projects', ['opportunity_id' => $opportunity->id, 'project_id' => $project->id]);
}
}