make(Kernel::class)->bootstrap(); $identity = portalQaAssertTarget($target); if (($argv[1] ?? '') === 'evidence') { DB::beginTransaction(); DB::statement('SET TRANSACTION READ ONLY'); $data = ['captured_at' => gmdate('c'), 'target' => $target, 'code_hash' => app(VirtualDeskInstallation::class)->fingerprint(), 'geometry_code_hash' => app(TerritoryGeometryImport::class)->fingerprint(), 'resource_code_hash' => app(OpportunityResourceInstallation::class)->fingerprint(), 'qa_path_journeys' => DB::table('investor_journeys')->whereNotNull('path_definition_id')->count(), 'qa_path_steps' => DB::table('journey_steps')->where('source_system', 'investor-path')->count(), 'qa_private_geometries' => DB::table('geo_geometries')->where('source_system', 'ine-cod-ab')->where('data_classification', 'internal')->count(), 'active_writes' => 0]; $resourceProof = $directory.'/resources-real-http-qa.json'; if (is_file($resourceProof)) { $resource = json_decode(file_get_contents($resourceProof), true, 512, JSON_THROW_ON_ERROR); if ($resource['target'] !== $target || ! $resource['api_persistence_passed']) { throw new RuntimeException('Resource evidence from another target refused.'); } $ids = [$resource['original_image_document_id'], ...array_column($resource['derivatives'], 'document_id'), DB::table('opportunity_documents')->where('id', $resource['document_resource_id'])->value('document_id')]; $documents = DB::table('documents')->whereIn('id', $ids)->get(['id', 'sha256', 'malware_scanned_sha256', 'status', 'storage_encryption', 'data_classification', 'malware_scan_status', 'malware_engine', 'malware_engine_version', 'malware_signatures_at', 'malware_scanned_at', 'malware_scan_attempts']); foreach ($documents as $document) { if ($document->malware_scan_status !== 'clean' || $document->malware_engine !== 'ClamAV' || trim($document->sha256) !== $document->malware_scanned_sha256 || $document->storage_encryption !== 'laravel' || $document->data_classification !== 'restricted') { throw new RuntimeException('Real independent clean/encrypted/restricted evidence absent.'); } } if (count($documents) !== 5) { throw new RuntimeException('Five original/derivative documents expected in this QA operation.'); } $persistence = ['captured_at' => gmdate('c'), 'target' => $target, 'documents' => $documents, 'scans' => DB::table('document_malware_scans')->whereIn('document_id', $ids)->get(), 'passed' => true, 'active_writes' => 0, 'interface_observed' => false]; file_put_contents($directory.'/document-real-persistence-qa.json', json_encode($persistence, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); } DB::rollBack(); file_put_contents($directory.'/investor-path-qa-runtime.json', json_encode($data, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo json_encode($data).PHP_EOL; exit; } if (($argv[1] ?? '') === 'fixtures') { config(['investor-path.enabled' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]); $actor = User::firstOrCreate(['email' => 'investor-path-qa@example.invalid'], [ 'name' => 'Investidor sintético A/B QA', 'password' => Hash::make('Investor-Path-QA-2026!'), 'email_verified_at' => now(), ]); if (! $actor->hasVerifiedEmail()) { $actor->forceFill(['email_verified_at' => now()])->save(); } $outsider = User::firstOrCreate(['email' => 'investor-path-outsider-qa@example.invalid'], [ 'name' => 'Outra conta sintética QA', 'password' => Hash::make('Outsider-Path-QA-2026!'), ]); if (! $outsider->hasVerifiedEmail()) { $outsider->forceFill(['email_verified_at' => now()])->save(); } $path = app(InvestorPath::class); $desk = app(VirtualDesk::class); $drafts = app(SimulationFormDrafts::class); $fixtures = []; foreach (['A', 'B'] as $index => $scenario) { $id = $path->create($actor, ['title' => 'Cenário '.$scenario.' sintético QA — '.($index ? 'terra e financiamento' : 'sem terra nem financiamento externo'), 'client_key' => '61000919-0000-4000-8000-00000000000'.($index + 1), 'reuse_consent' => true]); $view = $path->view($actor, $id); if ($view['facts']['land_needed'] === 'unknown' && $view['facts']['external_funding'] === 'unknown') { $path->facts($actor, $id, ['version' => $view['case']['version'], 'land_needed' => $index ? 'yes' : 'no', 'external_funding' => $index ? 'yes' : 'no', 'viability' => 'proceed', 'reason' => 'Fixture sintética para demonstração; decisão declarada de preparação, sem aprovação externa.']); } $request = Request::create('/', 'POST', ['context' => $id]); $request->setUserResolver(fn () => $actor); $case = $desk->case($actor, $id); if (! $case->simulation_id) { $snapshot = ['version' => 1, 'data' => ['budgetPeriod' => 'Mensal', 'loan' => ['principal' => '100000', 'rate' => '0', 'months' => '24', 'rateType' => 'nominal', 'periodMonths' => '1', 'gracePeriods' => '0', 'upfrontFee' => '0', 'recurringFee' => '0'], 'breakEven' => ['fixed' => '1000', 'price' => '100', 'variable' => '50'], 'funding' => ['total' => '100000', 'secured' => $index ? '30000' : '100000'], 'incomes' => [['label' => 'Receita sintética QA', 'amount' => '100']], 'expenses' => [['label' => 'Custo sintético QA', 'amount' => '50']]]]; $created = $drafts->create(['personal_snapshot' => [], 'economic_snapshot' => ['purpose' => 'simulate_only', 'project_title' => 'Ideia '.$scenario.' sintética QA', 'project_summary' => 'Dados fictícios para revisão do percurso integrado.', 'currency' => 'MZN', 'financial_tools_snapshot' => json_encode($snapshot, JSON_THROW_ON_ERROR)]], $request, 'qa-fixture-path-'.$scenario.'-2026'); $path->returnTool($request, $id, ['version' => $path->view($actor, $id)['case']['version'], 'draft_id' => $created['draft']->id, 'draft_version' => $created['draft']->version_no]); } $fixtures[] = ['scenario' => $scenario, 'id' => $id, 'url' => '/investimentos/'.$id, 'steps' => count($path->view($actor, $id)['steps']), 'ui_demonstrated' => false]; } $downRefused = false; try { (require $root.'/database/migrations/2026_10_09_190000_prepare_investor_path.php')->down(); } catch (RuntimeException $exception) { $downRefused = str_contains($exception->getMessage(), 'Preserve investor path history'); } if (! $downRefused) { throw new RuntimeException('Used-data rollback guard was not demonstrated.'); } $proof = ['target' => $target, 'fixtures' => $fixtures, 'account' => $actor->email, 'outsider_account' => $outsider->email, 'fixture_setup_only' => true, 'used_down_refused' => true, 'active_writes' => 0, 'visual_verified' => false]; file_put_contents($directory.'/investor-path-fixtures.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo json_encode($proof).PHP_EOL; exit; } $name = '2026_10_09_190000_prepare_investor_path'; $quote = fn (string $name): string => '"'.str_replace('"', '""', $name).'"'; $columns = []; foreach (DB::select("SELECT table_name,column_name FROM information_schema.columns WHERE table_schema='public' ORDER BY table_name,ordinal_position") as $row) { $columns[$row->table_name][] = $row->column_name; } $snapshot = function () use ($columns, $quote): array { $hashes = []; foreach ($columns as $table => $fields) { if (! Schema::hasTable($table) || ! DB::selectOne("SELECT EXISTS(SELECT 1 FROM pg_tables WHERE schemaname='public' AND tablename=?) AS present", [$table])->present) { continue; } $projection = implode(',', array_map($quote, $fields)); $rows = DB::select('SELECT row_to_json(t)::text AS row FROM (SELECT '.$projection.' FROM '.$quote($table).') t ORDER BY row_to_json(t)::text'); $hashes[$table] = hash('sha256', implode("\n", array_column($rows, 'row'))); } return $hashes; }; DB::beginTransaction(); try { DB::select('select pg_advisory_xact_lock(261009, 1919)'); $before = $snapshot(); if (Schema::hasColumn('investor_journeys', 'path_definition_id')) { // Idempotent repeat: validate the published snapshot, do not repeat installation or rollback used data. $record = DB::table('questionnaire_versions')->where('id', config('investor-path.definition_id'))->firstOrFail(); InvestorPathDefinition::validate(json_decode($record->schema_snapshot, true, 512, JSON_THROW_ON_ERROR)); $unused = ! DB::table('investor_journeys')->whereNotNull('path_definition_id')->exists(); if ($unused) { $migration = require $root.'/database/migrations/'.$name.'.php'; $migration->down(); $migration->up(); } DB::rollBack(); if ($snapshot() !== $before) { throw new RuntimeException('QA replay changed records.'); } $proofFile = $directory.'/investor-path-compatibility.json'; $proof = json_decode(file_get_contents($proofFile), true, 512, JSON_THROW_ON_ERROR); $proof['code_hash'] = app(VirtualDeskInstallation::class)->fingerprint(); $proof['rechecked_at'] = gmdate('c'); $proof['replay_preserved'] = true; $proof['final_unused_down_rechecked'] = $unused; file_put_contents($proofFile, json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo json_encode(['target' => $target, 'already_installed_qa' => true, 'installation_repeated' => false, 'active_writes' => 0]).PHP_EOL; exit; } $migration = require $root.'/database/migrations/'.$name.'.php'; $migration->up(); $migration->down(); // Immutable unused definition retained by down; remove the entire rehearsal through transaction rollback. DB::rollBack(); $after = $snapshot(); if ($before !== $after || Schema::hasColumn('investor_journeys', 'path_definition_id')) { throw new RuntimeException('Rehearsal rollback changed original data/schema.'); } DB::transaction(function () use ($migration, $name): void { $migration->up(); DB::table('migrations')->insert(['migration' => $name, 'batch' => (DB::table('migrations')->max('batch') ?? 0) + 1]); }); $after = $snapshot(); $exceptions = ['questionnaire_versions', 'migrations']; $changed = array_keys(array_filter($before, fn ($hash, $table) => ! in_array($table, $exceptions, true) && $hash !== ($after[$table] ?? null), ARRAY_FILTER_USE_BOTH)); if ($changed) { throw new RuntimeException('Original records changed during installation: '.implode(',', $changed)); } $proof = ['target' => $target, 'code_hash' => app(VirtualDeskInstallation::class)->fingerprint(), 'captured_at' => gmdate('c'), 'backup_dump_sha256' => $backup['sha256'], 'original_tables_compared' => count($before), 'changed_original_tables' => $changed, 'unused_down_and_transaction_restore_verified' => true, 'qa_installed' => true, 'migration' => $name, 'added_columns' => 2, 'definition_rows_added' => 1, 'existing_journeys_converted' => 0, 'new_journeys' => 0, 'new_steps' => 0, 'active_writes' => 0, 'integrated_ui_ab_passed' => false, 'activation_ready' => false]; file_put_contents($directory.'/investor-path-compatibility.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo json_encode($proof).PHP_EOL; } catch (Throwable $exception) { if (DB::transactionLevel()) { DB::rollBack(); } throw $exception; }