directory = storage_path('framework/testing/origin-'.Str::uuid()); mkdir($this->directory, 0700, true); file_put_contents($this->directory.'/dump', 'QA-only backup fixture'); config(['opportunity_origin.proof_path' => $this->directory.'/proof.json']); file_put_contents($this->directory.'/proof.json', json_encode(['restore_verified' => true, 'reversal_verified' => true, 'code_hash' => app(OpportunityOriginReview::class)->fingerprint(), 'verified_at' => gmdate('c'), 'target' => ['database' => config('database.connections.pgsql.database'), 'port' => 55441], 'dump_path' => $this->directory.'/dump', 'sha256' => hash_file('sha256', $this->directory.'/dump')])); } protected function tearDown(): void { foreach (glob($this->directory.'/*') ?: [] as $file) { unlink($file); } rmdir($this->directory); parent::tearDown(); } private function fixture(): array { $sector = Sector::create(['name' => 'QA sector', 'slug' => 'qa-origin', 'is_active' => true]); $opportunity = Opportunity::create(['sector_id' => $sector->id, 'reference' => 'INV-MZ-AGR-001', 'title' => 'Synthetic QA opportunity', 'description' => 'Synthetic QA fixture only', 'slug' => 'qa-origin', 'status' => 'published', 'published_at' => now()->subMinute()]); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $project = Project::factory()->create(); DB::table('opportunity_projects')->insert(['opportunity_id' => $opportunity->id, 'project_id' => $project->id]); DB::table('opportunity_favourites')->insert(['user_id' => $manager->id, 'opportunity_id' => $opportunity->id]); return compact('opportunity', 'manager', 'project'); } public function test_preview_has_no_decision_and_example_separation_preserves_records_and_interactions(): void { $f = $this->fixture(); $before = $f['opportunity']->fresh()->getRawOriginal(); $payload = ['id' => $f['opportunity']->id, 'classification' => 'example', 'reason' => 'Responsible QA declaration: synthetic fixture']; $preview = $this->actingAs($f['manager'])->postJson('/management/opportunity-origin/preview', $payload)->assertOk()->assertJsonPath('relationships_changed', false)->json(); $this->assertSame(0, DB::table('audit_events')->where('source_system', 'opportunity-origin-review')->count()); $this->assertSame('undetermined', app(OpportunityOriginReview::class)->state($f['opportunity']->id)['classification']); $apply = [...$payload, 'snapshot_hash' => $preview['current']['snapshot_hash'], 'confirm' => true]; $this->postJson('/management/opportunity-origin/apply', $apply)->assertOk()->assertJsonPath('changed', 1); $this->postJson('/management/opportunity-origin/apply', $apply)->assertOk()->assertJsonPath('replayed', true); $this->assertSame([], app(InvestmentCatalogue::class)->query([])->pluck('id')->all()); $this->get('/oportunidades/qa-origin')->assertNotFound(); $this->getJson('/api/v1/opportunities/qa-origin')->assertNotFound(); $this->assertSame(0, app(InvestmentCatalogue::class)->map([])['matched_count']); $this->get('/management/opportunity-origin')->assertOk(); $this->assertSame($before, $f['opportunity']->fresh()->getRawOriginal()); $this->assertDatabaseHas('opportunity_projects', ['opportunity_id' => $f['opportunity']->id, 'project_id' => $f['project']->id]); $this->assertDatabaseHas('opportunity_favourites', ['opportunity_id' => $f['opportunity']->id, 'user_id' => $f['manager']->id]); $state = app(OpportunityOriginReview::class)->state($f['opportunity']->id); $this->assertSame($f['manager']->id, $state['history'][0]['author']); $this->assertSame($payload['reason'], $state['history'][0]['reason']); $this->postJson('/management/opportunity-origin/apply', ['id' => $f['opportunity']->id, 'classification' => 'undetermined', 'reason' => 'QA restore previous classification', 'snapshot_hash' => $state['snapshot_hash'], 'confirm' => true])->assertOk(); $this->assertTrue(Opportunity::published()->whereKey($f['opportunity']->id)->exists()); $this->assertCount(2, app(OpportunityOriginReview::class)->state($f['opportunity']->id)['history']); } public function test_permissions_foundation_confirmation_and_conflicts_are_required(): void { $f = $this->fixture(); $payload = ['id' => $f['opportunity']->id, 'classification' => 'real', 'reason' => 'QA declaration']; $this->actingAs(User::factory()->create())->postJson('/management/opportunity-origin/preview', $payload)->assertForbidden(); $this->actingAs($f['manager'])->postJson('/management/opportunity-origin/preview', [...$payload, 'reason' => ''])->assertUnprocessable(); $state = app(OpportunityOriginReview::class)->state($f['opportunity']->id); $this->postJson('/management/opportunity-origin/apply', [...$payload, 'snapshot_hash' => $state['snapshot_hash']])->assertUnprocessable(); $f['opportunity']->update(['description' => 'QA correction']); $this->postJson('/management/opportunity-origin/apply', [...$payload, 'snapshot_hash' => $state['snapshot_hash'], 'confirm' => true])->assertConflict(); $this->assertSame(0, DB::table('audit_events')->where('source_system', 'opportunity-origin-review')->count()); unlink($this->directory.'/proof.json'); $this->postJson('/management/opportunity-origin/preview', $payload)->assertOk()->assertJsonPath('ready', false); } }