make(Kernel::class)->bootstrap(); $settings = json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/payables-demo-compras.json'), true, 32, JSON_THROW_ON_ERROR); if (($settings['APP_ENV'] ?? '') !== 'payable-testing' || ($settings['DB_HOST'] ?? '') !== '127.0.0.1' || ! preg_match('/^compras_receipts_test_([a-f0-9]{12})$/D', $settings['DB_DATABASE'] ?? '', $match) || ($settings['DB_USERNAME'] ?? '') !== 'payable_qa_'.$match[1] || realpath($settings['HOST_ROOT'] ?? '') !== realpath(dirname(__DIR__))) { throw new LogicException('Apenas avaliação local isolada existente.'); } $owner = DB::connection('pgsql')->getConfig(); unset($owner['name']); if ($owner['host'] !== '127.0.0.1') { throw new LogicException('Restauro de controlo apenas local.'); } $qa = [...$owner, 'url' => null, 'host' => $settings['DB_HOST'], 'port' => $settings['DB_PORT'] ?? '5432', 'database' => $settings['DB_DATABASE'], 'username' => $settings['DB_USERNAME'], 'password' => $settings['DB_PASSWORD']]; config(['database.connections.supply_install' => $qa, 'database.connections.supply_owner' => [...$owner, 'database' => 'postgres', 'url' => null]]); $connection = DB::connection('supply_install'); $identity = $connection->selectOne('select current_database() as db, current_user as role'); if ($identity->db !== $settings['DB_DATABASE'] || $identity->role !== $settings['DB_USERNAME']) { throw new LogicException('Identidade do destino inesperada.'); } $lotInstall = in_array('--lots', $argv ?? [], true); $countInstall = in_array('--counts', $argv ?? [], true); $contextsInstall = in_array('--contexts', $argv ?? [], true); $noticesInstall = in_array('--notices', $argv ?? [], true); $warehouseInstall = in_array('--warehouse-drafts', $argv ?? [], true); $fulfilmentInstall = in_array('--warehouse-events', $argv ?? [], true); if ((int) $lotInstall + (int) $countInstall + (int) $contextsInstall + (int) $noticesInstall + (int) $warehouseInstall + (int) $fulfilmentInstall > 1) { throw new LogicException('Seleccione uma única migração por instalação.'); } $migration = $contextsInstall ? '2026_11_01_000045_add_work_context_configuration' : ($countInstall ? '2026_11_01_000044_allow_signed_lot_adjustments' : ($lotInstall ? '2026_11_01_000043_create_inventory_lot_allocations' : '2026_11_01_000042_create_supply_records')); $newTable = $countInstall || $contextsInstall ? null : ($lotInstall ? 'inventory_lot_allocations' : 'supply_records'); if ($noticesInstall) { $migration = '2026_11_01_000046_create_workflow_notices'; $newTable = 'workflow_notices'; } if ($warehouseInstall) { $migration = '2026_11_01_000047_create_warehouse_requests'; $newTable = 'warehouse_requests'; } if ($fulfilmentInstall) { $migration = '2026_11_01_000048_create_warehouse_request_events'; $newTable = 'warehouse_request_events'; } if ($connection->table('migrations')->where('migration', $migration)->exists()) { echo "A migração de fornecimento já está instalada; nenhum registo alterado.\n"; exit; } $tables = array_map(fn ($row) => $row->tablename, $connection->select("select tablename from pg_tables where schemaname='public' order by tablename")); function supplyFingerprint($db, array $tables, bool $contexts = false): array { $result = []; foreach ($tables as $table) { $quoted = '"'.str_replace('"', '""', $table).'"'; $projection = $contexts ? "(to_jsonb(t) - ARRAY['work_context_policy','work_contexts','work_context_key'])::text" : 'row_to_json(t)::text'; $rows = $db->select('select '.$projection.' as row from '.$quoted.' t order by '.$projection); $result[$table] = hash('sha256', json_encode(array_column($rows, 'row'), JSON_THROW_ON_ERROR)); } return $result; } $baseline = supplyFingerprint($connection, $tables, $contextsInstall); $directory = 'C:/laragon/data/sgf-tempo/backups/compras-supply-'.gmdate('Ymd-His'); if (! mkdir($directory, 0700, true)) { throw new RuntimeException('Não foi possível criar a pasta privada de backup.'); } $dump = $directory.'/before.dump'; $bin = 'C:/Program Files/PostgreSQL/17/bin/'; function supplyPg(string $binary, array $arguments, array $config): void { $process = new Process([$binary, ...$arguments], null, ['PGHOST' => $config['host'], 'PGPORT' => (string) $config['port'], 'PGUSER' => $config['username'], 'PGPASSWORD' => $config['password']], timeout: 120); $process->run(); if (! $process->isSuccessful()) { throw new RuntimeException('Operação PostgreSQL de backup/restauro falhou; instalação interrompida.'); } } supplyPg($bin.'pg_dump.exe', ['--format=custom', '--file='.$dump, '--dbname='.$qa['database']], $qa); $restore = 'compras_supply_restore_'.bin2hex(random_bytes(6)); $admin = DB::connection('supply_owner'); $admin->statement('CREATE DATABASE "'.$restore.'"'); try { supplyPg($bin.'pg_restore.exe', ['--no-owner', '--no-privileges', '--exit-on-error', '--dbname='.$restore, $dump], $owner); config(['database.connections.supply_restore' => [...$owner, 'database' => $restore, 'url' => null]]); if (supplyFingerprint(DB::connection('supply_restore'), $tables, $contextsInstall) !== $baseline) { throw new RuntimeException('Restauro diferente da origem. Nenhuma migração aplicada.'); } DB::setDefaultConnection('supply_install'); $exit = Artisan::call('migrate', ['--database' => 'supply_install', '--path' => 'database/migrations/'.$migration.'.php', '--force' => true]); if ($exit !== 0) { throw new RuntimeException('Migração dirigida falhou.'); } $preserved = array_values(array_diff($tables, ['migrations'])); if (supplyFingerprint($connection, $preserved, $contextsInstall) !== array_intersect_key($baseline, array_flip($preserved)) || ($newTable !== null && $connection->table($newTable)->count() !== 0)) { throw new RuntimeException('Conferência posterior falhou; conservar backup e investigar.'); } $proof = ['checked_at' => gmdate('c'), 'migration' => $migration, 'environment' => 'local evaluation 8005', 'backup_file' => $dump, 'backup_sha256' => hash_file('sha256', $dump), 'restore_verified' => true, 'existing_tables_preserved' => count($preserved), 'new_tables' => $newTable === null ? [] : [$newTable => 0], 'accounts_or_grants_changed' => false, 'domain_installed' => false]; if ($contextsInstall) { foreach (['requisitions' => 'procurement.internal', 'people_processes' => 'people.internal'] as $table => $key) { if ($connection->table($table)->where('work_context_key', '!=', $key)->exists()) { throw new RuntimeException('Classificação interna inesperada após migração.'); } } $proof['existing_processes_classified_internal'] = true; $proof['commercial_policy_activated'] = false; } $proofName = $fulfilmentInstall ? 'warehouse-fulfilment-install' : ($warehouseInstall ? 'warehouse-drafts-install' : ($noticesInstall ? 'workflow-notices-install' : ($contextsInstall ? 'work-context-install' : ($countInstall ? 'inventory-count-install' : ($lotInstall ? 'inventory-lot-install' : 'supply-install'))))); file_put_contents(dirname(__DIR__).'/docs/proofs/'.$proofName.'.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo 'Migração dirigida instalada; restauro conferido, '.count($preserved)." tabelas preservadas. Sem atribuições ou dados de negócio.\n"; } finally { DB::purge('supply_restore'); $admin->select('select pg_terminate_backend(pid) from pg_stat_activity where datname = ? and pid <> pg_backend_pid()', [$restore]); $admin->statement('DROP DATABASE "'.$restore.'"'); }