104 lines
6.0 KiB
PHP
104 lines
6.0 KiB
PHP
<?php
|
|
|
|
use Illuminate\Contracts\Console\Kernel;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Symfony\Component\Process\Process;
|
|
|
|
require dirname(__DIR__).'/vendor/autoload.php';
|
|
$root = dirname(__DIR__);
|
|
if (! in_array('--prepare-issued-order-schema', $argv, true)) {
|
|
throw new LogicException('Explicit preparation flag required.');
|
|
}
|
|
$s = json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/payables-demo-compras.json'), true, 32, JSON_THROW_ON_ERROR);
|
|
if (($s['APP_ENV'] ?? '') !== 'payable-testing' || ($s['DB_HOST'] ?? '') !== '127.0.0.1' || realpath($s['HOST_ROOT'] ?? '') !== realpath($root) || ! preg_match('/^compras_receipts_test_([a-f0-9]{12})$/D', $s['DB_DATABASE'] ?? '', $m) || $s['DB_USERNAME'] !== 'payable_qa_'.$m[1]) {
|
|
throw new LogicException('Wrong pilot destination.');
|
|
}
|
|
$app = require $root.'/bootstrap/app.php';
|
|
$app->make(Kernel::class)->bootstrap();
|
|
$owner = DB::connection('pgsql')->getConfig();
|
|
unset($owner['name']);
|
|
if ($owner['host'] !== '127.0.0.1') {
|
|
throw new LogicException('Local server required.');
|
|
}
|
|
$qa = [...$owner, 'url' => null, 'database' => $s['DB_DATABASE'], 'username' => $s['DB_USERNAME'], 'password' => $s['DB_PASSWORD']];
|
|
config(['database.connections.receipt_install' => $qa, 'database.connections.receipt_owner' => [...$owner, 'url' => null, 'database' => 'postgres']]);
|
|
DB::setDefaultConnection('receipt_install');
|
|
$db = DB::connection();
|
|
$admin = DB::connection('receipt_owner');
|
|
$identity = $db->selectOne('select current_database() as db, current_user as role');
|
|
if ($identity->db !== $s['DB_DATABASE'] || $identity->role !== $s['DB_USERNAME']) {
|
|
throw new LogicException('Identity mismatch.');
|
|
}
|
|
$fingerprint = function ($connection, $tables) {
|
|
$out = [];
|
|
foreach ($tables as $table) {
|
|
$rows = $connection->select('select row_to_json(t)::text as row from "'.str_replace('"', '""', $table).'" t order by row_to_json(t)::text');
|
|
$out[$table] = hash('sha256', json_encode(array_column($rows, 'row'), JSON_THROW_ON_ERROR));
|
|
}
|
|
|
|
return $out;
|
|
};
|
|
$pg = function ($binary, $args, $connection) {
|
|
$p = new Process(['C:/Program Files/PostgreSQL/17/bin/'.$binary.'.exe', ...$args], null, ['PGHOST' => $connection['host'], 'PGPORT' => (string) $connection['port'], 'PGUSER' => $connection['username'], 'PGPASSWORD' => $connection['password']], timeout: 120);
|
|
$p->run();
|
|
if (! $p->isSuccessful()) {
|
|
throw new RuntimeException('Backup/restore failed.');
|
|
}
|
|
};
|
|
$tables = array_column($db->select("select tablename from pg_tables where schemaname='public' order by tablename"), 'tablename');
|
|
$migration = 'database/migrations/2026_11_03_000003_create_order_cancellations.php';
|
|
$newTables = ['order_cancellation_policies', 'order_cancellation_policy_reviews', 'order_cancellations'];
|
|
if (config('order_cancellation.enabled') || array_intersect($tables, $newTables)) {
|
|
throw new LogicException('Circuit must remain disabled and uninstalled in the pilot.');
|
|
}
|
|
$restore = 'compras_pilot_restore_'.bin2hex(random_bytes(6));
|
|
$created = false;
|
|
try {
|
|
$db->beginTransaction();
|
|
$db->statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');
|
|
$baseline = $fingerprint($db, $tables);
|
|
$directory = 'C:/laragon/data/sgf-tempo/backups/compras-issued-order-schema-'.gmdate('Ymd-His').'-'.bin2hex(random_bytes(3));
|
|
if (! mkdir($directory, 0700, true)) {
|
|
throw new RuntimeException('Backup unavailable.');
|
|
}
|
|
$dump = $directory.'/before.dump';
|
|
$snapshot = $db->selectOne('select pg_export_snapshot() as snapshot')->snapshot;
|
|
$pg('pg_dump', ['--format=custom', '--snapshot='.$snapshot, '--file='.$dump, '--dbname='.$qa['database']], $qa);
|
|
$db->commit();
|
|
$admin->statement('CREATE DATABASE "'.$restore.'"');
|
|
$created = true;
|
|
$pg('pg_restore', ['--no-owner', '--no-privileges', '--exit-on-error', '--dbname='.$restore, $dump], $owner);
|
|
config(['database.connections.receipt_restore' => [...$owner, 'url' => null, 'database' => $restore]]);
|
|
$restored = DB::connection('receipt_restore');
|
|
if ($fingerprint($restored, $tables) !== $baseline) {
|
|
throw new LogicException('Restored backup differs.');
|
|
}
|
|
$migrationCount = $restored->table('migrations')->count();
|
|
if (Artisan::call('migrate', ['--database' => 'receipt_restore', '--path' => $migration, '--force' => true]) !== 0) {
|
|
throw new RuntimeException('Disposable migration failed.');
|
|
}
|
|
$protected = array_values(array_diff($tables, ['migrations']));
|
|
if ($fingerprint($restored, $protected) !== array_intersect_key($baseline, array_flip($protected)) || $restored->table('migrations')->count() !== $migrationCount + 1) {
|
|
throw new LogicException('Unexpected existing data changes.');
|
|
}
|
|
foreach ($newTables as $table) {
|
|
if ($restored->table($table)->count() !== 0) {
|
|
throw new LogicException('New tables must remain empty.');
|
|
}
|
|
}
|
|
$result = ['checked_at' => gmdate('c'), 'result' => 'passed', 'environment' => 'http://127.0.0.1:8005', 'scope' => 'empty_schema_only_no_activation', 'migration' => $migration, 'migration_sha256' => hash_file('sha256', $root.'/'.$migration),
|
|
'new_empty_tables' => $newTables, 'backup' => $dump, 'backup_sha256' => hash_file('sha256', $dump), 'restored_tables_checked' => count($tables), 'protected_tables_checked' => count($protected),
|
|
'pilot_migration_applied' => false, 'pilot_configuration_changed' => false, 'policy_publication' => false, 'cancellation_commands' => false, 'mfa_enabled' => false, 'restore_database_removed' => false];
|
|
} finally {
|
|
if ($db->transactionLevel() > 0) {
|
|
$db->rollBack();
|
|
}DB::purge('receipt_restore');
|
|
if ($created) {
|
|
$admin->statement('DROP DATABASE "'.$restore.'" WITH (FORCE)');
|
|
}
|
|
}
|
|
$result['restore_database_removed'] = true;
|
|
file_put_contents($root.'/docs/proofs/issued-order-schema-preparation.json', json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL);
|
|
echo 'Empty cancellation schema rehearsed only in restored disposable backup; pilot unchanged.'.PHP_EOL;
|