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]); } }