94 lines
7.5 KiB
PHP
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]);
|
|
}
|
|
}
|