'testing', 'APP_DEBUG' => 'true', 'APP_URL' => 'http://127.0.0.1:18095', 'APP_KEY' => 'base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', 'DB_CONNECTION' => 'pgsql', 'DB_HOST' => '127.0.0.1', 'DB_PORT' => '55441', 'DB_DATABASE' => $target['database'], 'DB_USERNAME' => 'postgres', 'DB_PASSWORD' => '', 'DB_URL' => '', 'SESSION_DRIVER' => 'file', 'SESSION_COOKIE' => 'opportunity_management_qa', 'CACHE_STORE' => 'array', 'QUEUE_CONNECTION' => 'sync'] as $key => $value) { putenv($key.'='.$value); $_ENV[$key] = $_SERVER[$key] = $value; } require $root.'/vendor/autoload.php'; $app = require $root.'/bootstrap/app.php'; $app->make(Kernel::class)->bootstrap(); $identity = DB::selectOne('SELECT current_database() AS database,inet_server_port() AS port'); if ($identity->database !== $target['database'] || (int) $identity->port !== 55441) { throw new RuntimeException('Refusing non-QA target'); } config(['portal.managers' => ['opportunity-manager@example.invalid'], 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]); $sessions = $dir.'/qa-opportunity-management-sessions'; if (! is_dir($sessions)) { mkdir($sessions, 0700, true); } config(['session.files' => $sessions]); Vite::useHotFile($root.'/storage/framework/qa-opportunity-management-hot-absent'); if (PHP_SAPI === 'cli' && ($argv[1] ?? '') === 'prepare') { $migration = require $root.'/database/migrations/'.OpportunityManagementInstallation::MIGRATION.'.php'; $migration->up(); $oldColumns = array_keys((array) DB::selectOne('SELECT * FROM opportunities LIMIT 1')); $oldColumns = array_diff($oldColumns, ['management_version', 'opportunity_category', 'management_draft', 'management_creation_key', 'first_submitted_at', 'management_closed_once']); $projection = implode(',', array_map(fn ($column) => '"'.$column.'"', $oldColumns)); $rows = DB::select('SELECT row_to_json(t)::text AS row FROM (SELECT '.$projection.' FROM opportunities) t ORDER BY row_to_json(t)::text'); if (hash('sha256', implode("\n", array_column($rows, 'row'))) !== $backup['table_hashes']['"public"."opportunities"']) { throw new RuntimeException('Existing opportunity values changed'); } $migration->down(); foreach ($backup['table_hashes'] as $table => $hash) { $rows = DB::select('SELECT row_to_json(t)::text AS row FROM '.$table.' t ORDER BY row_to_json(t)::text'); if (hash('sha256', implode("\n", array_column($rows, 'row'))) !== $hash) { throw new RuntimeException('Restore/rollback comparison failed: '.$table); } } Artisan::call('migrate', ['--path' => 'database/migrations/'.OpportunityManagementInstallation::MIGRATION.'.php', '--force' => true]); $user = User::updateOrCreate(['email' => 'opportunity-manager@example.invalid'], ['name' => 'QA opportunity manager', 'password' => Hash::make('Opportunity-QA-2026!')]); $user->forceFill(['email_verified_at' => now()])->save(); $proof = ['target' => $target, 'code_hash' => app(OpportunityManagementInstallation::class)->fingerprint(), 'compatibility_verified' => true, 'reversal_verified' => true, 'tables_compared' => count($backup['table_hashes']), 'active_writes' => 0]; file_put_contents($dir.'/opportunity-management-compatibility.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo json_encode($proof).PHP_EOL; exit; } if (PHP_SAPI === 'cli' && ($argv[1] ?? '') === 'verify') { $columns = array_diff(array_keys((array) DB::selectOne('SELECT * FROM opportunities LIMIT 1')), ['management_version', 'opportunity_category', 'management_draft', 'management_creation_key', 'first_submitted_at', 'management_closed_once']); $projection = implode(',', array_map(fn ($column) => '"'.$column.'"', $columns)); $checks = []; foreach (['opportunities', 'opportunity_projects', 'opportunity_clusters', 'opportunity_favourites', 'interest_requests', 'interest_submission_receipts', 'investment_messages', 'tasks', 'project_locations', 'cluster_projects', 'audit_events'] as $table) { $key = '"public"."'.$table.'"'; $query = $table === 'opportunities' ? '(SELECT '.$projection.' FROM opportunities WHERE management_creation_key IS NULL)' : ($table === 'audit_events' ? "(SELECT * FROM audit_events WHERE source_system IS DISTINCT FROM 'opportunity-management')" : $key); $rows = DB::select('SELECT row_to_json(t)::text AS row FROM '.$query.' t ORDER BY row_to_json(t)::text'); $checks[$table] = hash('sha256', implode("\n", array_column($rows, 'row'))) === $backup['table_hashes'][$key]; if (! $checks[$table]) { throw new RuntimeException('Baseline data changed: '.$table); } } $browser = json_decode(file_get_contents($dir.'/opportunity-management-browser.json'), true, 512, JSON_THROW_ON_ERROR); $draft = DB::table('opportunities')->where('id', $browser['id'])->first(); if ($draft->status !== 'draft' || $draft->deleted_at !== null) { throw new RuntimeException('Browser cycle not restored'); } $proof = ['target' => $target, 'code_hash' => app(OpportunityManagementInstallation::class)->fingerprint(), 'preserved' => $checks, 'browser_draft_id' => $draft->id, 'browser_draft_restored' => true, 'origin_classifications' => DB::table('audit_events')->where('source_system', 'opportunity-origin-review')->count(), 'active_writes' => 0]; file_put_contents($dir.'/opportunity-management-preservation.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo json_encode($proof).PHP_EOL; exit; } $path = rawurldecode(parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'); $asset = realpath($root.'/public'.$path); $public = realpath($root.'/public'); if ($path !== '/' && $asset && $public && str_starts_with($asset, $public.DIRECTORY_SEPARATOR) && is_file($asset)) { return false; } header('X-Investmoz-QA-Target: '.$identity->database.':'.$identity->port); $app->handleRequest(Request::capture());