Files
compras/scripts/supply-install.php
T
2026-11-02 20:27:11 +02:00

141 lines
8.5 KiB
PHP

<?php
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Process\Process;
// Installs only the selected supply migration on the existing isolated local evaluation.
require dirname(__DIR__).'/vendor/autoload.php';
$app = require dirname(__DIR__).'/bootstrap/app.php';
$app->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);
$reservationInstall = in_array('--warehouse-reservations', $argv ?? [], true);
$recoveryInstall = in_array('--replenishment-recovery', $argv ?? [], true);
if ((int) $lotInstall + (int) $countInstall + (int) $contextsInstall + (int) $noticesInstall + (int) $warehouseInstall + (int) $fulfilmentInstall + (int) $reservationInstall + (int) $recoveryInstall > 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 ($reservationInstall) {
$migration = '2026_11_02_000049_create_warehouse_reservations';
$newTable = 'warehouse_reservations';
}
if ($recoveryInstall) {
$migration = '2026_11_02_000050_create_replenishment_work';
$newTable = 'replenishment_work';
}
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')))));
if ($reservationInstall) {
$proofName = 'warehouse-reservations-install';
}
if ($recoveryInstall) {
$proofName = 'replenishment-recovery-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.'"');
}