This commit is contained in:
2026-11-03 11:51:44 +02:00
parent badaada043
commit e753f7a0a4
309 changed files with 14537 additions and 529 deletions
+25
View File
@@ -0,0 +1,25 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const records=[];
for(const [role,expected] of [['admin',200],['procurement',200],['finance',200],['warehouse',403]]) {
const viewer=client(); await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const result=await viewer.request('/compras/fornecimento?view=calendar&agreement_form_key=prepare:new');
assert.equal(result.status,expected);
if(expected===200) {
assert.equal(result.page.encryptHistory,true);
assert.equal(result.page.props.agreementFormKey,'prepare:new');
if(!result.page.props.canPrepare) assert.deepEqual(result.page.props.agreements,[]);
assert.equal((await viewer.request('/compras/fornecimento?view=calendar&agreement_search=TEST')).status,422);
}
records.push({role,status:result.status});
}
await fs.writeFile('docs/proofs/agreement-source-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_changed:false,visual_review:false,
coverage:'Existing pilot access and required source scope; positive old source selection tested in disposable databases.'},null,2));
console.log('Escolha de acordos conferida em leitura no piloto.');
` + '})()')(fs, assert);
@@ -0,0 +1,25 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const routes=[];
for(const [role,expected] of [['admin',200],['procurement',403],['finance',403]]) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const response=await viewer.request('/compras/fornecimento?view=alternatives');assert.equal(response.status,expected);
if(expected===200) {
assert.equal(response.page.component,'supply/coordination');assert.equal(response.page.encryptHistory,true);
assert.equal(response.page.props.records.length,0);assert.equal(response.page.props.canPrepare,false);
assert.equal(response.page.props.canReview,false);
}
const tasks=await viewer.request('/requisicoes/tarefas?module=supply');assert.equal(tasks.status,200);
const pending=tasks.page.props.tasks.data.filter(row=>row.task.startsWith('alternative_'));assert.equal(pending.length,0);
routes.push({role,alternatives_status:response.status,alternative_tasks:pending.length});
}
await fs.writeFile('docs/proofs/alternative-task-integration-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',routes,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'Live synthetic scope has no technical alternatives or specific execution grants; technical review, independence and source changes tested in disposable databases.'},null,2));
console.log('Alternativas tecnicas e tarefas conferidas, preservando competencias especificas.');
` + '})()')(fs, assert);
@@ -0,0 +1,400 @@
<?php
use App\Http\Controllers\QuotationController;
use App\Models\ProposalEvaluation;
use App\Models\Quotation;
use App\Models\Requisition;
use App\Models\StockItem;
use App\Models\Supplier;
use App\Models\SupplyRecord;
use App\Models\User;
use App\Models\Warehouse;
use App\Services\ApprovalRouting;
use App\Services\AutomaticPurchases;
use App\Services\ConflictGuard;
use App\Services\ProposalComparison;
use App\Services\RequisitionCatalogue;
use App\Services\RequisitionWorkflow;
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 Symfony\Component\Process\Process;
require dirname(__DIR__).'/vendor/autoload.php';
$root = dirname(__DIR__);
$read = fn ($name) => 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;
+27
View File
@@ -0,0 +1,27 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const [role,expected] of [['admin',200],['procurement',200],['finance',200],['warehouse',403]]) {
const viewer=client(); await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const baseReport=await viewer.request('/relatorios');assert.equal(baseReport.status,expected);
for(const [attention,fact] of [['approved_without_quotes','without_registered_quotes'],['approved_without_order','without_visible_order']]) {
const result=await viewer.request('/relatorios?attention='+attention);assert.equal(result.status,expected);
if(expected===200) {
const props=result.page.props;
assert.equal(result.page.encryptHistory,true);assert.equal(props.filters.attention,attention);
assert.equal(props.records.total,baseReport.page.props.insights.approved[fact]);
assert.ok(props.records.data.every(row=>row.status==='approved'&&(attention==='approved_without_quotes'?row.quotations_count===0:row.order_number===null)));
assert.equal((await viewer.request('/relatorios/compras.csv?attention='+attention,'GET',undefined,false)).status,200);
}
records.push({role,attention,status:result.status});
}
}
await fs.writeFile('docs/proofs/approved-attention-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_changed:false,visual_review:false,
coverage:'Existing pilot facts and authorised CSV; populated overlap, registered expired proposals and pagination tested in disposable databases.'},null,2));
console.log('Consultas de aprovadas conferidas sem novas decisões no piloto.');
`+'})()')(fs,assert);
+33
View File
@@ -0,0 +1,33 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='))
.replace('async function request(path, method = \'GET\', data, inertia = true)', 'async function request(path, method = \'GET\', data, inertia = true, extraHeaders = {})')
.replace('headers: {', 'headers: { ...extraHeaders,');
assert.ok(common.includes('extraHeaders = {}'));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const role of ['admin','procurement','finance','warehouse']) {
const viewer=client(); await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const result=await viewer.request('/requisicoes/2');
if(result.status===200) {
assert.equal(result.page.encryptHistory,true);
assert.ok(result.page.props.audit.length<=25);
assert.ok(result.page.props.auditPagination.total>=result.page.props.audit.length);
const partial=await viewer.request('/requisicoes/2?audit_page=1','GET',undefined,true,{'X-Inertia-Partial-Component':'requisitions/show','X-Inertia-Partial-Data':'audit,auditPagination'});
assert.equal(partial.status,200);
assert.ok(Array.isArray(partial.page.props.audit));
assert.ok(partial.page.props.auditPagination);
assert.equal(partial.page.props.requisition,undefined);
assert.equal((await viewer.request('/requisicoes/2?audit_page=0')).status,422);
assert.equal((await viewer.request('/requisicoes/2/historico.csv','GET',undefined,false)).status,200);
} else assert.equal(result.status,404);
if(role==='admin') assert.equal(result.status,200);
records.push({role,status:result.status});
}
await fs.writeFile('docs/proofs/audit-pagination-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_changed:false,visual_review:false,
coverage:'Existing synthetic requisition 2: scoped detail, partial audit pagination and full CSV access.'},null,2));
console.log('Paginação parcial do histórico conferida sem comandos de negócio no piloto.');
`+'})()')(fs,assert);
+26
View File
@@ -0,0 +1,26 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const records=[];
for(const [role,expected] of [['admin',200],['warehouse',403],['procurement',403],['finance',403]]) {
const viewer=client(); await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
for(const view of ['reservations','deliveries','alternatives']) {
const result=await viewer.request('/compras/fornecimento.csv?view='+view+'&record_search=TEST');
assert.equal(result.status,expected);
if(expected===200) {
assert.ok(result.text.includes('Acompanhamento de fornecimentos'));
assert.ok(result.text.includes('Designação preservada'));
assert.equal((await viewer.request('/compras/fornecimento.csv?view='+view+'&record_state=unknown')).status,422);
}
records.push({role,view,status:result.status});
}
}
await fs.writeFile('docs/proofs/coordination-export-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_changed:false,visual_review:false,
coverage:'Existing pilot export access; populated exports and native quantities tested in disposable databases.'},null,2));
console.log('Exportação conferida sem comandos de negócio no piloto.');
` + '})()')(fs, assert);
+28
View File
@@ -0,0 +1,28 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const records=[];
for(const [role,expected] of [['admin',200],['warehouse',403],['procurement',403],['finance',403]]) {
const viewer=client(); await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
for(const view of ['reservations','deliveries','alternatives']) {
const result=await viewer.request('/compras/fornecimento?view='+view+'&record_search=TEST');
assert.equal(result.status,expected);
if(expected===200) {
assert.equal(result.page.encryptHistory,true);
assert.equal(result.page.props.canPrepare,false);
assert.equal(result.page.props.recordFilters.record_search,'TEST');
assert.equal((await viewer.request('/compras/fornecimento?view='+view+'&record_search='+'A'.repeat(101))).status,422);
assert.equal((await viewer.request('/compras/fornecimento?view='+view+'&record_state=unknown')).status,422);
}
records.push({role,view,status:result.status});
}
}
await fs.writeFile('docs/proofs/coordination-filters-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_changed:false,visual_review:false,
coverage:'Existing pilot access and validation; positive record filtering verified in disposable databases.'},null,2));
console.log('Consulta filtrada conferida no piloto sem comandos de negócio.');
` + '})()')(fs, assert);
@@ -0,0 +1,31 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source=await fs.readFile('scripts/supplier-evaluation-check.mjs','utf8');
const common=source.slice(source.indexOf('const base ='),source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const role of ['admin','warehouse','procurement','finance']) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const pages=[];
for(const view of ['reservations','deliveries','alternatives']) {
const result=await viewer.request('/compras/fornecimento?view='+view);
assert.ok([200,403].includes(result.status));if(role==='admin') assert.equal(result.status,200);
if(result.status===200) {
assert.equal(result.page.encryptHistory,true);
assert.equal(result.page.component,'supply/coordination');
assert.ok(result.page.props.recordPagination);assert.ok(result.page.props.records.length<=25);
if(role==='admin') assert.equal(result.page.props.canPrepare,false);
}
pages.push({view,status:result.status,can_prepare:result.page?.props?.canPrepare??null});
}
const tasks=await viewer.request('/requisicoes/tarefas?module=supply');assert.equal(tasks.status,200);
const followups=tasks.page.props.tasks.data.filter(r=>r.task==='reservation_followup'||r.task==='delivery_followup');
records.push({role,pages,first_page_followup_tasks:followups.length});
}
await fs.writeFile('docs/proofs/coordination-task-integration-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'Read-only pilot access; partial/full receipts, allocations and corrections exercised in disposable databases.'},null,2));
console.log('Acompanhamento e paginação conferidos sem criar previsões, reservas ou movimentos no piloto.');
`+'})()')(fs,assert);
+25
View File
@@ -0,0 +1,25 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source=await fs.readFile('scripts/supplier-evaluation-check.mjs','utf8');
const common=source.slice(source.indexOf('const base ='),source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const [role,expected] of [['admin',200],['warehouse',200],['procurement',403],['finance',403]]) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
for(const path of ['/stock/ocorrencias','/stock/estornos']) {
const result=await viewer.request(path+'?source_search=TEST&source_form_key=prepare:new');assert.equal(result.status,expected);
if(expected===200) {
assert.equal(result.page.encryptHistory,true);assert.equal(result.page.props.capabilities.prepare,false);
assert.deepEqual(result.page.props.receipts,[]);if(path==='/stock/estornos') assert.deepEqual(result.page.props.operations,[]);
assert.equal((await viewer.request(path+'?source_search='+ 'A'.repeat(101))).status,422);
}
records.push({role,path,status:result.status});
}
}
await fs.writeFile('docs/proofs/correction-source-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'Existing pilot read rights; positive old origins and native reversals tested in disposable databases.'},null,2));
console.log('Pesquisa de origens de correcção conferida sem decisões, concessões ou movimentos no piloto.');
`+'})()')(fs,assert);
+21
View File
@@ -0,0 +1,21 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source=await fs.readFile('scripts/supplier-evaluation-check.mjs','utf8');
const common=source.slice(source.indexOf('const base ='),source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const role of ['admin','procurement','finance']) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const page=await viewer.request('/compras/fornecimento?view=calendar&history_stock_item_id=1&history_warehouse_id=1&history_purpose=internal&history_period_from=2026-10-01&history_period_until=2026-10-31&history_form_key=prepare:new');
assert.equal(page.status,200);assert.equal(page.page.encryptHistory,true);assert.equal(page.page.props.historyFormKey,'prepare:new');
if(!page.page.props.canPrepare) assert.deepEqual(page.page.props.histories,[]);
assert.equal((await viewer.request('/compras/fornecimento?view=calendar&history_period_from=2026-10-01')).status,422);
records.push({role,can_prepare:page.page.props.canPrepare,source_choices:page.page.props.histories.length});
}
await fs.writeFile('docs/proofs/history-source-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'Existing pilot scope and lookup validation; approved historical periods and suspensions tested in disposable databases.'},null,2));
console.log('Pesquisa de análises conferida sem criar origens, decisões ou acessos no piloto.');
`+'})()')(fs,assert);
+31
View File
@@ -0,0 +1,31 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const routes=[];
for (const [role, expected] of [['admin',200],['procurement',403],['finance',403]]) {
const viewer=client(); await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const history=await viewer.request('/compras/fornecimento?view=history'); assert.equal(history.status,expected);
if (expected===200) {
assert.equal(history.page.component,'supply/history'); assert.equal(history.page.encryptHistory,true);
assert.equal(history.page.props.canPrepare,false); assert.equal(history.page.props.canReview,false); assert.equal(history.page.props.records.length,0);
}
const tasks=await viewer.request('/requisicoes/tarefas?module=supply'); assert.equal(tasks.status,200);
const historical=tasks.page.props.tasks.data.filter(row=>row.task==='history_review'); assert.equal(historical.length,0);
routes.push({role,history_status:history.status,history_tasks:historical.length});
if (role==='admin') {
const pilot=await viewer.request('/stock/piloto'); assert.equal(pilot.status,200);
const r=pilot.page.props.report; assert.equal(r.tables.requisition_commands,true); assert.equal(r.tables.requisition_process_version,true);
assert.equal(r.checks.find(row=>row.key==='installation').ok,true);
assert.equal(r.checks.find(row=>row.key==='catalogue').ok,true); assert.equal(r.checks.find(row=>row.key==='replenishment').ok,true);
routes.push({role,pilot_installation:true,warehouse_scenario_ready:r.warehouse_scenario_ready,replenishment_scenario_ready:r.replenishment_scenario_ready,client_configuration_complete:r.client_configuration_complete});
}
}
await fs.writeFile('docs/proofs/history-task-integration-http.json',JSON.stringify({checked_at:new Date().toISOString(),result:'passed',environment:base,routes,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'Existing current scope has no historical analysis and no historical execution grants; pending historical reviews tested only in disposable databases.'},null,2));
console.log('Historico, competencias especificas, tarefas e diagnostico da instalacao conferidos.');
` + '})()')(fs, assert);
@@ -0,0 +1,63 @@
<?php
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\Process\Process;
require dirname(__DIR__).'/vendor/autoload.php';
$root = dirname(__DIR__);
if (!in_array('--apply-authorized', $argv, true)) { throw new LogicException('Explicit authorized installation 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.'); }
$proofPath = $root.'/docs/proofs/receipt-block-installation.json';
if (file_exists($proofPath)) { throw new LogicException('Installation proof exists; inspect before repeating.'); }
$tables = array_column($db->select("select tablename from pg_tables where schemaname='public' order by tablename"),'tablename');
if (in_array('receipt_documents',$tables,true)) { throw new LogicException('Document table already exists.'); }
$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.'); } };
$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-receipt-documents-'.gmdate('Ymd-His').'-'.bin2hex(random_bytes(3));
if(!mkdir($directory,0700,true)) { throw new RuntimeException('Backup directory 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]]);
if($fingerprint(DB::connection('receipt_restore'),$tables)!==$baseline) { throw new LogicException('Restored backup differs.'); }
$disk=Storage::disk('receipt_documents'); $probe='.installation-'.bin2hex(random_bytes(8));
try { $disk->put($probe,'private-storage-probe'); if($disk->get($probe)!=='private-storage-probe') { throw new RuntimeException('Storage unavailable.'); } } finally { $disk->delete($probe); }
$migration='database/migrations/2026_11_03_000002_create_receipt_documents.php';
$db->transaction(function() use($db,$tables,$baseline,$fingerprint,$migration) {
foreach($tables as $table) { $db->statement('LOCK TABLE "'.str_replace('"','""',$table).'" IN SHARE MODE'); }
if($fingerprint($db,$tables)!==$baseline) { throw new LogicException('Pilot changed after backup; abort.'); }
if(Artisan::call('migrate',['--database'=>'receipt_install','--path'=>$migration,'--force'=>true])!==0) { throw new RuntimeException('Target migration failed.'); }
$protected=array_values(array_diff($tables,['migrations']));
if($fingerprint($db,$protected)!==array_intersect_key($baseline,array_flip($protected))) { throw new LogicException('Business data changed.'); }
if($db->table('receipt_documents')->count()!==0 || $db->table('migrations')->where('migration','2026_11_03_000001_add_two_factor_columns_to_users')->exists()) { throw new LogicException('Unexpected installation state.'); }
});
$proof=['checked_at'=>gmdate('c'),'environment'=>'http://127.0.0.1:8005','result'=>'passed','authorization'=>'User explicitly authorized receipt document block installation','migration'=>$migration,'backup'=>$dump,'backup_sha256'=>hash_file('sha256',$dump),'restored_tables_checked'=>count($tables),'protected_tables_checked'=>count($tables)-1,'business_data_unchanged'=>true,'documents_created'=>0,'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)'); }
}
$proof['restore_database_removed']=true;
file_put_contents($proofPath,json_encode($proof,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES).PHP_EOL);
echo 'Receipt documents installed; backup restored and business data preserved.'.PHP_EOL;
+30
View File
@@ -0,0 +1,30 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source=await fs.readFile('scripts/supplier-evaluation-check.mjs','utf8');
const common=source.slice(source.indexOf('const base ='),source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const [role,expected] of [['admin',200],['warehouse',200],['procurement',403],['finance',403]]) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
for(const path of ['/stock/ocorrencias','/stock/estornos']) {
const result=await viewer.request(path);assert.equal(result.status,expected);
if(expected===200) {
assert.equal(result.page.encryptHistory,true);assert.equal(result.page.props.recordPagination.total,0);
assert.deepEqual(result.page.props.records,[]);assert.deepEqual(result.page.props.filters,{search:'',state:''});
assert.equal(result.page.props.capabilities.prepare,false);assert.equal(result.page.props.capabilities.review,false);
assert.equal(result.page.props.capabilities.execute,role==='warehouse');
const filtered=await viewer.request(path+'?search=TEST&state=approved');assert.equal(filtered.status,200);
assert.equal(filtered.page.props.recordPagination.total,0);
assert.equal((await viewer.request(path+'?records_page=0')).status,422);
assert.equal((await viewer.request(path+'?state=unknown')).status,422);
}
records.push({role,path,status:result.status});
}
}
await fs.writeFile('docs/proofs/inventory-pagination-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'Pilot access and empty paginated/filtered lists; pending processes, native corrections and old sources tested in disposable databases.'},null,2));
console.log('Ocorrências e estornos paginados conferidos sem decisões ou movimentos novos no piloto.');
`+'})()')(fs,assert);
@@ -0,0 +1,27 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const records=[];
for(const [role,expected] of [['admin',200],['warehouse',200],['procurement',403],['finance',403]]) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
for(const [path,component] of [['/stock/ocorrencias','stock/dispositions'],['/stock/estornos','stock/reversals']]) {
const response=await viewer.request(path);assert.equal(response.status,expected);
if(expected===200) {
assert.equal(response.page.component,component);assert.equal(response.page.encryptHistory,true);assert.equal(response.page.props.records.length,0);
assert.equal(response.page.props.capabilities.prepare,false);assert.equal(response.page.props.capabilities.review,false);
assert.equal(response.page.props.capabilities.execute,role==='warehouse');
}
records.push({role,path,status:response.status});
}
const tasks=await viewer.request('/requisicoes/tarefas?module=warehouse');assert.equal(tasks.status,200);
assert.equal(tasks.page.props.tasks.data.filter(row=>row.task.startsWith('disposition_')||row.task.startsWith('reversal_')).length,0);
}
await fs.writeFile('docs/proofs/inventory-task-integration-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'No persistent stock dispositions or reversals exist; pending decisions and physical movements tested in disposable databases only.'},null,2));
console.log('Ocorrencias, estornos e tarefas conferidos nos perfis existentes.');
` + '})()')(fs, assert);
+25
View File
@@ -0,0 +1,25 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source=await fs.readFile('scripts/supplier-evaluation-check.mjs','utf8');
const common=source.slice(source.indexOf('const base ='),source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const [role,expected] of [['admin',200],['warehouse',403],['procurement',403],['finance',403]]) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const result=await viewer.request('/stock/lotes');assert.equal(result.status,expected);
if(expected===200) {
assert.equal(result.page.encryptHistory,true);
assert.equal(result.page.props.controlPagination.total,0);assert.equal(result.page.props.historyPagination.total,0);
assert.deepEqual(result.page.props.records,[]);assert.deepEqual(result.page.props.history,[]);
assert.equal((await viewer.request('/stock/lotes?controls_page=0')).status,422);
assert.equal((await viewer.request('/stock/lotes?history_page=-1')).status,422);
}
records.push({role,status:result.status});
}
await fs.writeFile('docs/proofs/lot-pagination-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'Read-only pilot access and empty pagination; 210 controls/history and pending-active pairs tested in disposable databases.'},null,2));
console.log('Paginação de lotes conferida no piloto, sem novos dados ou permissões.');
`+'})()')(fs,assert);
@@ -0,0 +1,26 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source=await fs.readFile('scripts/supplier-evaluation-check.mjs','utf8');
const common=source.slice(source.indexOf('const base ='),source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const [role,expected] of [['admin',200],['warehouse',403],['procurement',403],['finance',403]]) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const page=await viewer.request('/stock/lotes');assert.equal(page.status,expected);
if(expected===200) {
assert.equal(page.page.encryptHistory,true);assert.equal(page.page.props.records.length,0);
for(const action of ['prepare','review','release','hold']) assert.equal(page.page.props.capabilities[action],false);
assert.equal((await viewer.request('/stock/lotes?lot_id=999999&warehouse_id=1')).status,422);
}
const tasks=await viewer.request('/requisicoes/tarefas?module=warehouse');assert.equal(tasks.status,200);
const quality=tasks.page.props.tasks.data.filter(row=>row.task==='lot_identify'||row.task==='lot_release'||row.task==='lot_hold');
assert.equal(quality.length,0);
records.push({role,status:page.status,quality_tasks:quality.length});
}
await fs.writeFile('docs/proofs/lot-quality-task-integration-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'Pilot has no active lot controls or operational quality grants; pending decisions and lot links exercised in disposable databases.'},null,2));
console.log('Consulta e restrições de qualidade conferidas sem alterar lotes, decisões ou direitos no piloto.');
`+'})()')(fs,assert);
+24
View File
@@ -0,0 +1,24 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const records=[];
for(const [role,expected] of [['admin',200],['warehouse',403],['procurement',403],['finance',403]]) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const response=await viewer.request('/stock/lotes');assert.equal(response.status,expected);
if(expected===200) {
assert.equal(response.page.component,'stock/lots');assert.equal(response.page.encryptHistory,true);assert.equal(response.page.props.records.length,0);
assert.equal(response.page.props.capabilities.prepare,false);assert.equal(response.page.props.capabilities.review,false);
}
const tasks=await viewer.request('/requisicoes/tarefas?module=warehouse');assert.equal(tasks.status,200);
assert.equal(tasks.page.props.tasks.data.filter(row=>row.task==='lot_setup_activate').length,0);
records.push({role,status:response.status,lot_setup_tasks:0});
}
await fs.writeFile('docs/proofs/lot-task-integration-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'Live pilot has no lot setups or operational lot grants; pending configuration review tested only in disposable databases.'},null,2));
console.log('Controlo por lotes e tarefas conferidos sem novas concessoes.');
` + '})()')(fs, assert);
+19
View File
@@ -0,0 +1,19 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const role of ['admin','warehouse']) {
const viewer=client(); await viewer.enter(role);
const result=await viewer.request('/user/confirm-password');
assert.equal(result.status,200);
assert.equal(result.page.component,'auth/confirm-password');
assert.equal(result.page.encryptHistory,true);
assert.equal((await viewer.request('/two-factor-challenge')).status,404);
records.push({role,passwordConfirmation:result.status,twoFactorDisabled:true});
}
await fs.writeFile('docs/proofs/mfa-foundation-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
enrollment_commands:false,password_changes:false,accounts_or_grants_changed:false,pilot_migration_applied:false,sgf_changed:false,visual_review:false},null,2));
console.log('Base de confirmação conferida; MFA do piloto continua desactivado.');
`+'})()')(fs,assert);
+24
View File
@@ -0,0 +1,24 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const role of ['admin','warehouse']) {
const viewer=client(); await viewer.enter(role);
const result=await viewer.request('/settings/security');
assert.equal(result.status,200);
assert.equal(result.page.component,'settings/security');
assert.equal(result.page.encryptHistory,true);
assert.equal(result.page.props.canManageTwoFactor,undefined);
assert.equal(result.page.props.secretKey,undefined);
assert.equal(result.page.props.recoveryCodes,undefined);
assert.equal((await viewer.request('/user/two-factor-secret-key')).status,404);
assert.equal((await viewer.request('/user/two-factor-recovery-codes')).status,404);
records.push({role,status:result.status,mfaDisabled:true});
}
await fs.writeFile('docs/proofs/mfa-management-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
enrollment_commands:false,password_changes:false,accounts_or_grants_changed:false,pilot_migration_applied:false,sgf_changed:false,visual_review:false,
coverage:'Pilot remains disabled; enabled native management flows tested in disposable databases.'},null,2));
console.log('Interface de segurança conferida; adesão MFA do piloto permanece desactivada.');
`+'})()')(fs,assert);
+30
View File
@@ -0,0 +1,30 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const [role,expected] of [['admin',200],['procurement',200],['finance',200],['warehouse',403]]) {
const viewer=client(); await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const baseReport=await viewer.request('/relatorios');
assert.equal(baseReport.status,expected);
const report=await viewer.request('/relatorios?attention=unapproved_needed');
assert.equal(report.status,expected);
if(expected===200) {
assert.equal(report.page.encryptHistory,true);
const props=report.page.props;
assert.equal(props.filters.attention,'unapproved_needed');
assert.equal(props.records.total,baseReport.page.props.insights.unapproved_past_needed_date);
assert.ok(props.records.data.every(row=>['draft','pending'].includes(row.status)));
const csv=await viewer.request('/relatorios/compras.csv?attention=unapproved_needed','GET',undefined,false);
assert.equal(csv.status,200);assert.ok(csv.text.includes('Data de necessidade'));
assert.equal((await viewer.request('/relatorios?attention=unknown')).status,422);
}
records.push({role,status:report.status});
}
await fs.writeFile('docs/proofs/needed-attention-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_changed:false,visual_review:false,
coverage:'Existing pilot report scopes; populated dates, pagination and revoked streaming access tested in disposable databases.'},null,2));
console.log('Filtro de necessidades por aprovar conferido em leitura no piloto.');
`+'})()')(fs,assert);
+26
View File
@@ -0,0 +1,26 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const records=[];
for(const role of ['admin','warehouse','procurement','finance']) {
const viewer=client(); await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
for(const view of ['all','unread','tasks','outcomes']) {
const result=await viewer.request('/requisicoes/avisos?view='+view+'&search=TEST&page=999');
assert.equal(result.status,200);
assert.equal(result.page.encryptHistory,true);
assert.equal(result.page.props.search,'TEST');
assert.equal(result.page.props.view,view);
assert.ok(result.page.props.notices.current_page>=1);
records.push({role,view,status:result.status,total:result.page.props.notices.total});
}
assert.equal((await viewer.request('/requisicoes/avisos?search='+'A'.repeat(101))).status,422);
}
await fs.writeFile('docs/proofs/notice-consultation-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
read_commands:false,business_decisions:false,accounts_or_grants_changed:false,sgf_changed:false,visual_review:false,
coverage:'Existing synthetic profiles in read-only consultation; populated notice and mark-read flows tested in disposable databases.'},null,2));
console.log('Consulta dos avisos conferida sem marcar avisos do piloto como lidos.');
` + '})()')(fs, assert);
@@ -0,0 +1,25 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const records=[];
for(const role of ['department_head','procurement','requester']) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const detail=await viewer.request('/requisicoes/2');assert.equal(detail.status,200);
const r=detail.page.props.requisition;assert.equal(r.status,'approved');assert.equal(r.items[0].quantity,'2.000');assert.equal(r.total_minor,2320);
assert.equal(r.steps.length,1);assert.equal(r.steps[0].decision,'approved');assert.equal(r.steps[0].approver_id,10);assert.equal(detail.page.props.canDecide,false);
const tasks=await viewer.request('/requisicoes/tarefas');assert.equal(tasks.status,200);
assert.equal(tasks.page.props.tasks.data.filter(row=>row.id.startsWith('purchase:2:')).length,0);
if(role==='procurement') {
const task=tasks.page.props.tasks.data.find(row=>row.id==='quotation:2:prepare');assert.ok(task);
const market=await viewer.request(task.href);assert.equal(market.status,200);assert.equal(market.page.props.canCreate,true);
records.push({role,status:r.status,market_task:true,eligible_suppliers:market.page.props.suppliers.length});
} else records.push({role,status:r.status,approval_task:false});
}
await fs.writeFile('docs/proofs/pilot-approval-activation-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
quantity:'2.000',total_minor_mzn:2320,approved_by:10,business_commands:false,supplier_selection_or_order:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false},null,2));
console.log('Aprovacao unica e encaminhamento para consulta ao mercado confirmados.');
` + '})()')(fs, assert);
@@ -0,0 +1,29 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const records=[];
for(const role of ['procurement','finance','department_head']) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const page=await viewer.request('/requisicoes/2/cotacoes');assert.equal(page.status,200);
assert.equal(page.page.props.ready,true);
const rows=page.page.props.quotations;assert.equal(rows.length,3);
for(const row of rows) {
assert.equal(row.evaluations.length,2);
assert.deepEqual(row.evaluations.map(e=>e.evaluator_id).sort((a,b)=>a-b),[10,12]);
for(const e of row.evaluations) for(const criterion of ['quality','history','payment']) assert.equal(Number(e[criterion]),5);
}
const tasks=await viewer.request('/requisicoes/tarefas?module=purchases');assert.equal(tasks.status,200);
assert.equal(tasks.page.props.tasks.data.filter(row=>row.task==='quotation_evaluate'&&[2,3,4].some(id=>row.id==='quotation:'+id+':evaluate')).length,0);
const comparison=tasks.page.props.tasks.data.some(row=>row.id==='quotation:2:result');
if(role==='procurement') assert.equal(comparison,true);
const detail=await viewer.request('/requisicoes/2');assert.equal(detail.status,200);
assert.equal(detail.page.props.requisition.status,'approved');assert.equal(detail.page.props.requisition.total_minor,2320);
records.push({role,evaluations:6,comparison_ready:true,comparison_task:comparison});
}
await fs.writeFile('docs/proofs/pilot-evaluation-activation-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,business_commands:false,supplier_selection_or_order:false,visual_review:false},null,2));
console.log('Seis avaliações neutras e encaminhamento para comparação conferidos por HTTP.');
` + '})()')(fs, assert);
+32
View File
@@ -0,0 +1,32 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
const proof = JSON.parse(await fs.readFile('docs/proofs/pilot-market-activation.json', 'utf8'));
await new Function('fs', 'assert', 'proof', 'return (async()=>{' + common + `
const records=[];
for(const role of ['procurement','finance','department_head']) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const page=await viewer.request('/requisicoes/2/cotacoes');assert.equal(page.status,200);
const quotes=page.page.props.quotations;assert.ok(quotes);
const rows=Array.isArray(quotes)?quotes:quotes.data;assert.equal(rows.length,3);
assert.deepEqual(rows.map(row=>row.total_minor).sort((a,b)=>a-b),[2320,2552,2784]);
const tasks=await viewer.request('/requisicoes/tarefas?module=purchases');assert.equal(tasks.status,200);
const evaluate=tasks.page.props.tasks.data.filter(row=>row.task==='quotation_evaluate'&&proof.quotation_ids.some(id=>row.id==='quotation:'+id+':evaluate'));
const supplierTasks=tasks.page.props.tasks.data.filter(row=>row.task==='supplier_review');
assert.equal(supplierTasks.length,0);
const detail=await viewer.request('/requisicoes/2');assert.equal(detail.status,200);assert.equal(detail.page.props.requisition.status,'approved');assert.equal(detail.page.props.requisition.total_minor,2320);
if(role==='finance') {
for(const id of [proof.supplier_ids.alternative_a,proof.supplier_ids.alternative_b]) {
const supplier=await viewer.request('/fornecedores/'+id);assert.equal(supplier.status,200);
assert.equal(supplier.page.props.supplier.status,'active');assert.equal(supplier.page.props.supplier.reviewed_by,12);
}
}
records.push({role,quotations:rows.length,evaluation_tasks:evaluate.length,pending_supplier_tasks:supplierTasks.length});
}
await fs.writeFile('docs/proofs/pilot-market-activation-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
totals_minor_mzn:[2320,2552,2784],business_commands:false,supplier_selection_or_order:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false},null,2));
console.log('Cadastros independentes, tres propostas e encaminhamento conferidos sem avaliar.');
` + '})()')(fs, assert, proof);
@@ -0,0 +1,35 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
const proof = JSON.parse(await fs.readFile('docs/proofs/pilot-replenishment-activation.json', 'utf8'));
await new Function('fs', 'assert', 'proof', 'return (async()=>{' + common + `
assert.equal(proof.result, 'applied');
const requester = client(); await requester.enter('requester');
assert.equal((await requester.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const tasks = await requester.request('/requisicoes/tarefas'); assert.equal(tasks.status,200);
const task = tasks.page.props.tasks.data.find(row => row.id === 'purchase:' + proof.purchase_id + ':draft'); assert.ok(task);
const page = await requester.request(task.href); assert.equal(page.status,200);
const r = page.page.props.requisition;
assert.equal(r.status,'draft'); assert.equal(r.items.length,1); assert.equal(r.items[0].quantity,'2.000'); assert.equal(r.total_minor,2320);
assert.equal(r.steps.length,0); assert.equal(page.page.props.canEdit,true); assert.ok(page.page.props.automaticSource);
const checks = [];
for (const [role, prepare, review] of [['procurement',true,false],['finance',false,true]]) {
const viewer=client(); await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const response=await viewer.request('/stock/reposicao?rule_id='+proof.rule_id);
assert.equal(response.status,200); assert.equal(response.page.props.canPrepare,prepare); assert.equal(response.page.props.canReview,review);
assert.equal(response.page.props.rules.data.length,1); assert.equal(response.page.props.rules.data[0].state,'approved');
checks.push({role,result:'passed'});
}
const stock=client(); await stock.enter('warehouse');
assert.equal((await stock.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const availability=await stock.request('/stock/disponibilidade?warehouse_id=1'); assert.equal(availability.status,200);
const row=availability.page.props.rows.data.find(row=>row.id===1); assert.equal(row.physical_milli,1000); assert.equal(row.free_milli,1000);
await fs.writeFile('docs/proofs/pilot-replenishment-activation-http.json',JSON.stringify({checked_at:new Date().toISOString(),result:'passed',environment:base,
purchase_id:r.id,purchase_number:r.number,status:r.status,quantity:r.items[0].quantity,total_minor_mzn:r.total_minor,requester_task:true,
automatic_source_reused:true,physical_milli:row.physical_milli,free_milli:row.free_milli,checks,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false},null,2));
console.log('Rascunho, origem da reposicao, tarefas, competencias e saldo confirmados por HTTP.');
` + '})()')(fs, assert, proof);
+324
View File
@@ -0,0 +1,324 @@
<?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;
}
@@ -0,0 +1,25 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const records=[];
for(const role of ['requester','department_head']) {
const viewer=client(); await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const tasks=await viewer.request('/requisicoes/tarefas'); assert.equal(tasks.status,200);
const rows=tasks.page.props.tasks.data.filter(row=>row.id.startsWith('purchase:2:'));
if(role==='requester') assert.equal(rows.length,0);
else {assert.equal(rows.length,1);assert.equal(rows[0].task,'approve');}
const detail=await viewer.request('/requisicoes/2');assert.equal(detail.status,200);
const r=detail.page.props.requisition;assert.equal(r.number,'REQ-2026-000002');assert.equal(r.status,'pending');assert.equal(r.total_minor,2320);
assert.equal(r.items[0].quantity,'2.000');assert.ok(r.steps.length>0);assert.ok(r.steps.every(step=>step.decision==='pending'));
assert.equal(detail.page.props.canEdit,false);assert.equal(detail.page.props.canDecide,role==='department_head');
assert.deepEqual(detail.page.props.approvalRouting.people.map(person=>person.id),[10]);
records.push({role,status:r.status,task_count:rows.length,can_decide:detail.page.props.canDecide});
}
await fs.writeFile('docs/proofs/pilot-submission-activation-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
quantity:'2.000',total_minor_mzn:2320,all_approval_steps_pending:true,business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false},null,2));
console.log('Submissao e encaminhamento confirmados, sem aprovar a compra.');
` + '})()')(fs, assert);
+24
View File
@@ -0,0 +1,24 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source=await fs.readFile('scripts/supplier-evaluation-check.mjs','utf8');
const common=source.slice(source.indexOf('const base ='),source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const [role,expected] of [['admin',200],['procurement',403],['finance',403]]) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const result=await viewer.request('/compras/fornecimento?view=suggestions&policy_search=ART&policy_form_key=suggestion:new');
assert.equal(result.status,expected);
if(expected===200) {
assert.equal(result.page.encryptHistory,true);assert.equal(result.page.props.canPrepare,false);
assert.deepEqual(result.page.props.policies,[]);assert.equal(result.page.props.policyFormKey,'suggestion:new');
assert.equal((await viewer.request('/compras/fornecimento?view=suggestions&policy_search='+ 'A'.repeat(101))).status,422);
}
records.push({role,status:result.status});
}
await fs.writeFile('docs/proofs/policy-source-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'Existing pilot access and lookup validation; positive source choices and suspensions tested in disposable databases.'},null,2));
console.log('Pesquisa de políticas conferida sem criar sugestões, compras ou competências no piloto.');
`+'})()')(fs,assert);
@@ -0,0 +1,23 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source=await fs.readFile('scripts/supplier-evaluation-check.mjs','utf8');
const common=source.slice(source.indexOf('const base ='),source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const role of ['admin','warehouse','procurement']) {
const viewer=client(); await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const result=await viewer.request('/recepcoes'); assert.equal(result.status,200);
const rows=result.page.props.receipts.data;
assert.ok(Array.isArray(rows));
for(const row of rows.slice(0,2)) {
const detail=await viewer.request('/recepcoes/'+row.id); assert.equal(detail.status,200);
assert.equal(detail.page.props.documentsReady,true);
assert.ok(Array.isArray(detail.page.props.documents));
assert.ok(detail.page.props.documents.every(d=>d.path===undefined));
}
records.push({role,status:result.status,detailsChecked:Math.min(rows.length,2)});
}
await fs.writeFile('docs/proofs/receipt-block-installation-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,business_commands:false,documents_uploaded:false,visual_review:false},null,2));
console.log('Recepções consultadas no piloto; sem comandos de negócio.');
`+'})()')(fs,assert);
+24
View File
@@ -0,0 +1,24 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const [role,expected] of [['admin',200],['warehouse',403]]) {
const viewer=client(); await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const result=await viewer.request('/stock/piloto');assert.equal(result.status,expected);
if(expected===200) {
const runtime=result.page.props.report.runtime;
assert.equal(typeof runtime.schedulerRecent,'boolean');
assert.equal(runtime.schedulerBound,undefined);
assert.equal(runtime.recoveryReady,true);
assert.ok(Number(runtime.work.total)>=0);
}
records.push({role,status:result.status});
}
await fs.writeFile('docs/proofs/replenishment-health-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
pilot_health_command_executed:false,business_commands:false,accounts_or_grants_changed:false,sgf_changed:false,visual_review:false,
coverage:'Existing tenant-scoped diagnostic UI and rights preserved; CLI health executed only in disposable databases.'},null,2));
console.log('Diagnóstico do piloto conferido em leitura, sem executar reposições.');
`+'})()')(fs,assert);
+25
View File
@@ -0,0 +1,25 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const role of ['admin','procurement','finance','warehouse']) {
const viewer=client(); await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const detail=await viewer.request('/requisicoes/2');
const result=await viewer.request('/requisicoes/2/historico.csv','GET',undefined,false);
assert.equal(result.status,detail.status);
if(role==='admin') assert.equal(result.status,200);
if(result.status===200) {
assert.ok(result.text.includes('Metadados do histórico da requisição'));
assert.ok(result.text.includes('Modo de execução'));
assert.ok(!result.text.includes('account_identifier'));
}
records.push({role,detail:detail.status,export:result.status});
}
await fs.writeFile('docs/proofs/requisition-audit-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_changed:false,visual_review:false,
coverage:'Read-only metadata export of existing synthetic requisition 2, compared with native detail visibility.'},null,2));
console.log('Metadados do histórico conferidos no piloto sem decisões ou alterações de dados.');
`+'})()')(fs,assert);
+23
View File
@@ -0,0 +1,23 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source=await fs.readFile('scripts/supplier-evaluation-check.mjs','utf8');
const common=source.slice(source.indexOf('const base ='),source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const [role,expected] of [['admin',200],['warehouse',403],['procurement',403],['finance',403]]) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const result=await viewer.request('/compras/fornecimento?view=reservations');assert.equal(result.status,expected);
if(expected===200) {
assert.equal(result.page.encryptHistory,true);assert.equal(result.page.props.canPrepare,false);assert.deepEqual(result.page.props.operations,[]);
assert.equal((await viewer.request('/compras/fornecimento?view=reservations&issue_search=TEST')).status,422);
assert.equal((await viewer.request('/compras/fornecimento?view=reservations&issue_record_id=999999&issue_search=TEST')).status,422);
}
records.push({role,status:result.status});
}
await fs.writeFile('docs/proofs/reservation-source-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'Existing pilot access and source binding validation; positive searches and associations exercised in disposable databases.'},null,2));
console.log('Pesquisa de saídas conferida sem conceder competências ou criar movimentos no piloto.');
`+'})()')(fs,assert);
@@ -0,0 +1,25 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const routes=[];
for(const [role,expected] of [['admin',200],['procurement',403],['finance',403]]) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const response=await viewer.request('/compras/fornecimento?view=suggestions');assert.equal(response.status,expected);
if(expected===200) {
assert.equal(response.page.component,'supply/suggestions');assert.equal(response.page.encryptHistory,true);
assert.equal(response.page.props.records.length,0);assert.equal(response.page.props.canPrepare,false);
assert.equal(response.page.props.canReview,false);assert.equal(response.page.props.canConvert,false);
}
const tasks=await viewer.request('/requisicoes/tarefas?module=supply');assert.equal(tasks.status,200);
const pending=tasks.page.props.tasks.data.filter(row=>row.task.startsWith('suggestion_'));assert.equal(pending.length,0);
routes.push({role,suggestions_status:response.status,suggestion_tasks:pending.length});
}
await fs.writeFile('docs/proofs/suggestion-task-integration-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',routes,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'Live synthetic scope has no manual suggestions or specific execution grants; review, conversion and closure tested in disposable databases.'},null,2));
console.log('Sugestoes e tarefas conferidas, preservando competencias especificas.');
` + '})()')(fs, assert);
@@ -0,0 +1,20 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const records=[];
for(const role of ['procurement','finance','department_head']) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const page=await viewer.request('/fornecedores/avaliacoes');assert.equal(page.status,200);
assert.equal(page.page.encryptHistory,true);
const tasks=await viewer.request('/requisicoes/tarefas?module=purchases');assert.equal(tasks.status,200);
records.push({role,evaluation_page:page.status,quality_evaluations:page.page.props.records.total,
improvement_tasks:tasks.page.props.tasks.data.filter(row=>row.task.startsWith('supplier_improvement_')).length});
}
await fs.writeFile('docs/proofs/supplier-improvement-task-integration-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,created_quality_evaluations_or_measures:false,visual_review:false},null,2));
console.log('Consulta de qualidade e tarefas conferidas, sem criar avaliações ou medidas no piloto.');
` + '})()')(fs, assert);
@@ -0,0 +1,35 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const records=[];
for(const role of ['procurement','finance','department_head']) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const result=await viewer.request('/requisicoes/tarefas?module=purchases&search=AVA-');assert.equal(result.status,200);
assert.equal(result.page.encryptHistory,true);
const index=await viewer.request('/fornecedores/avaliacoes');assert.equal(index.status,200);assert.equal(index.page.props.canCreate,true);
const tasks=result.page.props.tasks;
const checked=[];
for(const row of tasks.data) {
assert.equal(row.module,'purchases');assert.equal('payload' in row,false);assert.equal('decision' in row,false);
const detail=await viewer.request(row.href.split('#')[0]);assert.equal(detail.status,200);
assert.equal(detail.page.encryptHistory,true);
if(row.task.startsWith('supplier_evaluation_')) assert.equal(detail.page.props[row.task.endsWith('_review')?'canReview':'canEdit'],true);
if(row.task.startsWith('supplier_response_')) {
const id=Number(row.id.split(':')[1]);
const response=detail.page.props.responses.records.find(r=>r.id===id);assert.ok(response);
assert.equal(response[row.task.endsWith('_review')?'canReview':'canEdit'],true);
}
checked.push({id:row.id,task:row.task,detail:200});
}
const quotes=await viewer.request('/requisicoes/2/cotacoes');assert.equal(quotes.status,200);assert.equal(quotes.page.props.ready,true);
assert.equal(quotes.page.props.quotations.reduce((n,r)=>n+r.evaluations.length,0),6);
records.push({role,total_quality_tasks:tasks.total,checked,proposal_evaluations:6});
}
await fs.writeFile('docs/proofs/supplier-quality-task-integration-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,supplier_selection_or_order:false,visual_review:false},null,2));
console.log('Tarefas de qualidade, páginas autorizadas e seis avaliações anteriores conferidas sem decisões novas.');
` + '})()')(fs, assert);
@@ -0,0 +1,29 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const records=[];
for(const role of ['procurement','finance','requester']) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const tasks=await viewer.request('/requisicoes/tarefas?module=purchases');assert.equal(tasks.status,200);
const pending=tasks.page.props.tasks.data.filter(row=>row.task==='supplier_review');assert.equal(pending.length,0);
for(const id of [2,3]) {
const response=await viewer.request('/fornecedores/'+id);
assert.equal(response.status,role==='requester'?403:200);
if(role!=='requester') {
assert.equal(response.page.encryptHistory,true);
assert.equal(response.page.props.supplier.status,'active');
assert.equal(response.page.props.canReview,role==='finance');
assert.equal(response.page.props.canEdit,role==='procurement');
}
}
records.push({role,pending_supplier_tasks:pending.length,supplier_detail_status:role==='requester'?403:200});
}
await fs.writeFile('docs/proofs/supplier-task-integration-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'Current supplier records are active; pending review, stale forms and revoked authority tested in disposable databases.'},null,2));
console.log('Cadastros, tarefas e competencias de preparacao e conferencia confirmados.');
` + '})()')(fs, assert);
+25
View File
@@ -0,0 +1,25 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source=await fs.readFile('scripts/supplier-evaluation-check.mjs','utf8');
const common=source.slice(source.indexOf('const base ='),source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const [role,expected] of [['admin',200],['warehouse',403],['procurement',403],['finance',403]]) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
for(const view of ['reservations','deliveries']) {
const result=await viewer.request('/compras/fornecimento?view='+view+'&line_search=TEST&line_form_key=prepare:new');assert.equal(result.status,expected);
if(expected===200) {
assert.equal(result.page.encryptHistory,true);assert.equal(result.page.props.canPrepare,false);
assert.deepEqual(result.page.props.orderLines,[]);assert.deepEqual(result.page.props.requestLines,[]);
assert.equal((await viewer.request('/compras/fornecimento?view='+view+'&line_search='+ 'A'.repeat(101))).status,422);
}
records.push({role,view,status:result.status});
}
}
await fs.writeFile('docs/proofs/supply-line-source-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'Existing pilot access; positive canonical line searches and planning commands tested in disposable databases.'},null,2));
console.log('Pesquisa de documentos de fornecimento conferida sem novas decisões ou acessos no piloto.');
`+'})()')(fs,assert);
+37
View File
@@ -0,0 +1,37 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source=await fs.readFile('scripts/supplier-evaluation-check.mjs','utf8');
const common=source.slice(source.indexOf('const base ='),source.indexOf('const author ='));
await new Function('fs','assert','return (async()=>{'+common+`
const records=[];
for(const role of ['admin','procurement','finance']) {
const viewer=client();await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const pages=[];
for(const view of ['agreements','calendar','history','suggestions','imports']) {
const result=await viewer.request('/compras/fornecimento?view='+view);
assert.ok([200,403].includes(result.status));
if(role==='admin'||['agreements','calendar'].includes(view)) assert.equal(result.status,200);
if(result.status===200) {
assert.equal(result.page.encryptHistory,true);assert.ok(result.page.props.recordPagination);
assert.ok(result.page.props.records.length<=25);
if(role==='admin') {
assert.equal(result.page.props.canPrepare,false);
assert.equal((await viewer.request('/compras/fornecimento?view='+view+'&records_page=0')).status,422);
}
}
if(view==='imports') {
const search=await viewer.request('/compras/fornecimento?view=imports&order_search=TEST');
assert.equal(search.status,result.status);
if(search.status===200) { assert.equal(search.page.props.canPrepare,false); assert.deepEqual(search.page.props.orders,[]); }
}
pages.push({view,status:result.status,total:result.page?.props?.recordPagination?.total??null});
}
records.push({role,pages});
}
await fs.writeFile('docs/proofs/supply-pagination-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false,
coverage:'Current pilot pagination and existing role access; old records and selected sources exercised in disposable databases.'},null,2));
console.log('Consultas de fornecimento paginadas conferidas sem novas decisões ou acessos no piloto.');
`+'})()')(fs,assert);
+38
View File
@@ -0,0 +1,38 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const routes = [];
for (const [role, prepare, review] of [['procurement', true, false], ['finance', false, true]]) {
const viewer = client();
await viewer.enter(role);
assert.equal((await viewer.request('/contexto', 'POST', { area: 'procurement', option: 'procurement.internal' })).status, 302);
const tasks = await viewer.request('/requisicoes/tarefas?module=supply');
assert.equal(tasks.status, 200);
for (const task of tasks.page.props.tasks.data) {
const detail = await viewer.request(task.href);
assert.equal(detail.status, 200);
const rows = detail.page.props.records ?? detail.page.props.rules?.data ?? [];
assert.equal(rows.length, 1);
assert.equal(rows[0].canApprove, true);
}
const components = [];
for (const path of ['/compras/fornecimento?view=agreements', '/compras/fornecimento?view=calendar', '/stock/reposicao']) {
const response = await viewer.request(path);
assert.equal(response.status, 200);
assert.equal(response.page.encryptHistory, true);
assert.equal(response.page.props.canPrepare, prepare);
assert.equal(response.page.props.canReview, review);
components.push({ path, component: response.page.component, result: 'passed' });
}
routes.push({ role, supply_tasks_shown: tasks.page.props.tasks.data.length, task_links_followed: tasks.page.props.tasks.data.length, components });
}
await fs.writeFile('docs/proofs/supply-task-integration-http.json', JSON.stringify({
checked_at: new Date().toISOString(), environment: base, result: 'passed', routes,
business_commands: false, accounts_or_grants_changed: false, sgf_installation_changed: false,
visual_review: false, coverage: 'Existing current scope and first-page tasks only; pending decisions tested in disposable databases.'
}, null, 2));
console.log('Fornecimentos, reposicao e tarefas conferidos nos perfis sinteticos existentes.');
` + '})()')(fs, assert);
+60
View File
@@ -0,0 +1,60 @@
<?php
use App\Http\Controllers\QuotationController;
use App\Http\Controllers\SupplierController;
use App\Http\Requests\SaveSupplierRequest;
use App\Models\Quotation;
use App\Models\Requisition;
use App\Models\Supplier;
use App\Services\RequisitionWorkflow;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/** Shared driver of native product commands, only called by guarded local pilot scripts. */
function pilotMarketScenario($app, array $proposal, array $next, Closure $actor, Closure $check): array
{
$preparer = $actor('prepare');
$check($preparer->id === $next['preparer_id'], 'Preparador diferente da proposta.');
$purchase = Requisition::query()->findOrFail($next['purchase_id']);
$check($purchase->status === 'approved' && $purchase->number === $next['purchase_number'] && $purchase->items()->sole()->quantity === '2.000' && $purchase->total_minor === 2320, 'Compra diferente da proposta.');
$check(Quotation::query()->where('requisition_id', $purchase->id)->count() === 0, 'Já existem propostas: não duplicar.');
$existing = Supplier::query()->findOrFail($next['existing_eligible_supplier_id']);
$check($existing->eligible(), 'O fornecedor existente deixou de estar elegível.');
$protected = ['users', 'organization_access', 'organization_profiles', 'organization_memberships', '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();
$suppliersBefore = DB::table('suppliers')->orderBy('id')->get()->toArray();
$supplierIds = ['existing' => $existing->id];
$workflow = app(RequisitionWorkflow::class);
foreach ($next['new_synthetic_suppliers'] as $index => $values) {
$preparer = $actor('prepare');
$form = SaveSupplierRequest::create($proposal['environment'].'/fornecedores', 'POST', [...$next['synthetic_supplier_fields'], ...$values]);
$form->setContainer($app)->setRedirector($app['redirect']);
$form->setUserResolver(fn () => $preparer);
$form->validateResolved();
app(SupplierController::class)->store($form, $workflow);
$supplier = Supplier::query()->where('nuit', $values['nuit'])->sole();
$reviewer = $actor('review');
$check($reviewer->id === $next['supplier_reviewer_id'] && $supplier->created_by !== $reviewer->id, 'Conferência sem independência.');
$review = Request::create($proposal['environment'].'/fornecedores/'.$supplier->id.'/validar', 'POST', ['version' => $supplier->version, 'status' => 'active',
'comment' => 'Activação exclusivamente sintética para o piloto local; não representa validação comercial real.']);
$review->setUserResolver(fn () => $reviewer);
app(SupplierController::class)->review($review, $supplier, $workflow);
$supplierIds[$index === 0 ? 'alternative_a' : 'alternative_b'] = $supplier->id;
}
$preparer = $actor('prepare');
foreach ($next['quotations'] as $quote) {
$input = Request::create($proposal['environment'].'/requisicoes/'.$purchase->id.'/cotacoes', 'POST', ['supplier_id' => $supplierIds[$quote['supplier']],
'reference' => $quote['reference'], 'delivery_days' => $quote['delivery_days'], 'valid_until' => $next['quotation_valid_until'], 'payment_terms' => $next['payment_terms'],
'no_conflict' => true, 'items' => [['requisition_item_id' => $purchase->items()->sole()->id, 'unit_price' => $quote['unit_price']]]]);
$input->setUserResolver(fn () => $preparer);
app(QuotationController::class)->store($input, $purchase, $workflow);
$saved = Quotation::query()->where('requisition_id', $purchase->id)->where('reference', $quote['reference'])->sole();
$check($saved->total_minor === $quote['expected_total_minor'], 'Totais diferentes da proposta sintética.');
}
$check($fingerprint() === $baseline && Quotation::query()->where('requisition_id', $purchase->id)->count() === 3, 'A consulta alterou dados fora do âmbito.');
$check(DB::table('suppliers')->whereNotIn('id', [$supplierIds['alternative_a'], $supplierIds['alternative_b']])->orderBy('id')->get()->toArray() == $suppliersBefore, 'Cadastro anterior alterado.');
$check(Quotation::query()->where('requisition_id', $purchase->id)->withCount('evaluations')->get()->sum('evaluations_count') === 0, 'Não avaliar automaticamente.');
return ['supplier_ids' => $supplierIds, 'quotation_ids' => Quotation::query()->where('requisition_id', $purchase->id)->orderBy('id')->pluck('id')->all(), 'protected_tables' => $protected];
}
+32
View File
@@ -0,0 +1,32 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const records=[];
for(const role of ['admin','warehouse','procurement','finance']) {
const viewer=client(); await viewer.enter(role);
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
for(const due of ['all','overdue','today','upcoming','undated']) {
const result=await viewer.request('/requisicoes/tarefas?due='+due);
assert.equal(result.status,200);
const props=result.page.props;
assert.equal(result.page.encryptHistory,true);
assert.equal(props.filters.due,due);
assert.match(props.asOf,/^\\d{4}-\\d{2}-\\d{2}$/);
if(due!=='all') {
assert.equal(props.tasks.total,props.deadlineCounts[due]);
assert.ok(props.tasks.data.every(task=>task.due_status===due));
} else {
assert.equal(props.tasks.total,Object.values(props.deadlineCounts).reduce((sum,value)=>sum+value,0));
}
records.push({role,due,status:result.status,total:props.tasks.total});
}
assert.equal((await viewer.request('/requisicoes/tarefas?due=invalid')).status,422);
}
await fs.writeFile('docs/proofs/task-deadlines-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',records,
business_commands:false,accounts_or_grants_changed:false,sgf_changed:false,visual_review:false,
coverage:'Existing synthetic profiles and actual task projections; populated date boundary and pagination scenarios tested in disposable databases.'},null,2));
console.log('Prazos da caixa de tarefas conferidos em leitura no piloto.');
` + '})()')(fs, assert);
@@ -0,0 +1,20 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
const source = await fs.readFile('scripts/supplier-evaluation-check.mjs', 'utf8');
const common = source.slice(source.indexOf('const base ='), source.indexOf('const author ='));
await new Function('fs', 'assert', 'return (async()=>{' + common + `
const viewer=client();await viewer.enter('finance');
assert.equal((await viewer.request('/contexto','POST',{area:'procurement',option:'procurement.internal'})).status,302);
const quotes=await viewer.request('/requisicoes/2/cotacoes');assert.equal(quotes.status,200);
assert.equal(quotes.page.props.quotations.length,3);assert.equal(quotes.page.props.ready,false);
assert.ok(quotes.page.props.quotations.every(row=>row.evaluations.length===0));
const request=await viewer.request('/requisicoes/2');assert.equal(request.status,200);
assert.equal(request.page.props.requisition.status,'approved');assert.equal(request.page.props.requisition.total_minor,2320);
const availability=await viewer.request('/stock/disponibilidade?warehouse_id=1');assert.equal(availability.status,200);
assert.equal(availability.page.props.rows.data.find(row=>row.id===1).physical_milli,1000);
await fs.writeFile('docs/proofs/warehouse-task-pilot-status-http.json',JSON.stringify({checked_at:new Date().toISOString(),environment:base,result:'passed',
purchase_id:2,purchase_status:'approved',total_minor_mzn:2320,quotations:3,evaluations:0,comparison_ready:false,physical_milli:1000,
business_commands:false,accounts_or_grants_changed:false,sgf_installation_changed:false,visual_review:false},null,2));
console.log('Estado do piloto preservado: tres propostas sem avaliacoes e saldo de uma caixa.');
` + '})()')(fs, assert);
+195 -1
View File
@@ -31,6 +31,39 @@ $quotationTasks = in_array('--quotation-tasks', $argv ?? [], true);
$organizationPreview = in_array('--organization-preview', $argv ?? [], true);
$pilotAccessProposal = in_array('--pilot-access-proposal', $argv ?? [], true);
$formalTasks = in_array('--formal-tasks', $argv ?? [], true);
$supplyTasks = in_array('--supply-tasks', $argv ?? [], true);
$historyTasks = in_array('--history-tasks', $argv ?? [], true);
$suggestionTasks = in_array('--suggestion-tasks', $argv ?? [], true);
$alternativeTasks = in_array('--alternative-tasks', $argv ?? [], true);
$supplierTasks = in_array('--supplier-tasks', $argv ?? [], true);
$dispositionTasks = in_array('--disposition-tasks', $argv ?? [], true);
$inventoryTasks = in_array('--inventory-tasks', $argv ?? [], true);
$lotTasks = in_array('--lot-tasks', $argv ?? [], true);
$improvementTasks = in_array('--improvement-tasks', $argv ?? [], true);
$qualityTasks = in_array('--quality-tasks', $argv ?? [], true);
$coordinationTasks = in_array('--coordination-tasks', $argv ?? [], true);
$lotQualityTasks = in_array('--lot-quality-tasks', $argv ?? [], true);
$lotPagination = in_array('--lot-pagination', $argv ?? [], true);
$supplyPagination = in_array('--supply-pagination', $argv ?? [], true);
$correctionPagination = in_array('--correction-pagination', $argv ?? [], true);
$reservationSources = in_array('--reservation-sources', $argv ?? [], true);
$historySources = in_array('--history-sources', $argv ?? [], true);
$policySources = in_array('--policy-sources', $argv ?? [], true);
$correctionSources = in_array('--correction-sources', $argv ?? [], true);
$supplyLineSources = in_array('--supply-line-sources', $argv ?? [], true);
$coordinationFilters = in_array('--coordination-filters', $argv ?? [], true);
$coordinationExport = in_array('--coordination-export', $argv ?? [], true);
$taskDeadlines = in_array('--task-deadlines', $argv ?? [], true);
$noticeConsultation = in_array('--notice-consultation', $argv ?? [], true);
$agreementSources = in_array('--agreement-sources', $argv ?? [], true);
$mfaFoundation = in_array('--mfa-foundation', $argv ?? [], true);
$mfaManagement = in_array('--mfa-management', $argv ?? [], true);
$requisitionAudit = in_array('--requisition-audit', $argv ?? [], true);
$auditPagination = in_array('--audit-pagination', $argv ?? [], true);
$neededAttention = in_array('--needed-attention', $argv ?? [], true);
$approvedAttention = in_array('--approved-attention', $argv ?? [], true);
$replenishmentHealth = in_array('--replenishment-health', $argv ?? [], true);
$receiptBlock = in_array('--receipt-block', $argv ?? [], true);
$inbox = in_array('--inbox', $argv ?? [], true) || $transport;
$warehouse = in_array('--warehouse', $argv ?? [], true) || $pilot || $workdesk || $readiness;
try {
@@ -77,7 +110,135 @@ try {
if ($formalTasks) {
$tests = ['tests/Feature/FormalAdmissionTest.php', 'tests/Feature/FormalRecommendationTest.php', 'tests/Feature/DocumentaryQualificationTest.php'];
}
$process = new Process([PHP_BINARY, 'vendor/bin/pest', '--compact', ...$tests, ...($formalTasks ? ['--filter=formal task'] : ($quotationTasks ? ['--filter=quotation tasks'] : ($awardTasks ? ['--filter=task inbox'] : [])))], dirname(__DIR__), $environment, timeout: $financeIntegration || $awardTasks || $quotationTasks || $organizationPreview || $pilotAccessProposal || $formalTasks ? 300 : 120);
if ($supplyTasks) {
$tests = ['tests/Feature/SupplyTaskIntegrationTest.php', 'tests/Feature/AutomaticPurchasesTest.php'];
}
if ($historyTasks) {
$tests = ['tests/Feature/SupplyHistoryTest.php', 'tests/Feature/WarehousePilotReadinessTest.php'];
}
if ($suggestionTasks) {
$tests = ['tests/Feature/SupplySuggestionsTest.php'];
}
if ($alternativeTasks) {
$tests = ['tests/Feature/SupplyCoordinationTest.php'];
}
if ($supplierTasks) {
$tests = ['tests/Feature/SupplierMaintenanceTest.php'];
}
if ($dispositionTasks) {
$tests = ['tests/Feature/InventoryDispositionsTest.php'];
}
if ($inventoryTasks) {
$tests = ['tests/Feature/InventoryDispositionsTest.php', 'tests/Feature/InventoryReversalsTest.php'];
}
if ($lotTasks) {
$tests = ['tests/Feature/InventoryLotsTest.php'];
}
if ($improvementTasks) {
$tests = ['tests/Feature/SupplierImprovementTest.php'];
}
if ($qualityTasks) {
$tests = ['tests/Feature/SupplierEvaluationTest.php', 'tests/Feature/SupplierResponseTest.php'];
}
if ($coordinationTasks) {
$tests = ['tests/Feature/SupplyCoordinationTest.php', 'tests/Feature/InventoryReversalsTest.php'];
}
if ($lotQualityTasks) {
$tests = ['tests/Feature/InventoryLotsTest.php'];
}
if ($lotPagination) {
$tests = ['tests/Feature/InventoryLotsTest.php'];
}
if ($supplyPagination) {
$tests = ['tests/Feature/SupplyPaginationTest.php', 'tests/Feature/SupplyHistoryTest.php'];
}
if ($correctionPagination) {
$tests = ['tests/Feature/InventoryCorrectionPaginationTest.php'];
}
if ($reservationSources) {
$tests = ['tests/Feature/SupplyCoordinationTest.php', 'tests/Feature/InventoryReversalsTest.php'];
}
if ($historySources) {
$tests = ['tests/Feature/SupplyHistoryTest.php'];
}
if ($policySources) {
$tests = ['tests/Feature/SupplySuggestionsTest.php'];
}
if ($correctionSources) {
$tests = ['tests/Feature/InventoryCorrectionPaginationTest.php'];
}
if ($supplyLineSources) {
$tests = ['tests/Feature/SupplyCoordinationTest.php'];
}
if ($coordinationFilters || $coordinationExport) {
$tests = ['tests/Feature/SupplyCoordinationTest.php'];
}
if ($taskDeadlines) {
$tests = ['tests/Feature/WorkflowTaskInboxTest.php', 'tests/Feature/SupplyCoordinationTest.php'];
}
if ($noticeConsultation) {
$tests = ['tests/Feature/InternalRequisitionCandidateTest.php'];
}
if ($agreementSources) {
$tests = ['tests/Feature/SupplyPaginationTest.php'];
}
if ($mfaFoundation) {
$tests = ['tests/Feature/Auth/TwoFactorFoundationTest.php'];
}
if ($mfaManagement) {
$tests = ['tests/Feature/Settings/TwoFactorManagementTest.php'];
}
if ($requisitionAudit || $auditPagination) {
$tests = ['tests/Feature/InternalRequisitionCandidateTest.php'];
}
if ($neededAttention || $approvedAttention) {
$tests = ['tests/Feature/ProcurementReportsTest.php'];
}
if ($replenishmentHealth) {
$tests = ['tests/Feature/ReplenishmentHealthTest.php'];
}
if ($receiptBlock) {
$tests = ['tests/Feature/ReceiptDocumentBlockTest.php'];
}
$filter = match (true) {
$receiptBlock => ['--filter=receipt document block'],
$replenishmentHealth => ['--filter=replenishment health'],
$approvedAttention => ['--filter=approved attention reports'],
$neededAttention => ['--filter=needed attention report'],
$auditPagination => ['--filter=requisition audit export'],
$requisitionAudit => ['--filter=requisition audit export'],
$mfaManagement => ['--filter=mfa management'],
$mfaFoundation => ['--filter=two factor foundation'],
$agreementSources => ['--filter=agreement source search'],
$noticeConsultation => ['--filter=notice consultation'],
$taskDeadlines => ['--filter=task deadline'],
$coordinationExport => ['--filter=coordination export'],
$coordinationFilters => ['--filter=coordination record filters'],
$supplyLineSources => ['--filter=supply line search'],
$correctionSources => ['--filter=older selected|correction source search'],
$policySources => ['--filter=policy source search'],
$historySources => ['--filter=history source search'],
$reservationSources => ['--filter=reservation source search|coordination task returns'],
$supplyPagination => ['--filter=supply pagination'],
$lotPagination => ['--filter=lot pagination'],
$lotQualityTasks => ['--filter=lot quality task'],
$coordinationTasks => ['--filter=coordination task'],
$qualityTasks => ['--filter=quality task|response task'],
$improvementTasks => ['--filter=improvement task'],
$lotTasks => ['--filter=lot setup task'],
$inventoryTasks => ['--filter=disposition task|reversal task'],
$dispositionTasks => ['--filter=disposition task'],
$supplierTasks => ['--filter=supplier task'],
$alternativeTasks => ['--filter=alternative task'],
$suggestionTasks => ['--filter=suggestion task'],
$historyTasks => ['--filter=history task|command readiness'],
$supplyTasks => ['--filter=supply task'],
$formalTasks => ['--filter=formal task'],
$quotationTasks => ['--filter=quotation tasks'],
$awardTasks => ['--filter=task inbox'],
default => [],
};
$process = new Process([PHP_BINARY, 'vendor/bin/pest', '--compact', ...$tests, ...$filter], dirname(__DIR__), $environment, timeout: $financeIntegration || $awardTasks || $quotationTasks || $organizationPreview || $pilotAccessProposal || $formalTasks || $supplyTasks || $historyTasks || $suggestionTasks || $alternativeTasks || $supplierTasks || $dispositionTasks || $inventoryTasks || $lotTasks || $improvementTasks || $qualityTasks || $coordinationTasks || $lotQualityTasks || $lotPagination || $supplyPagination || $correctionPagination || $reservationSources || $historySources || $policySources || $correctionSources || $supplyLineSources || $noticeConsultation ? 300 : 120);
$process->run();
echo $process->getOutput();
if (! $process->isSuccessful()) {
@@ -206,6 +367,39 @@ try {
DB::purge('warehouse_proof_read');
}
$proofName = match (true) {
$receiptBlock => 'receipt-block-pg',
$replenishmentHealth => 'replenishment-health-pg',
$approvedAttention => 'approved-attention-pg',
$neededAttention => 'needed-attention-pg',
$auditPagination => 'audit-pagination-pg',
$requisitionAudit => 'requisition-audit-pg',
$mfaManagement => 'mfa-management-pg',
$mfaFoundation => 'mfa-foundation-pg',
$agreementSources => 'agreement-source-pg',
$noticeConsultation => 'notice-consultation-pg',
$taskDeadlines => 'task-deadlines-pg',
$coordinationExport => 'coordination-export-pg',
$coordinationFilters => 'coordination-filters-pg',
$supplyLineSources => 'supply-line-source-pg',
$correctionSources => 'correction-source-pg',
$policySources => 'policy-source-pg',
$historySources => 'history-source-pg',
$reservationSources => 'reservation-source-pg',
$correctionPagination => 'inventory-pagination-pg',
$supplyPagination => 'supply-pagination-pg',
$lotPagination => 'lot-pagination-pg',
$lotQualityTasks => 'lot-quality-task-integration-pg',
$coordinationTasks => 'coordination-task-integration-pg',
$qualityTasks => 'supplier-quality-task-integration-pg',
$improvementTasks => 'supplier-improvement-task-integration-pg',
$lotTasks => 'lot-task-integration-pg',
$inventoryTasks => 'inventory-task-integration-pg',
$dispositionTasks => 'disposition-task-integration-pg',
$supplierTasks => 'supplier-task-integration-pg',
$alternativeTasks => 'alternative-task-integration-pg',
$suggestionTasks => 'suggestion-task-integration-pg',
$historyTasks => 'history-task-integration-pg',
$supplyTasks => 'supply-task-integration-pg',
$formalTasks => 'formal-task-integration-pg',
$pilotAccessProposal => 'pilot-access-proposal-pg',
$organizationPreview => 'organization-activation-preview-pg',