Files
SGF/scripts/payable-page-final.php
T
2026-11-04 09:37:20 +02:00

40 lines
8.2 KiB
PHP

<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use App\Shared\Context\CompanyContext;
use App\Modules\Purchasing\SupplierPayables;
use Tempo\Finance\Payables\Payables;
$f=json_decode(file_get_contents($s['root'].'/fixture.json'),true,64,JSON_THROW_ON_ERROR);$scope=$f['scope'];config(['supplier_payable_history.synthetic_enabled'=>true]);$u=App\Models\User::findOrFail($f['actors']['read']);auth()->setUser($u);app(CompanyContext::class)->selectFor($u->id,$scope['tenant_id'],$scope['company_id'],$scope['unit_id']);$service=app(SupplierPayables::class);
if($phase==='page-filter-positive'){
DB::beginTransaction();try{
$base=$service->document($u,$f['source_id']);unset($base['local_id']);$base['document']=999999;$base['number']='PENDING-RECOGNITION-FIXTURE';$payload=Payables::canonical($base);$id=DB::table('pa_imported_sources')->insertGetId($scope+['payload'=>$payload,'receipt_hash'=>hash('sha256',$payload)]);$visible=$service->listing($u,['q'=>'PENDING-RECOGNITION-FIXTURE','recognition'=>'unrecognised']);if($visible['pagination']['documents']['total']!==1||$visible['documents'][0]['local_id']!==$id||$visible['obligations']||!$visible['events']->isEmpty()||!$visible['proposals']->isEmpty())throw new RuntimeException('Unrecognised filter is not scoped before pagination');$absent=$service->listing($u,['q'=>'PENDING-RECOGNITION-FIXTURE','recognition'=>'recognised']);if($absent['pagination']['documents']['total']!==0)throw new RuntimeException('Recognised predicate mismatch');
$base['number']='INVISIBLE-SAME-COMPANY';$payload=Payables::canonical($base);$foreign=DB::table('pa_imported_sources')->insertGetId([...$scope,'unit_id'=>$scope['unit_id']+100,'payload'=>$payload,'receipt_hash'=>hash('sha256',$payload)]);if($service->listing($u,['q'=>'INVISIBLE-SAME-COMPANY'])['pagination']['documents']['total']!==0)throw new RuntimeException('Unit leak');try{$service->document($u,$foreign);throw new RuntimeException('Other-unit document accepted');}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){if($e->getStatusCode()!==403)throw $e;}
file_put_contents($s['root'].'/page-filter-positive.json',json_encode(['passed'=>true,'positive_unrecognised_document_visible'=>true,'recognised_filter_excludes_it'=>true,'other_unit_same_company_excluded'=>true,'no_other_evidence_returned'=>true,'test_rows_rolled_back'=>true],JSON_PRETTY_PRINT));
}finally{while(DB::transactionLevel()>0)DB::rollBack();}echo "Positive recognition filters and same-company other-unit isolation verified.\n";exit;
}
if($phase==='page-concurrent-read'){
$id=$service->listing($u)['documents'][0]['local_id'];if($id===$f['source_id'])throw new LogicException('Use a clone');$input=$s['root'].'/page-read-worker.json';file_put_contents($input,'{}');$process=null;DB::beginTransaction();
try{
DB::table('companies')->where('id',$scope['company_id'])->where('tenant_id',$scope['tenant_id'])->lockForUpdate()->firstOrFail();$row=DB::table('pa_source_histories')->where($scope)->where('local_source_id',$id)->first();$payload=json_decode($row->payload,true);$payload['receipt_version']=2;DB::table('pa_source_histories')->insert($scope+['local_source_id'=>$id,'version'=>2,'payload'=>Payables::canonical($payload),'receipt_hash'=>str_repeat('0',64),'created_at'=>now()]);
$pipes=[];$process=proc_open([PHP_BINARY,__DIR__.'/payable-history-sgf-proof.php',$file,'page-read-worker',$input],[0=>['pipe','r'],1=>['file',$s['root'].'/page-read-worker-stdout.txt','w'],2=>['file',$s['root'].'/page-read-worker-stderr.txt','w']],$pipes,dirname(__DIR__));if(!is_resource($process))throw new RuntimeException('Worker failed');fclose($pipes[0]);$deadline=microtime(true)+8;do{$status=proc_get_status($process);if(!$status['running'])break;usleep(25000);}while(microtime(true)<$deadline);if($status['running'])throw new RuntimeException('Read waited for writer company lock');$exit=proc_close($process);$process=null;if($exit===-1)$exit=$status['exitcode'];if($exit!==0)throw new RuntimeException('Read worker failed');$result=json_decode(file_get_contents($input.'.result'),true);if($result['documents']!==25||count(array_unique($result['balances']))!==1||$result['balances'][0]!==3000||DB::transactionLevel()!==1)throw new RuntimeException('Read did not preserve committed snapshot');
file_put_contents($s['root'].'/page-concurrent-read.json',json_encode(['passed'=>true,'reader_completed_before_writer_release'=>true,'uncommitted_receipt_not_observed'=>true,'documents'=>25,'balance_minor'=>3000,'reader_time_ms'=>$result['time_ms'],'not_throughput_capacity'=>true],JSON_PRETTY_PRINT));
}finally{if(is_resource($process)){proc_terminate($process);proc_close($process);}while(DB::transactionLevel()>0)DB::rollBack();}echo "Page completed while writer held company lock; only committed receipt observed.\n";exit;
}
if($phase==='page-fresh-command'){
DB::beginTransaction();try{
$data=$service->listing($u);$id=$data['documents'][0]['local_id'];if($id===$f['source_id'])throw new LogicException('Use a benchmark clone, never the original workflow source');$row=DB::table('pa_imported_sources')->where($scope)->where('id',$id)->first();$doc=json_decode($row->payload,true,64,JSON_THROW_ON_ERROR);$doc['number']='CHANGED-AFTER-PAGE';DB::table('pa_imported_sources')->where('id',$id)->update(['payload'=>Payables::canonical($doc),'receipt_hash'=>hash('sha256',Payables::canonical($doc))]);DB::table('pa_access')->insert($scope+['user_id'=>$u->id,'action'=>'prepare','person'=>'sgf-history-person-'.$u->id]);
try{$service->engine($u)->command('prepare_history',$id,'Synthetic conference after a read snapshot.',(string)Str::uuid());throw new RuntimeException('A read snapshot leaked into a command');}catch(DomainException $e){if(!str_contains($e->getMessage(),'Recibo histórico antigo'))throw $e;}
file_put_contents($s['root'].'/page-fresh-command.json',json_encode(['passed'=>true,'read_cache_not_used_by_command'=>true,'current_document_and_history_anchor_revalidated'=>true,'test_changes_rolled_back'=>true],JSON_PRETTY_PRINT));
}finally{while(DB::transactionLevel()>0)DB::rollBack();}echo "Current source revalidated by command after a read page; no snapshot reuse.\n";exit;
}
DB::beginTransaction();DB::statement('SET TRANSACTION READ ONLY');
try{
$native=json_decode(file_get_contents($s['root'].'/source.json'),true,64,JSON_THROW_ON_ERROR)['source'];unset($native['local_id']);$original=DB::table('pa_imported_sources')->where($scope)->where('id',$f['source_id'])->first();if(Payables::canonical($native)!==Payables::canonical(json_decode($original->payload,true)))throw new LogicException('Original native receipt changed');
$counts=[];foreach(['pa_imported_sources','pa_sources','pa_proposals','pa_obligations','pa_history_imports'] as $table)$counts[$table]=DB::table($table)->where($scope)->count();$n=$counts['pa_imported_sources'];if(!in_array($n,[1000,5000,10000],true)||count(array_unique($counts))!==1||DB::table('pa_imported_payments')->where($scope)->count()!==3*$n||DB::getSchemaBuilder()->hasTable('pa_payment_decisions')||DB::getSchemaBuilder()->hasTable('ps_allocations'))throw new LogicException('Unexpected final synthetic ledger state');
if(DB::table('pa_access')->where($scope)->count()!==5)throw new LogicException('Temporary QA action was not restored');
$hash=hash_init('sha256');foreach(['pa_imported_sources','pa_source_histories','pa_sources','pa_proposals','pa_obligations','pa_history_imports','pa_imported_payments','pa_events','pa_commands','pa_access'] as $table){hash_update($hash,$table."\n");foreach(DB::table($table)->where($scope)->orderBy('id')->cursor() as $row)hash_update($hash,Payables::canonical((array)$row)."\n");}
$report=['read_only'=>true,'original_workflow_receipt_unchanged'=>true,'counts'=>$counts,'imported_payments'=>3*$n,'temporary_decision_and_allocation_tables_absent'=>true,'scoped_business_fingerprint'=>hash_final($hash),'benchmark_clones'=>$n-1,'native_workflow_sources'=>1,'no_operational_installation'=>true];
$output=$s['root'].'/page-post.json';if(is_file($output))copy($output,$output.'.'.hash_file('sha256',$output).'.archive');file_put_contents($output,json_encode($report,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode($report)."\n";
}finally{DB::rollBack();}