Files
compras/scripts/sgf-sourcing-demo.php
T
2026-11-02 20:27:11 +02:00

66 lines
11 KiB
PHP

<?php
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Process\Process;
use App\Modules\Identity\Services\Permissions;
use App\Modules\Identity\Services\Audit;
use Tempo\Purchasing\Delivery\CodeDeployment;
use Tempo\Purchasing\SgfSourcing\Schema;
$root=dirname(__DIR__);$host='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/development-app';$recovery=dirname($host).'/private/sourcing-recovery';
require $host.'/vendor/autoload.php';$app=require $host.'/bootstrap/app.php';$app->make(Kernel::class)->bootstrap();
try{
$db=DB::connection();$identity=$db->selectOne("select current_database() as db,current_user as role,inet_server_port() as port,current_setting('cluster_name') as cluster");
if(str_replace('\\','/',$app->basePath())!==$host||$identity->db!=='sgf_modules_dev_2576427d1939'||$identity->role!=='sgf_modules_qa_2576427d1939'||(int)$identity->port!==55439||$identity->cluster!=='sgf_modular_regression_2576427d1939'||config('app.url')!=='http://127.0.0.1:8039')throw new RuntimeException('Wrong persistent synthetic demonstration');
app(Permissions::class)->require(1,1,1,'access.manage');
$candidate=json_decode(file_get_contents($root.'/docs/proofs/sgf-sourcing-candidate.json'),true,32,JSON_THROW_ON_ERROR);
if(!class_exists(Schema::class))require_once $candidate['directory'].'/integrations/sgf/sourcing/Schema.php';require_once $root.'/integrations/sgf/operational/CodeDeployment.php';
$hash=function(string $table)use($db){return hash('sha256',json_encode($db->table($table)->orderBy('id')->get(),JSON_THROW_ON_ERROR));};
$action=$argv[1]??'';
if($action==='prepare'){
if(is_dir($recovery))throw new RuntimeException('Recovery exists: do not overwrite');
$proof=json_decode(file_get_contents($root.'/docs/proofs/sgf-sourcing-isolated.json'),true);if(!$proof['passed']||!$proof['persistent_demo_unchanged']||!($proof['recovery']['data_schema_restore_verified']??false)||$proof['setup']['deployment']['deployment_id']!==substr($candidate['archive']['manifest_sha256'],0,24))throw new RuntimeException('Current candidate needs accepted isolation/recovery proof');
if($db->table('users')->count()!==3)throw new RuntimeException('Unexpected accounts: reconcile before proceeding');
mkdir($recovery,0700,true);$cfg=$db->getConfig();(new Process(['C:/Program Files/PostgreSQL/17/bin/pg_dump.exe','--format=custom','--file='.$recovery.'/before.dump','--dbname='.$identity->db],null,['PGHOST'=>'127.0.0.1','PGPORT'=>'55439','PGUSER'=>$identity->role,'PGPASSWORD'=>$cfg['password']],null,90))->mustRun();
(new Process(['C:/Program Files/PostgreSQL/17/bin/pg_restore.exe','--list',$recovery.'/before.dump']))->mustRun();
$baseline=['candidate'=>$candidate,'identity'=>$identity,'backup_sha256'=>hash_file('sha256',$recovery.'/before.dump'),'users'=>$hash('users'),'requests'=>$hash('ir_requests'),'budget'=>$hash('cb_budget_entries'),'consumption'=>$hash('cb_budget_consumptions'),'grants'=>$db->table('permission_grants')->orderBy('id')->get()->all(),'pointer_sha256'=>hash_file('sha256',$host.'/packages/purchasing/current.json')];
file_put_contents($recovery.'/baseline.json',json_encode($baseline,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode(['prepared'=>true,'identity'=>$identity,'backup_sha256'=>$baseline['backup_sha256']]);
}elseif($action==='apply'){
$baseline=json_decode(file_get_contents($recovery.'/baseline.json'),true,64,JSON_THROW_ON_ERROR);
foreach(['users'=>'users','requests'=>'ir_requests','budget'=>'cb_budget_entries','consumption'=>'cb_budget_consumptions'] as $key=>$table)if($baseline[$key]!==$hash($table))throw new RuntimeException('Baseline changed: '.$table);
if($baseline['candidate']['archive']['manifest_sha256']!==$candidate['archive']['manifest_sha256']||!hash_equals($baseline['backup_sha256'],hash_file('sha256',$recovery.'/before.dump')))throw new RuntimeException('Candidate/backup changed');
if(is_file($host.'/storage/framework/down'))throw new RuntimeException('Unexpected prior maintenance');file_put_contents($host.'/storage/framework/down','{}');
$old=json_decode(file_get_contents($host.'/packages/internal-requisitions/manifest.json'),true);$current=json_decode(file_get_contents($host.'/storage/app/private/module-deployments/current.json'),true);$next=json_decode(file_get_contents($candidate['directory'].'/manifest.json'),true);
$migration=Schema::migrate($db,$baseline['backup_sha256'],'Owner-confirmed synthetic sourcing 8039');
$receipt=['host'=>$host,'from_manifest_sha256'=>$current['manifest_sha256'],'to_manifest_sha256'=>$candidate['archive']['manifest_sha256'],'schema_verified'=>Schema::compatible($db),'changes'=>[]];
foreach($next['support_files'] as $file=>$sha)if(($old['support_files'][$file]??null)!==$sha)$receipt['changes'][$file]=['before'=>$old['support_files'][$file]??null,'after'=>$sha];
file_put_contents($host.'/storage/app/private/module-deployments/schema-transition-'.$candidate['archive']['manifest_sha256'].'.json',json_encode($receipt));
$deploy=new CodeDeployment($host);$plan=$deploy->plan($candidate['directory'],$candidate['archive']['manifest_sha256']);$applied=$deploy->apply($candidate['directory'],$candidate['archive']['manifest_sha256'],$plan['target_sha256'],'Owner-confirmed synthetic sourcing 8039',true);file_put_contents($recovery.'/applied.json',json_encode($applied,JSON_PRETTY_PRINT));
$db->transaction(function()use($db){
foreach([1=>['purchasing_sourcing.prepare'],2=>['purchasing_sourcing.evaluate','purchasing_sourcing.propose']] as $id=>$rights)foreach($rights as $right){
$scope=['user_id'=>$id,'tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'permission'=>$right];$inserted=$db->table('permission_grants')->insertOrIgnore($scope+['created_at'=>now(),'updated_at'=>now()]);
if($inserted)Audit::record('access.granted',1,1,1,1,$id,details:['permission'=>$right,'reason'=>'Owner explicitly authorised exact sourcing map; no fourth persistent account.']);
}
foreach(['A','B','C'] as $letter)$db->table('ss_suppliers')->insertOrIgnore(['tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'reference'=>'SOURCING-DEMO-'.$letter,'payload'=>json_encode(['name'=>'Fornecedor sintético '.$letter,'active'=>true,'blocked'=>false,'review_until'=>'2027-12-31','version'=>1,'synthetic_only'=>true]),'created_at'=>now()]);
});
file_put_contents($host.'/config/purchasing-sourcing.php','<?php return ["enabled"=>true];');unlink($host.'/storage/framework/down');echo json_encode(['applied'=>true,'deployment'=>$applied,'schema'=>$migration,'persistent_accounts_created'=>false,'grants_added'=>3,'confirmation_blocked_without_independent_identity'=>true]);
}elseif($action==='apply-code'){
if(!Schema::compatible($db))throw new RuntimeException('Code-only update requires the existing compatible sourcing schema');
$proof=json_decode(file_get_contents($root.'/docs/proofs/sgf-sourcing-isolated.json'),true);if(!$proof['passed']||$proof['setup']['deployment']['deployment_id']!==substr($candidate['archive']['manifest_sha256'],0,24))throw new RuntimeException('Candidate proof mismatch');
$before=[];foreach([...Schema::TABLES,'ir_requests','cb_budget_entries','cb_budget_consumptions','permission_grants','users'] as $table)$before[$table]=$hash($table);
if(is_file($host.'/storage/framework/down'))throw new RuntimeException('Unexpected prior maintenance');
$deploy=new CodeDeployment($host);$plan=$deploy->plan($candidate['directory'],$candidate['archive']['manifest_sha256']);
file_put_contents($recovery.'/before-final-code.json',json_encode(['candidate'=>$candidate,'data_hashes'=>$before,'plan'=>$plan],JSON_PRETTY_PRINT));file_put_contents($host.'/storage/framework/down','{}');
$applied=$deploy->apply($candidate['directory'],$candidate['archive']['manifest_sha256'],$plan['target_sha256'],'Authorised sourcing final code refinement',true);
foreach($before as $table=>$expected)if($hash($table)!==$expected)throw new RuntimeException('Code update changed data: '.$table);
file_put_contents($recovery.'/final-code.json',json_encode($applied,JSON_PRETTY_PRINT));unlink($host.'/storage/framework/down');echo json_encode(['applied'=>true,'code_only'=>true,'deployment'=>$applied,'data_preserved'=>true,'schema_changed'=>false,'rights_changed'=>false]);
}elseif($action==='verify'){
if(hash_file('sha256',$host.'/packages/internal-requisitions/manifest.json')!==$candidate['archive']['manifest_sha256'])throw new RuntimeException('Installed candidate does not match the delivery report');
$baseline=json_decode(file_get_contents($recovery.'/baseline.json'),true,64,JSON_THROW_ON_ERROR);foreach(['users'=>'users','requests'=>'ir_requests','budget'=>'cb_budget_entries','consumption'=>'cb_budget_consumptions'] as $key=>$table)if($baseline[$key]!==$hash($table))throw new RuntimeException('Protected baseline changed: '.$table);
$current=$db->table('permission_grants')->orderBy('id')->get()->keyBy('id');foreach($baseline['grants'] as $old)if((array)$current[$old['id']]!=$old)throw new RuntimeException('Previous grant changed');
$new=$current->except(array_column($baseline['grants'],'id'))->values()->all();if(count($new)!==3)throw new RuntimeException('Expected exactly three authorised new rights');foreach($new as $g)if([$g->tenant_id,$g->company_id,$g->unit_id]!==[1,1,1]||!in_array([$g->user_id,$g->permission],[[1,'purchasing_sourcing.prepare'],[2,'purchasing_sourcing.evaluate'],[2,'purchasing_sourcing.propose']],true))throw new RuntimeException('Unapproved grant');
if(hash_file('sha256',$host.'/packages/purchasing/current.json')!==$baseline['pointer_sha256'])throw new RuntimeException('Pointer changed');
$process=$db->table('ss_processes')->where(['tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'request_id'=>1])->first();$state=json_decode($process->payload,true);if($state['confirmed']!==null||$state['proposals']!==[])throw new RuntimeException('Incomplete comparison must not be awarded');
$report=['passed'=>true,'identity'=>$identity,'scope'=>'1:1:1','candidate'=>$candidate,'protected_users_requisition_reservation_consumption_unchanged'=>true,'previous_grants_preserved'=>true,'new_grants'=>$new,'persistent_accounts_created'=>false,'proposals'=>0,'confirmed'=>false,'suppliers_selected'=>count($state['suppliers']),'quotations'=>count($state['quotes']),'pending'=>'Second eligible evaluator and independent confirmer absent; no account created by owner decision.','backup_sha256'=>$baseline['backup_sha256'],'recovery_directory'=>$recovery,'checked_at'=>gmdate('c')];file_put_contents($root.'/docs/proofs/sgf-sourcing-demo.json',json_encode($report,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode($report,JSON_PRETTY_PRINT);
}else throw new RuntimeException('Use prepare/apply/verify');
}catch(Throwable $e){fwrite(STDERR,$e->getMessage()."\n");exit(1);}