Files
compras/scripts/pilot-replenishment-dry-run.php
T
2026-11-03 11:51:44 +02:00

325 lines
26 KiB
PHP

<?php
use App\Http\Controllers\QuotationController;
use App\Models\Quotation;
use App\Models\Requisition;
use App\Models\StockItem;
use App\Models\SupplyRecord;
use App\Models\User;
use App\Models\Warehouse;
use App\Services\ApprovalRouting;
use App\Services\AutomaticPurchases;
use App\Services\ProposalComparison;
use App\Services\RequisitionCatalogue;
use App\Services\RequisitionWorkflow;
use App\Services\StockWorkflow;
use App\Services\SupplyPlanning;
use App\Services\WarehouseAvailability;
use App\Services\WorkflowTaskInbox;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Process\Process;
// Deliberately has no apply mode: all business commands target a disposable restored copy.
require dirname(__DIR__).'/vendor/autoload.php';
$root = dirname(__DIR__);
$proposalFile = $root.'/docs/proofs/pilot-replenishment-proposal.json';
$proposal = json_decode(file_get_contents($proposalFile), true, 32, JSON_THROW_ON_ERROR);
$settings = json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/payables-demo-compras.json'), true, 32, JSON_THROW_ON_ERROR);
if ($proposal['status'] !== 'pending_owner_authorization' || $proposal['environment'] !== 'http://127.0.0.1:8005'
|| $proposal['scope'] !== 'synthetic_local_pilot_only' || ($settings['APP_ENV'] ?? '') !== 'payable-testing'
|| ($settings['DB_HOST'] ?? '') !== '127.0.0.1' || realpath($settings['HOST_ROOT'] ?? '') !== realpath($root)
|| ! preg_match('/^compras_receipts_test_([a-f0-9]{12})$/D', $settings['DB_DATABASE'] ?? '', $match)
|| ($settings['DB_USERNAME'] ?? '') !== 'payable_qa_'.$match[1]) {
throw new LogicException('Origem e proposta limitadas ao piloto sintético local.');
}
$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('Cópia descartável exclusivamente local.');
}
$clone = 'compras_receipts_test_'.bin2hex(random_bytes(6));
if ($clone === $settings['DB_DATABASE']) {
throw new LogicException('Destino descartável tem de ser distinto da origem.');
}
config(['database.connections.pilot_clone_admin' => [...$owner, 'url' => null, 'database' => 'postgres']]);
$admin = DB::connection('pilot_clone_admin');
$bin = 'C:/Program Files/PostgreSQL/17/bin/';
// Resolve the installed binaries without embedding credentials in commands or logs.
if (! is_file($bin.'pg_dump.exe')) {
$candidates = glob('C:/laragon/bin/postgresql/*/bin/pg_dump.exe');
if (count($candidates) !== 1) {
throw new LogicException('Instalação PostgreSQL não inequívoca.');
}
$bin = dirname($candidates[0]).'/';
}
$privateRoot = 'C:/laragon/data/sgf-tempo/proofs';
if (! is_dir($privateRoot)) {
mkdir($privateRoot, 0700, true);
}
$dump = $privateRoot.'/replenishment-dry-run-'.bin2hex(random_bytes(6)).'.dump';
$runPg = function (string $binary, array $arguments, array $connection) {
$process = new Process([$binary, ...$arguments], null, ['PGHOST' => $connection['host'], 'PGPORT' => (string) $connection['port'],
'PGUSER' => $connection['username'], 'PGPASSWORD' => $connection['password']], timeout: 120);
$process->run();
if (! $process->isSuccessful()) {
throw new RuntimeException('Cópia/restauro descartável não concluído.');
}
};
$created = false;
$proof = null;
$failure = null;
try {
$qa = [...$owner, 'host' => $settings['DB_HOST'], 'port' => $settings['DB_PORT'] ?? '5432', 'database' => $settings['DB_DATABASE'],
'username' => $settings['DB_USERNAME'], 'password' => $settings['DB_PASSWORD']];
$runPg($bin.'pg_dump.exe', ['--format=custom', '--file='.$dump, '--dbname='.$qa['database']], $qa);
$admin->statement('CREATE DATABASE "'.$clone.'"');
$created = true;
$runPg($bin.'pg_restore.exe', ['--no-owner', '--no-privileges', '--exit-on-error', '--dbname='.$clone, $dump], $owner);
config(['database.connections.pilot_clone' => [...$owner, 'url' => null, 'database' => $clone], 'work_context.composition' => 'sgc', 'app.env' => 'testing']);
DB::setDefaultConnection('pilot_clone');
$app->detectEnvironment(fn () => 'testing');
// Reproduce only the existing evaluation entitlement fallback in this restored copy.
// It is not a persisted customer contract or a new permission grant.
$accessProposal = json_decode(file_get_contents($root.'/docs/proofs/pilot-access-proposal.json'), true, 64, JSON_THROW_ON_ERROR);
config(['work_context.entitlements.'.$proposal['tenant_id'] => $accessProposal['enabled_contexts']]);
if (DB::connection()->getDatabaseName() !== $clone || ! preg_match('/^compras_receipts_test_[a-f0-9]{12}$/D', $clone)) {
throw new LogicException('Comandos proibidos fora da cópia descartável.');
}
$request = Request::create($proposal['environment'].'/stock/reposicao', 'GET', server: ['REMOTE_ADDR' => '127.0.0.1']);
$request->setLaravelSession($app['session']->driver('array'));
$app->instance('request', $request);
$request->setUserResolver(fn ($guard = null) => auth()->guard($guard)->user());
$check = function (bool $value, string $message) {
if (! $value) {
throw new LogicException($message);
}
};
$actor = function (string $task) use ($proposal, $request) {
$user = User::query()->where('tenant_id', $proposal['tenant_id'])->findOrFail($proposal['actors'][$task]);
auth()->guard('web')->setUser($user);
$request->session()->put('work_context', ['key' => 'procurement.internal', 'user_id' => $user->id, 'tenant_id' => $user->tenant_id]);
return $user;
};
if (in_array('--evaluations', $argv, true)) {
$nextFile = $root.'/docs/proofs/pilot-evaluation-proposal.json';
$next = json_decode(file_get_contents($nextFile), true, 32, JSON_THROW_ON_ERROR);
$check($next['environment'] === $proposal['environment'] && $next['status'] === 'pending_owner_authorization', 'Proposta sintética de avaliação inválida.');
$preparer = $actor('prepare');
$purchase = Requisition::query()->findOrFail($next['purchase_id']);
$check($purchase->status === 'approved' && $purchase->number === $next['purchase_number'], 'Compra diferente da proposta.');
$quotes = Quotation::query()->where('requisition_id', $purchase->id)->orderBy('id')->get();
$check($quotes->pluck('id')->all() === $next['quotation_ids'] && $quotes->sum(fn ($quote) => $quote->evaluations()->count()) === 0, 'Propostas ou avaliações já alteradas.');
$protected = ['users', 'organization_access', 'organization_profiles', 'organization_memberships', 'suppliers', 'quotations', 'quotation_items', 'purchase_orders', 'stock_movements', 'requisitions', 'requisition_items', 'budget_entries', 'purchase_awards'];
$fingerprint = fn () => collect($protected)->mapWithKeys(fn ($table) => [$table => hash('sha256', json_encode(DB::table($table)->orderBy('id')->get(), JSON_THROW_ON_ERROR))])->all();
$baseline = $fingerprint();
$duplicateRefused = false;
foreach ($next['evaluators'] as $id) {
$evaluator = User::query()->where('tenant_id', $proposal['tenant_id'])->findOrFail($id);
auth()->guard('web')->setUser($evaluator);
$request->session()->put('work_context', ['key' => 'procurement.internal', 'user_id' => $evaluator->id, 'tenant_id' => $evaluator->tenant_id]);
foreach ($quotes as $quote) {
$input = Request::create($proposal['environment'].'/cotacoes/'.$quote->id.'/avaliar', 'POST', $next['grades_for_each_quotation_and_each_evaluator']);
$input->setUserResolver(fn () => $evaluator);
app(QuotationController::class)->evaluate($input, $quote, app(RequisitionWorkflow::class));
if (! $duplicateRefused) {
try {
app(QuotationController::class)->evaluate($input, $quote, app(RequisitionWorkflow::class));
} catch (ValidationException|HttpException) {
$duplicateRefused = true;
}
$check($duplicateRefused, 'Uma segunda avaliação da mesma pessoa não pode duplicar o registo.');
}
}
}
$preparer = $actor('prepare');
$comparison = app(ProposalComparison::class)->inspect($purchase);
$check($comparison['ready'] && $comparison['policy']['minimum_evaluators'] === $next['expected_minimum_qualified_evaluators_per_quotation'], 'Comparação não ficou pronta segundo a política existente.');
$check($quotes->sum(fn ($quote) => $quote->evaluations()->count()) === $next['expected_evaluations'] && $fingerprint() === $baseline, 'Resultados fora do âmbito de avaliação.');
$check(collect(app(WorkflowTaskInbox::class)->build($preparer, 'procurement'))->contains('id', 'quotation:'.$purchase->id.':result'), 'Comparação sem encaminhamento para conferência humana.');
$proof = ['checked_at' => now()->toIso8601String(), 'proposal_sha256' => hash_file('sha256', $nextFile), 'result' => 'passed', 'database' => 'disposable restored copy of local synthetic SGC',
'source_business_commands' => false, 'evaluations_in_copy' => 6, 'evaluators' => $next['evaluators'], 'duplicate_evaluation_refused' => $duplicateRefused,
'comparison_ready' => true, 'human_comparison_review_task' => true, 'supplier_selection_award_or_order' => false, 'protected_tables_unchanged' => $protected,
'outbound_communication' => false, 'accounts_or_grants_changed' => false, 'sgf_installation_changed' => false];
} elseif (in_array('--market', $argv, true)) {
$nextFile = $root.'/docs/proofs/pilot-market-proposal.json';
$next = json_decode(file_get_contents($nextFile), true, 32, JSON_THROW_ON_ERROR);
$check($next['environment'] === $proposal['environment'] && $next['status'] === 'pending_owner_authorization', 'Proposta sintética de consulta inválida.');
require_once __DIR__.'/support/pilot-market-scenario.php';
$scenario = pilotMarketScenario($app, $proposal, $next, $actor, $check);
$protected = $scenario['protected_tables'];
$proof = ['checked_at' => now()->toIso8601String(), 'proposal_sha256' => hash_file('sha256', $nextFile), 'result' => 'passed', 'database' => 'disposable restored copy of local synthetic SGC',
'source_business_commands' => false, 'new_synthetic_suppliers_in_copy' => 2, 'supplier_preparation_and_review_independent' => true, 'quotations_in_copy' => 3,
'totals_minor_mzn' => [2320, 2552, 2784], 'evaluations' => 0, 'supplier_selection_award_or_order' => false, 'protected_tables_unchanged' => $protected,
'outbound_communication' => false, 'accounts_or_grants_changed' => false, 'sgf_installation_changed' => false];
} elseif (in_array('--approval', $argv, true)) {
$nextFile = $root.'/docs/proofs/pilot-approval-proposal.json';
$next = json_decode(file_get_contents($nextFile), true, 32, JSON_THROW_ON_ERROR);
$check($next['environment'] === $proposal['environment'] && $next['status'] === 'pending_owner_authorization' && $next['action'] === 'approve', 'Proposta de aprovação inválida.');
$user = User::query()->where('tenant_id', $proposal['tenant_id'])->findOrFail($next['approver_id']);
auth()->guard('web')->setUser($user);
$request->session()->put('work_context', ['key' => 'procurement.internal', 'user_id' => $user->id, 'tenant_id' => $user->tenant_id]);
$purchase = Requisition::query()->findOrFail($next['purchase_id']);
$check($purchase->status === 'pending' && $purchase->number === $next['purchase_number'] && $purchase->canDecide($user)
&& $purchase->items()->sole()->quantity === $next['quantity'] && $purchase->total_minor === $next['total_minor_mzn'], 'Compra ou competência diferente da proposta.');
$protected = ['users', 'organization_access', 'organization_profiles', 'organization_memberships', 'purchase_orders', 'stock_movements', 'requisition_items', 'budget_entries'];
$fingerprint = fn () => collect($protected)->mapWithKeys(fn ($table) => [$table => hash('sha256', json_encode(DB::table($table)->orderBy('id')->get(), JSON_THROW_ON_ERROR))])->all();
$baseline = $fingerprint();
$count = Requisition::query()->count();
$key = (string) Str::uuid();
$workflow = app(RequisitionWorkflow::class);
$workflow->decide($user, $purchase, 'approved', $next['decision_comment'], $key);
$workflow->decide($user, $purchase->fresh(), 'approved', $next['decision_comment'], $key);
$purchase->refresh();
$check($purchase->status === 'approved' && $purchase->steps()->count() === 1 && $purchase->steps()->sole()->approver_id === $user->id, 'Resultado diferente da etapa de aprovação prevista.');
$check($fingerprint() === $baseline && Requisition::query()->count() === $count, 'A decisão alterou artigos, stock, contas ou ordens.');
$reservation = DB::table('budget_entries')->where('requisition_id', $purchase->id)->sole();
$check($reservation->kind === 'reserve' && (int) $reservation->reserved_minor === $next['total_minor_mzn'] && (int) $reservation->committed_minor === 0, 'A aprovação da requisição deve preservar a reserva existente, sem compromisso de encomenda.');
$procurement = $actor('prepare');
$tasks = app(WorkflowTaskInbox::class)->build($procurement, 'procurement');
$check(collect($tasks)->contains('id', 'quotation:'.$purchase->id.':prepare'), 'Compra aprovada sem encaminhamento para consulta ao mercado.');
$proof = ['checked_at' => now()->toIso8601String(), 'proposal_sha256' => hash_file('sha256', $nextFile), 'result' => 'passed',
'database' => 'disposable restored copy of local synthetic SGC', 'source_business_commands' => false, 'purchase_id' => $purchase->id,
'purchase_number' => $purchase->number, 'new_status_in_copy' => $purchase->status, 'approved_by_in_copy' => $user->id,
'budget_reserved_minor' => (int) $reservation->reserved_minor, 'budget_committed_minor' => 0, 'market_consultation_task_for_procurement' => true,
'same_command_repeated_without_duplication' => true, 'items_values_stock_orders_access_preserved' => true, 'sgf_installation_changed' => false];
} elseif (in_array('--submission', $argv, true)) {
$nextFile = $root.'/docs/proofs/pilot-submission-proposal.json';
$next = json_decode(file_get_contents($nextFile), true, 32, JSON_THROW_ON_ERROR);
$check($next['environment'] === $proposal['environment'] && $next['status'] === 'pending_owner_authorization' && $next['action'] === 'submit', 'Proposta de submissão inválida.');
$requester = $actor('requester');
$purchase = Requisition::query()->where('tenant_id', $proposal['tenant_id'])->findOrFail($next['purchase_id']);
$check($purchase->number === $next['purchase_number'] && $purchase->requester_id === $next['requester_id'] && $purchase->status === 'draft'
&& $purchase->items()->sole()->quantity === $next['quantity'] && $purchase->total_minor === $next['total_minor_mzn'] && $purchase->steps()->count() === 0, 'Rascunho diferente da proposta de submissão.');
$protected = ['users', 'organization_access', 'organization_profiles', 'organization_memberships', 'purchase_orders', 'stock_movements'];
$fingerprint = fn () => collect($protected)->mapWithKeys(fn ($table) => [$table => hash('sha256', json_encode(DB::table($table)->orderBy('id')->get(), JSON_THROW_ON_ERROR))])->all();
$baseline = $fingerprint();
$count = Requisition::query()->count();
$itemsBefore = $purchase->items()->get()->toArray();
$migration = 'database/migrations/2026_11_01_000046_add_requisition_command_versions.php';
$installedInCopy = false;
if (! Schema::hasTable('requisition_commands')) {
$check(! Schema::hasColumn('requisitions', 'process_version'), 'Instalação parcial inesperada.');
$check(Artisan::call('migrate', ['--database' => 'pilot_clone', '--path' => $migration, '--force' => true]) === 0, 'Actualização de recibos de comandos falhou na cópia.');
$installedInCopy = true;
$purchase->refresh();
}
$key = (string) Str::uuid();
$workflow = app(RequisitionWorkflow::class);
$workflow->submit($requester, $purchase, $key);
$purchase->refresh();
$stepsBefore = $purchase->steps()->get()->toArray();
$workflow->submit($requester, $purchase, $key);
$purchase->refresh();
$check($purchase->status === 'pending' && $purchase->steps()->count() > 0 && $purchase->steps()->where('decision', '!=', 'pending')->count() === 0, 'As aprovações têm de permanecer humanas e pendentes.');
$check($purchase->steps()->get()->toArray() === $stepsBefore && Requisition::query()->count() === $count, 'A repetição não pode duplicar passos ou compras.');
$check($purchase->items()->get()->toArray() === $itemsBefore && $purchase->total_minor === $next['total_minor_mzn'] && $fingerprint() === $baseline, 'A submissão alterou dados fora do âmbito.');
$routing = app(ApprovalRouting::class)->current($purchase, $requester);
$check($routing['status'] === 'available', 'A etapa não tem um responsável elegível.');
$eligible = [];
foreach ($routing['people'] as $person) {
$user = User::query()->findOrFail($person['id']);
auth()->guard('web')->setUser($user);
$request->session()->put('work_context', ['key' => 'procurement.internal', 'user_id' => $user->id, 'tenant_id' => $user->tenant_id]);
$check(collect(app(WorkflowTaskInbox::class)->build($user, 'procurement'))->contains('id', 'purchase:'.$purchase->id.':'.$routing['step_id']), 'A etapa não chegou às tarefas do aprovador elegível.');
$eligible[] = $person;
}
$requester = $actor('requester');
$check(! collect(app(WorkflowTaskInbox::class)->build($requester, 'procurement'))->contains('id', 'purchase:'.$purchase->id.':draft'), 'Rascunho já submetido continua como preparação pendente.');
$proof = ['checked_at' => now()->toIso8601String(), 'proposal_sha256' => hash_file('sha256', $nextFile), 'result' => 'passed', 'database' => 'disposable restored copy of local synthetic SGC',
'source_business_commands' => false, 'purchase_id' => $purchase->id, 'purchase_number' => $purchase->number, 'new_status_in_copy' => $purchase->status,
'current_role' => $routing['role'], 'eligible_people' => $eligible, 'all_approval_steps_pending' => true, 'same_command_repeated_without_duplication' => true,
'items_values_stock_orders_access_preserved' => true, 'sgf_installation_changed' => false];
$proof['required_command_receipt_migration'] = $migration;
$proof['command_receipt_migration_tested_in_copy'] = $installedInCopy;
} else {
$envelope = fn (array $data, string $action = 'save', ?SupplyRecord $old = null) => ['operation_key' => (string) Str::uuid(),
'record_key' => $old?->record_key, 'version' => $old?->version ?? 0, 'action' => $action,
'reason' => 'Simulação descartável da proposta sintética, ainda sem aplicação no piloto.', 'data' => $data];
$publisher = $actor('catalogue');
$check((int) DB::table('organization_access')->where('tenant_id', $proposal['tenant_id'])->value('version') === $proposal['expected_access_version'], 'A configuração dos acessos mudou.');
$stock = StockItem::query()->findOrFail($proposal['source']['stock_item_id']);
$check($stock->only(['code', 'name', 'unit']) === array_intersect_key($proposal['source'], array_flip(['code', 'name', 'unit'])), 'Cadastro diferente da proposta.');
$warehouse = Warehouse::query()->findOrFail($proposal['source']['warehouse_id']);
$check($warehouse->code === $proposal['source']['warehouse_code'], 'Armazém diferente da proposta.');
$balance = app(WarehouseAvailability::class)->row($stock, $warehouse, false);
$check($balance['physical_milli'] === $proposal['source']['physical_milli'] && $balance['free_milli'] === $proposal['source']['free_milli']
&& $balance['ledger_version'] === $proposal['source']['ledger_version'], 'Disponibilidade diferente da proposta.');
$check(! DB::table('requisition_catalogue')->where('tenant_id', $proposal['tenant_id'])->where('stock_item_id', $stock->id)->exists(), 'A publicação inicial mudou.');
$planning = app(SupplyPlanning::class);
$check($planning->latest('policy')->where('state', 'approved')->where('stock_item_id', $stock->id)->count() === 0, 'Já existe uma política activa.');
$check($planning->latest('auto_rule')->where('state', 'approved')->where('stock_item_id', $stock->id)->count() === 0, 'Já existe uma regra activa.');
$protected = ['users', 'organization_access', 'organization_profiles', 'organization_memberships', 'approval_steps', 'purchase_orders', 'stock_movements'];
$fingerprint = fn () => collect($protected)->mapWithKeys(fn ($table) => [$table => hash('sha256', json_encode(DB::table($table)->orderBy('id')->get(), JSON_THROW_ON_ERROR))])->all();
$before = $fingerprint();
$count = Requisition::query()->count();
app(RequisitionCatalogue::class)->save($publisher, $stock->id, [...$proposal['catalogue'], 'version' => 0,
'stock_basis' => app(RequisitionCatalogue::class)->basis($stock), 'reason' => 'Publicação proposta apenas para o ensaio sintético descartável.']);
$prepare = $actor('prepare');
$policy = $planning->save($prepare, 'policy', $envelope($proposal['policy']));
$review = $actor('review');
$tasks = app(WorkflowTaskInbox::class)->build($review, 'procurement');
$check(collect($tasks)->contains('id', 'supply:'.$policy->id.':review'), 'Política sem encaminhamento independente.');
$approved = $planning->save($review, 'policy', $envelope([], 'approve', $policy));
$automatic = app(AutomaticPurchases::class);
$prepare = $actor('prepare');
$rule = $automatic->save($prepare, $envelope([...$proposal['rule'], 'policy_key' => $approved->record_key]));
$review = $actor('review');
$check(collect(app(WorkflowTaskInbox::class)->build($review, 'procurement'))->contains('id', 'supply:'.$rule->id.':review'), 'Regra sem encaminhamento independente.');
$command = $envelope([], 'approve', $rule);
$automatic->save($review, $command);
$automatic->save($review, $command);
$automatic->evaluate($review, $stock->id, $warehouse->id);
$automatic->evaluate($review, $stock->id, $warehouse->id);
$check(Requisition::query()->count() === $count + 1, 'A proposta deve criar exactamente um rascunho.');
$purchase = Requisition::query()->latest('id')->firstOrFail();
$check($purchase->status === 'draft' && $purchase->requester_id === $proposal['actors']['requester'] && $purchase->steps()->count() === 0, 'Submissão/aprovações humanas têm de ficar pendentes.');
$check($purchase->items()->sole()->quantity === $proposal['expected']['quantity'], 'Quantidade gerada não coincide com a proposta.');
$check($purchase->subtotal_minor === 2000 && $purchase->tax_minor === 320 && $purchase->total_minor === 2320, 'Estimativa de custo diferente da proposta.');
$check(app(StockWorkflow::class)->balance($stock->id, $warehouse->id)['quantity'] === $proposal['expected']['physical_milli_after'], 'A criação do rascunho não pode alterar stock.');
$check($fingerprint() === $before, 'A simulação alterou acessos, aprovações, ordens ou stock.');
$requester = $actor('requester');
$check(collect(app(WorkflowTaskInbox::class)->build($requester, 'procurement'))->contains('id', 'purchase:'.$purchase->id.':draft'), 'Rascunho sem encaminhamento ao requisitante.');
$proof = ['checked_at' => now()->toIso8601String(), 'proposal_sha256' => hash_file('sha256', $proposalFile), 'result' => 'passed',
'database' => 'disposable restored copy of local synthetic SGC', 'source_business_commands' => false, 'accounts_or_grants_changed' => false,
'drafts_created_in_copy' => 1, 'quantity' => $purchase->items()->sole()->quantity, 'duplicate_approval_and_two_reevaluations' => 'one draft',
'estimated_total_minor_mzn' => $purchase->total_minor, 'physical_milli_after' => $proposal['expected']['physical_milli_after'],
'independent_policy_and_rule_tasks' => true, 'requester_task' => true, 'human_submission_and_approvals_pending' => true,
'existing_evaluation_context_fallback_reproduced_only_in_copy_runtime' => true,
'protected_tables_unchanged_in_copy' => $protected, 'sgf_installation_changed' => false, 'visual_review' => false];
}
} catch (Throwable $error) {
$failure = ['result' => 'failed', 'type' => get_class($error),
'locations' => array_map(fn ($frame) => ['file' => $frame['file'] ?? null, 'line' => $frame['line'] ?? null], array_slice($error->getTrace(), 0, 9))];
if ($error instanceof ValidationException) {
$failure['validation'] = $error->errors();
}
} finally {
DB::purge('pilot_clone');
if ($created) {
$admin->statement('DROP DATABASE "'.$clone.'" WITH (FORCE)');
}
if (is_file($dump) && dirname(realpath($dump)) === realpath($privateRoot)) {
unlink($dump);
}
}
if ($failure !== null) {
fwrite(STDERR, json_encode($failure, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR).PHP_EOL);
exit(1);
}
if ($proof !== null) {
$proof['database_and_private_copy_removed'] = true;
$proofName = in_array('--evaluations', $argv, true) ? 'pilot-evaluation-dry-run' : (in_array('--market', $argv, true) ? 'pilot-market-dry-run' : (in_array('--approval', $argv, true) ? 'pilot-approval-dry-run' : (in_array('--submission', $argv, true) ? 'pilot-submission-dry-run' : 'pilot-replenishment-dry-run')));
file_put_contents($root.'/docs/proofs/'.$proofName.'.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR).PHP_EOL);
echo 'Proposta ensaiada na cópia descartável; piloto sem comandos de negócio.'.PHP_EOL;
}