json_decode(ltrim(file_get_contents($root.'/docs/proofs/'.$name.'.json'), "\xEF\xBB\xBF"), true, 64, JSON_THROW_ON_ERROR); $proposal = $read('pilot-replenishment-proposal'); $tested = $read('pilot-replenishment-dry-run'); $authorization = $read('pilot-replenishment-authorization'); $hash = hash_file('sha256', $root.'/docs/proofs/pilot-replenishment-proposal.json'); $apply = in_array('--apply-authorized', $argv, true); $installCommands = in_array('--install-command-receipts', $argv, true); $submitPurchase = in_array('--submit-purchase', $argv, true); $approvePurchase = in_array('--approve-purchase', $argv, true); $marketScenario = in_array('--market', $argv, true); $evaluationScenario = in_array('--evaluations', $argv, true); $evaluation = $evaluationScenario ? $read('pilot-evaluation-proposal') : null; if ($evaluationScenario) { $evaluationHash = hash_file('sha256', $root.'/docs/proofs/pilot-evaluation-proposal.json'); $evaluationTested = $read('pilot-evaluation-dry-run'); $evaluationAuthorization = $read('pilot-evaluation-authorization'); if ($marketScenario || $approvePurchase || $submitPurchase || $installCommands || $evaluationTested['result'] !== 'passed' || $evaluationTested['proposal_sha256'] !== $evaluationHash || $evaluationAuthorization['proposal_sha256'] !== $evaluationHash || $evaluationAuthorization['environment'] !== $evaluation['environment'] || $evaluation['scope'] !== 'synthetic_local_pilot_only') { throw new LogicException('Avaliações exigem a proposta exacta ensaiada e autorizada.'); } } $market = $marketScenario ? $read('pilot-market-proposal') : null; if ($marketScenario) { $marketHash = hash_file('sha256', $root.'/docs/proofs/pilot-market-proposal.json'); $marketTested = $read('pilot-market-dry-run'); $marketAuthorization = $read('pilot-market-authorization'); if ($approvePurchase || $submitPurchase || $installCommands || $marketTested['result'] !== 'passed' || $marketTested['proposal_sha256'] !== $marketHash || $marketAuthorization['proposal_sha256'] !== $marketHash || $marketAuthorization['environment'] !== $market['environment'] || $market['scope'] !== 'synthetic_local_pilot_only' || $market['quotation_valid_until'] < date('Y-m-d')) { throw new LogicException('Consulta sintética exige a proposta exacta ensaiada e autorizada.'); } } $verifyApproved = in_array('--verify-approval-committed', $argv, true); if ($verifyApproved && ! $approvePurchase) { throw new LogicException('Verificação limitada à aprovação autorizada.'); } $approval = $approvePurchase ? $read('pilot-approval-proposal') : null; if ($approvePurchase) { $approvalHash = hash_file('sha256', $root.'/docs/proofs/pilot-approval-proposal.json'); $approvalAuthorization = $read('pilot-approval-authorization'); $approvalTested = $read('pilot-approval-dry-run'); if ($installCommands || $submitPurchase || $approvalTested['result'] !== 'passed' || $approvalTested['proposal_sha256'] !== $approvalHash || $approvalAuthorization['proposal_sha256'] !== $approvalHash || $approvalAuthorization['environment'] !== $approval['environment'] || $approval['action'] !== 'approve' || $approval['scope'] !== 'synthetic_local_pilot_only') { throw new LogicException('Aprovação exige a proposta exacta ensaiada e autorizada.'); } } $submission = $submitPurchase ? $read('pilot-submission-proposal') : null; if ($submitPurchase) { $submissionHash = hash_file('sha256', $root.'/docs/proofs/pilot-submission-proposal.json'); $submissionAuthorization = $read('pilot-submission-authorization'); $submissionTested = $read('pilot-submission-dry-run'); if ($installCommands || $submissionTested['result'] !== 'passed' || $submissionTested['proposal_sha256'] !== $submissionHash || $submissionAuthorization['proposal_sha256'] !== $submissionHash || $submissionAuthorization['owner_reply'] !== 'autorizo' || $submissionAuthorization['environment'] !== $submission['environment'] || $submission['action'] !== 'submit') { throw new LogicException('Submissão exige a proposta exacta ensaiada e autorizada.'); } } if ($tested['result'] !== 'passed' || $tested['proposal_sha256'] !== $hash || $authorization['proposal_sha256'] !== $hash || $authorization['environment'] !== 'http://127.0.0.1:8005' || $proposal['environment'] !== $authorization['environment'] || $proposal['scope'] !== 'synthetic_local_pilot_only' || $proposal['rule']['valid_until'] < date('Y-m-d')) { throw new LogicException('Proposta, ensaio e autorização têm de coincidir e estar vigentes.'); } $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' || 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('Aplicação exclusivamente na avaliação local autorizada.'); } $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('Restauro de controlo exclusivamente 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.replenishment_apply' => $qa, 'database.connections.replenishment_owner' => [...$owner, 'url' => null, 'database' => 'postgres'], 'app.env' => 'payable-testing', 'work_context.composition' => 'sgc']); $app->detectEnvironment(fn () => 'payable-testing'); DB::setDefaultConnection('replenishment_apply'); $db = DB::connection(); $identity = $db->selectOne('select current_database() as db, current_user as role'); if ($identity->db !== $settings['DB_DATABASE'] || $identity->role !== $settings['DB_USERNAME']) { throw new LogicException('Destino diferente do autorizado.'); } $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()); $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; }; $check = function (bool $valid, string $message) { if (! $valid) { throw new LogicException($message); } }; $fingerprint = function ($connection, array $tables) { $result = []; foreach ($tables as $table) { $quoted = '"'.str_replace('"', '""', $table).'"'; $rows = $connection->select('select row_to_json(t)::text as row from '.$quoted.' t order by row_to_json(t)::text'); $result[$table] = hash('sha256', json_encode(array_column($rows, 'row'), JSON_THROW_ON_ERROR)); } return $result; }; $pg = function (string $binary, array $arguments, array $connection) { $process = new Process(['C:/Program Files/PostgreSQL/17/bin/'.$binary.'.exe', ...$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 de controlo não concluído.'); } }; $restore = null; $failure = null; $admin = DB::connection('replenishment_owner'); try { $result = $db->transaction(function () use ($db, $actor, $check, $proposal, $hash, $apply, $installCommands, $submitPurchase, $submission, $approvePurchase, $verifyApproved, $approval, $marketScenario, $market, $evaluationScenario, $evaluation, $app, $fingerprint, $pg, $qa, $owner, $admin, &$restore) { DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ'); app(ConflictGuard::class)->lockCompany($actor('catalogue')); $check((int) DB::table('organization_access')->where('tenant_id', $proposal['tenant_id'])->lockForUpdate()->value('version') === $proposal['expected_access_version'], 'Os acessos mudaram.'); $stock = StockItem::query()->lockForUpdate()->findOrFail($proposal['source']['stock_item_id']); $warehouse = Warehouse::query()->findOrFail($proposal['source']['warehouse_id']); $check($stock->only(['code', 'name', 'unit']) === array_intersect_key($proposal['source'], array_flip(['code', 'name', 'unit'])) && $warehouse->code === $proposal['source']['warehouse_code'], 'Cadastro 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.'); $planning = app(SupplyPlanning::class); if ($evaluationScenario) { $purchase = Requisition::query()->lockForUpdate()->findOrFail($evaluation['purchase_id']); $quotes = Quotation::query()->where('requisition_id', $purchase->id)->orderBy('id')->get(); $check($purchase->number === $evaluation['purchase_number'] && $purchase->status === 'approved' && $quotes->pluck('id')->all() === $evaluation['quotation_ids'] && $quotes->sum(fn ($quote) => $quote->evaluations()->count()) === 0, 'Compra, propostas ou avaliações já alteradas; não reaplicar.'); } elseif ($marketScenario) { $purchase = Requisition::query()->lockForUpdate()->findOrFail($market['purchase_id']); $check($purchase->number === $market['purchase_number'] && $purchase->status === 'approved' && $purchase->total_minor === 2320 && Quotation::query()->where('requisition_id', $purchase->id)->count() === 0, 'Consulta já iniciada ou compra alterada; não duplicar.'); $check(! Supplier::query()->whereIn('nuit', array_column($market['new_synthetic_suppliers'], 'nuit'))->exists(), 'Cadastro sintético já existe; não reaplicar.'); } elseif ($approvePurchase) { $purchase = Requisition::query()->lockForUpdate()->findOrFail($approval['purchase_id']); $check($purchase->number === $approval['purchase_number'] && $purchase->status === ($verifyApproved ? 'approved' : 'pending') && $purchase->items()->sole()->quantity === $approval['quantity'] && $purchase->total_minor === $approval['total_minor_mzn'] && $purchase->steps()->count() === 1 && $purchase->steps()->sole()->role === $approval['current_role'], 'Compra ou etapa diferente da proposta.'); } elseif ($submitPurchase) { $purchase = Requisition::query()->lockForUpdate()->findOrFail($submission['purchase_id']); $check($purchase->number === $submission['purchase_number'] && $purchase->requester_id === $submission['requester_id'] && $purchase->status === 'draft' && $purchase->items()->sole()->quantity === $submission['quantity'] && $purchase->total_minor === $submission['total_minor_mzn'] && $purchase->steps()->count() === 0, 'Rascunho diferente da proposta; não repetir submissão.'); } elseif ($installCommands) { $testedSubmission = json_decode(file_get_contents(base_path('docs/proofs/pilot-submission-dry-run.json')), true, 32, JSON_THROW_ON_ERROR); $check($testedSubmission['result'] === 'passed' && $testedSubmission['command_receipt_migration_tested_in_copy'] === true, 'Instalação tem de estar ensaiada na cópia.'); $check(! Schema::hasTable('requisition_commands') && ! Schema::hasColumn('requisitions', 'process_version'), 'Instalação já existe ou está parcial; não reaplicar.'); } else { $check(! DB::table('requisition_catalogue')->where('tenant_id', $proposal['tenant_id'])->where('stock_item_id', $stock->id)->exists(), 'Artigo já publicado/configurado; não reaplicar.'); foreach (['policy', 'auto_rule'] as $kind) { $check(! $planning->latest($kind)->where('stock_item_id', $stock->id)->where('warehouse_id', $warehouse->id)->exists(), 'Já existe parametrização; não duplicar.'); } } if (! $apply) { return ['result' => 'preflight_passed_no_changes', 'proposal_sha256' => $hash]; } $tables = array_map(fn ($r) => $r->tablename, $db->select("select tablename from pg_tables where schemaname='public' order by tablename")); $baseline = $fingerprint($db, $tables); $protected = array_values(array_diff($tables, ['requisition_catalogue', 'supply_records', 'requisitions', 'requisition_items', 'audit_logs', 'replenishment_work'])); if ($verifyApproved) { // Original consistent pre-decision backup, retained when a post-commit callback failed. $dump = 'C:/laragon/data/sgf-tempo/backups/compras-pilot-replenishment-20261102-193430-6b25f8/before.dump'; $check(is_file($dump), 'Cópia original de recuperação não disponível.'); } else { $directory = 'C:/laragon/data/sgf-tempo/backups/compras-pilot-replenishment-'.gmdate('Ymd-His').'-'.bin2hex(random_bytes(3)); $check(mkdir($directory, 0700, true), 'Não foi possível preparar a cópia de recuperação.'); $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); } $restore = 'compras_pilot_restore_'.bin2hex(random_bytes(6)); $admin->statement('CREATE DATABASE "'.$restore.'"'); $pg('pg_restore', ['--no-owner', '--no-privileges', '--exit-on-error', '--dbname='.$restore, $dump], $owner); config(['database.connections.replenishment_restore' => [...$owner, 'url' => null, 'database' => $restore]]); $restoredBaseline = $fingerprint(DB::connection('replenishment_restore'), $tables); if ($verifyApproved) { $baseline = $restoredBaseline; } else { $check($restoredBaseline === $baseline, 'Restauro diferente da origem.'); } if ($evaluationScenario) { $evaluationTable = (new ProposalEvaluation)->getTable(); $protected = array_values(array_diff($tables, [$evaluationTable, 'audit_logs'])); foreach ($evaluation['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($evaluation['environment'].'/cotacoes/'.$quote->id.'/avaliar', 'POST', $evaluation['grades_for_each_quotation_and_each_evaluator']); $input->setUserResolver(fn () => $evaluator); app(QuotationController::class)->evaluate($input, $quote, app(RequisitionWorkflow::class)); } } $records = ProposalEvaluation::query()->whereIn('quotation_id', $evaluation['quotation_ids'])->orderBy('id')->get(); $check($records->count() === 6 && $records->every(fn ($record) => in_array($record->evaluator_id, $evaluation['evaluators'], true) && (string) $record->quality === '5.00' && (string) $record->history === '5.00' && (string) $record->payment === '5.00'), 'Avaliações diferentes das autorizadas.'); $check($fingerprint($db, $protected) === array_intersect_key($baseline, array_flip($protected)), 'Alteração fora da avaliação autorizada; cancelar.'); $oldEvaluations = DB::connection('replenishment_restore')->table($evaluationTable)->orderBy('id')->get()->toJson(); $check(DB::table($evaluationTable)->whereNotIn('id', $records->modelKeys())->orderBy('id')->get()->toJson() === $oldEvaluations, 'Avaliações anteriores alteradas.'); $preparer = $actor('prepare'); $comparison = app(ProposalComparison::class)->inspect($purchase); $check($comparison['ready'] && collect(app(WorkflowTaskInbox::class)->build($preparer, 'procurement'))->contains('id', 'quotation:'.$purchase->id.':result'), 'Sem encaminhamento da comparação para conferência humana.'); return ['result' => 'evaluations_registered', 'registered_at' => now()->toIso8601String(), 'environment' => $evaluation['environment'], 'proposal_sha256' => hash_file('sha256', base_path('docs/proofs/pilot-evaluation-proposal.json')), 'purchase_id' => $purchase->id, 'evaluation_ids' => $records->modelKeys(), 'evaluators' => $evaluation['evaluators'], 'evaluations' => 6, 'grades' => '5.00', 'comparison_ready' => true, 'human_comparison_review_task' => true, 'supplier_selection_award_or_order' => false, 'outbound_communication' => false, 'backup' => $dump, 'backup_sha256' => hash_file('sha256', $dump), 'restored_tables_checked' => count($tables), 'protected_tables_checked' => count($protected), 'accounts_or_grants_changed' => false, 'sgf_installation_changed' => false]; } if ($marketScenario) { $protected = array_values(array_diff($tables, ['suppliers', 'quotations', 'quotation_items', 'audit_logs'])); require_once __DIR__.'/support/pilot-market-scenario.php'; $scenario = pilotMarketScenario($app, $proposal, $market, $actor, $check); $check($fingerprint($db, $protected) === array_intersect_key($baseline, array_flip($protected)), 'Alteração fora da consulta autorizada; cancelar.'); $oldQuotes = DB::connection('replenishment_restore')->table('quotations')->orderBy('id')->get()->toJson(); $check(DB::table('quotations')->whereNotIn('id', $scenario['quotation_ids'])->orderBy('id')->get()->toJson() === $oldQuotes, 'Propostas anteriores alteradas.'); return ['result' => 'market_registered', 'registered_at' => now()->toIso8601String(), 'environment' => $market['environment'], 'proposal_sha256' => hash_file('sha256', base_path('docs/proofs/pilot-market-proposal.json')), 'purchase_id' => $purchase->id, 'supplier_ids' => $scenario['supplier_ids'], 'quotation_ids' => $scenario['quotation_ids'], 'totals_minor_mzn' => [2320, 2552, 2784], 'evaluations' => 0, 'supplier_selection_award_or_order' => false, 'outbound_communication' => false, 'backup' => $dump, 'backup_sha256' => hash_file('sha256', $dump), 'restored_tables_checked' => count($tables), 'protected_tables_checked' => count($protected), 'accounts_or_grants_changed' => false, 'sgf_installation_changed' => false]; } if ($approvePurchase) { $protected = array_values(array_diff($tables, ['requisitions', 'approval_steps', 'requisition_commands', 'audit_logs', 'workflow_notices', 'replenishment_work'])); $othersBefore = DB::table('requisitions')->where('id', '!=', $purchase->id)->orderBy('id')->get()->toJson(); $approver = User::query()->where('tenant_id', $proposal['tenant_id'])->findOrFail($approval['approver_id']); auth()->guard('web')->setUser($approver); request()->session()->put('work_context', ['key' => 'procurement.internal', 'user_id' => $approver->id, 'tenant_id' => $approver->tenant_id]); if ($verifyApproved) { $receipt = DB::table('requisition_commands')->where('tenant_id', $approver->tenant_id)->where('actor_id', $approver->id)->get() ->filter(fn ($row) => (json_decode($row->result, true, 64, JSON_THROW_ON_ERROR)['id'] ?? null) === $purchase->id)->sole(); $recorded = json_decode($receipt->result, true, 64, JSON_THROW_ON_ERROR); $check($recorded['status'] === 'approved' && $purchase->steps()->sole()->decision === 'approved' && $purchase->steps()->sole()->comment === $approval['decision_comment'], 'Recibo ou decisão diferente da autorização.'); $commandKey = $receipt->operation_key; $othersBefore = DB::connection('replenishment_restore')->table('requisitions')->where('id', '!=', $purchase->id)->orderBy('id')->get()->toJson(); } else { $check($purchase->canDecide($approver), 'A competência actual não permite a decisão.'); $commandKey = (string) Str::uuid(); app(RequisitionWorkflow::class)->decide($approver, $purchase, 'approved', $approval['decision_comment'], $commandKey, (int) $purchase->process_version); } $purchase->refresh(); $check($purchase->status === 'approved' && $purchase->steps()->sole()->approver_id === $approver->id && $fingerprint($db, $protected) === array_intersect_key($baseline, array_flip($protected)) && DB::table('requisitions')->where('id', '!=', $purchase->id)->orderBy('id')->get()->toJson() === $othersBefore, 'Alteração fora da decisão autorizada; cancelar.'); $check(collect(app(WorkflowTaskInbox::class)->build($actor('prepare'), 'procurement'))->contains('id', 'quotation:'.$purchase->id.':prepare'), 'Sem encaminhamento para consulta ao mercado.'); // Keep the command actor until after-commit callbacks have finished. auth()->guard('web')->setUser($approver); request()->session()->put('work_context', ['key' => 'procurement.internal', 'user_id' => $approver->id, 'tenant_id' => $approver->tenant_id]); return ['result' => 'approved', 'approved_at' => (string) $purchase->steps()->sole()->decided_at, 'verified_at' => now()->toIso8601String(), 'environment' => $approval['environment'], 'proposal_sha256' => hash_file('sha256', base_path('docs/proofs/pilot-approval-proposal.json')), 'purchase_id' => $purchase->id, 'purchase_number' => $purchase->number, 'status' => $purchase->status, 'approved_by' => $approver->id, 'quantity' => $purchase->items()->sole()->quantity, 'total_minor_mzn' => $purchase->total_minor, 'market_consultation_task' => true, 'budget_entries_preserved' => true, 'backup' => $dump, 'backup_sha256' => hash_file('sha256', $dump), 'restored_tables_checked' => count($tables), 'protected_tables_checked' => count($protected), 'command_key' => $commandKey, 'verified_existing_committed_receipt_without_replay' => $verifyApproved, 'supplier_selection_or_order' => false, 'accounts_or_grants_changed' => false, 'sgf_installation_changed' => false]; } if ($submitPurchase) { $protected = array_values(array_diff($tables, ['requisitions', 'approval_steps', 'requisition_commands', 'audit_logs', 'workflow_notices', 'budget_entries', 'replenishment_work'])); $otherRequests = fn () => DB::table('requisitions')->where('id', '!=', $purchase->id)->orderBy('id')->get()->toJson(); $othersBefore = $otherRequests(); $budgetBefore = DB::table('budget_entries')->orderBy('id')->get()->toJson(); $workOutside = fn () => DB::table('replenishment_work')->whereNot(fn ($query) => $query->where('tenant_id', $proposal['tenant_id'])->where('stock_item_id', $stock->id)->where('warehouse_id', $warehouse->id))->orderBy('id')->get()->toJson(); $workOutsideBefore = $workOutside(); $beforeCount = Requisition::query()->count(); $requester = $actor('requester'); $check($requester->id === $submission['requester_id'], 'Requisitante diferente do autorizado.'); $commandKey = (string) Str::uuid(); app(RequisitionWorkflow::class)->submit($requester, $purchase, $commandKey, (int) $purchase->process_version); $purchase->refresh(); $check($purchase->status === 'pending' && $purchase->steps()->count() > 0 && $purchase->steps()->where('decision', '!=', 'pending')->count() === 0, 'As decisões têm de permanecer pendentes.'); $protectedAfter = $fingerprint($db, $protected); $changed = array_keys(array_diff_assoc($protectedAfter, array_intersect_key($baseline, array_flip($protected)))); $check(Requisition::query()->count() === $beforeCount && $otherRequests() === $othersBefore && $changed === [], 'Alteração fora da submissão autorizada; cancelar. Tabelas: '.implode(', ', $changed)); $check(DB::table('budget_entries')->where('requisition_id', '!=', $purchase->id)->orderBy('id')->get()->toJson() === $budgetBefore && $workOutside() === $workOutsideBefore, 'Movimentos de outro processo alterados.'); $reservation = DB::table('budget_entries')->where('requisition_id', $purchase->id)->sole(); $check($reservation->kind === 'reserve' && (int) $reservation->created_by === $requester->id && (int) $reservation->committed_minor === 0 && (int) $reservation->reserved_minor > 0 && (int) $reservation->reserved_minor <= $purchase->total_minor, 'Reserva orçamental incompatível com a submissão.'); $routing = app(ApprovalRouting::class)->current($purchase, $requester); $check($routing['status'] === 'available' && array_column($routing['people'], 'id') === [10], 'Encaminhamento diferente do ensaiado.'); return ['result' => 'submitted', 'submitted_at' => now()->toIso8601String(), 'environment' => $submission['environment'], 'proposal_sha256' => hash_file('sha256', base_path('docs/proofs/pilot-submission-proposal.json')), 'purchase_id' => $purchase->id, 'purchase_number' => $purchase->number, 'status' => $purchase->status, 'quantity' => $purchase->items()->sole()->quantity, 'total_minor_mzn' => $purchase->total_minor, 'current_role' => $routing['role'], 'eligible_people' => $routing['people'], 'all_approval_steps_pending' => true, 'command_key' => $commandKey, 'backup' => $dump, 'backup_sha256' => hash_file('sha256', $dump), 'budget_reserved_minor' => (int) $reservation->reserved_minor, 'budget_committed_minor' => 0, 'other_budget_entries_and_replenishment_scopes_preserved' => true, 'restored_tables_checked' => count($tables), 'protected_tables_checked' => count($protected), 'other_requisitions_preserved' => true, 'accounts_or_grants_changed' => false, 'sgf_installation_changed' => false]; } if ($installCommands) { $migration = 'database/migrations/2026_11_01_000046_add_requisition_command_versions.php'; $protected = array_values(array_diff($tables, ['requisitions', 'migrations'])); $requisitionProjection = fn () => array_column($db->select("select (to_jsonb(t) - 'process_version')::text as row from requisitions t order by (to_jsonb(t) - 'process_version')::text"), 'row'); $beforeRequisitions = $requisitionProjection(); $check(Artisan::call('migrate', ['--database' => 'replenishment_apply', '--path' => $migration, '--force' => true]) === 0, 'Actualização técnica não concluída.'); $check(Schema::hasTable('requisition_commands') && Schema::hasColumn('requisitions', 'process_version') && DB::table('requisition_commands')->count() === 0, 'Estrutura de comandos inesperada.'); $check($requisitionProjection() === $beforeRequisitions && DB::table('requisitions')->where('process_version', '!=', 1)->count() === 0 && $fingerprint($db, $protected) === array_intersect_key($baseline, array_flip($protected)), 'A instalação alterou registos existentes; cancelar.'); $check((int) $db->selectOne("select count(*) as total from pg_trigger where tgrelid='requisition_commands'::regclass and not tgisinternal and tgname in ('rq_command_update','rq_command_delete')")->total === 2, 'Protecções de imutabilidade não instaladas.'); return ['result' => 'installed', 'installed_at' => now()->toIso8601String(), 'environment' => $proposal['environment'], 'migration' => $migration, 'backup' => $dump, 'backup_sha256' => hash_file('sha256', $dump), 'restored_tables_checked' => count($tables), 'protected_tables_checked' => count($protected), 'existing_requisitions_preserved_except_initial_process_version' => true, 'command_receipts' => 0, 'immutability_triggers' => 2, 'business_commands' => false, 'accounts_or_grants_changed' => false, 'sgf_installation_changed' => false]; } $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' => 'Proposta de reposição sintética SGC8005 autorizada expressamente pelo proprietário neste chat em 02/11/2026.', 'data' => $data]; $count = Requisition::query()->count(); app(RequisitionCatalogue::class)->save($actor('catalogue'), $stock->id, [...$proposal['catalogue'], 'version' => 0, 'stock_basis' => app(RequisitionCatalogue::class)->basis($stock), 'reason' => 'Publicação sintética ART-TEST expressamente autorizada pelo proprietário para o piloto local.']); $policy = $planning->save($actor('prepare'), 'policy', $envelope($proposal['policy'])); $policy = $planning->save($actor('review'), 'policy', $envelope([], 'approve', $policy)); $automatic = app(AutomaticPurchases::class); $rule = $automatic->save($actor('prepare'), $envelope([...$proposal['rule'], 'policy_key' => $policy->record_key])); $review = $actor('review'); $rule = $automatic->save($review, $envelope([], 'approve', $rule)); $check(Requisition::query()->count() === $count + 1, 'Esperado exactamente um novo rascunho.'); $purchase = Requisition::query()->latest('id')->firstOrFail(); $check($purchase->status === 'draft' && $purchase->requester_id === $proposal['actors']['requester'] && $purchase->steps()->count() === 0 && $purchase->items()->sole()->quantity === $proposal['expected']['quantity'] && $purchase->subtotal_minor === 2000 && $purchase->tax_minor === 320 && $purchase->total_minor === 2320, 'Rascunho diferente da proposta.'); $check($fingerprint($db, $protected) === array_intersect_key($baseline, array_flip($protected)), 'Alteração fora do âmbito autorizado; cancelar aplicação.'); $check(collect(app(WorkflowTaskInbox::class)->build($actor('requester'), 'procurement'))->contains('id', 'purchase:'.$purchase->id.':draft'), 'Sem encaminhamento ao requisitante.'); return ['result' => 'applied', 'applied_at' => now()->toIso8601String(), 'proposal_sha256' => $hash, 'environment' => $proposal['environment'], 'policy_id' => $policy->id, 'rule_id' => $rule->id, 'purchase_id' => $purchase->id, 'purchase_number' => $purchase->number, 'quantity' => $purchase->items()->sole()->quantity, 'total_minor_mzn' => $purchase->total_minor, 'purchase_status' => $purchase->status, 'physical_milli_after' => $balance['physical_milli'], 'human_submission_and_approvals_pending' => true, 'protected_tables_checked' => count($protected), 'backup' => $dump, 'backup_sha256' => hash_file('sha256', $dump), 'restored_tables_checked' => count($tables), 'accounts_or_grants_changed' => false, 'sgf_installation_changed' => false]; }); } catch (Throwable $error) { $failure = 'Aplicação não concluída: '.get_class($error).($error instanceof LogicException ? ' '.$error->getMessage() : ''); $failure .= ' '.json_encode(array_map(fn ($frame) => ['file' => $frame['file'] ?? null, 'line' => $frame['line'] ?? null], array_slice($error->getTrace(), 0, 8)), JSON_THROW_ON_ERROR); } finally { DB::purge('replenishment_restore'); if ($restore !== null && preg_match('/^compras_pilot_restore_[a-f0-9]{12}$/D', $restore)) { $admin->statement('DROP DATABASE IF EXISTS "'.$restore.'" WITH (FORCE)'); } } if ($failure !== null) { fwrite(STDERR, $failure.PHP_EOL); exit(1); } if ($apply) { $result['disposable_restore_removed'] = true; $proofName = $evaluationScenario ? 'pilot-evaluation-activation' : ($marketScenario ? 'pilot-market-activation' : ($approvePurchase ? 'pilot-approval-activation' : ($submitPurchase ? 'pilot-submission-activation' : ($installCommands ? 'pilot-command-receipts-installation' : 'pilot-replenishment-activation')))); file_put_contents($root.'/docs/proofs/'.$proofName.'.json', json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR).PHP_EOL); } echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR).PHP_EOL;