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

135 lines
14 KiB
PHP

<?php
// Included only after the exact database/role/cluster guards of payable-history-sgf-proof.php.
use App\Models\User;
use App\Modules\Purchasing\{SupplierPayables, SupplierPaymentsController};
use App\Shared\Context\CompanyContext;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tempo\Finance\Payables\{Payables, SettlementSchema, ReversalSchema};
$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]);
$service = app(SupplierPayables::class);
$enter = function (string $role) use ($f, $scope): User {
$u = User::findOrFail($f['actors'][$role]); auth()->setUser($u);
app(CompanyContext::class)->selectFor($u->id, $scope['tenant_id'], $scope['company_id'], $scope['unit_id']);
return $u;
};
$command = fn (string $role, string $action, array $data) => $service->engine($enter($role))->settlements()->command($action, $data, (string) Str::uuid());
$fingerprint = function (): string {
$h = 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', ...SettlementSchema::TABLES, ...ReversalSchema::TABLES] as $table) {
hash_update($h, $table."\n");
foreach (DB::table($table)->orderBy($table === 'ps_outbox' ? 'event_id' : 'id')->cursor() as $row) hash_update($h, Payables::canonical((array) $row)."\n");
}
return hash_final($h);
};
if ($phase === 'settlement-page-http') {
auth()->forgetGuards();
Illuminate\Support\Facades\Route::middleware('web')->get('/qa/settlement-enter', function (Request $r) use ($f) {
$u=User::findOrFail($f['actors']['read']); auth()->login($u); $r->session()->regenerate();
$token=bin2hex(random_bytes(24)); $r->session()->put('auth_version',$u->auth_version);
$r->session()->put('company_context',$f['scope']+['token'=>$token,'module_id'=>'purchasing']);
return response()->json(['csrf'=>csrf_token(),'context'=>$token])->header('Cache-Control','no-store');
});
$app->handleRequest(Request::capture()); exit;
}
if ($phase === 'settlement-page-setup') {
if (DB::getSchemaBuilder()->hasTable('ps_payments')) throw new LogicException('Fresh settlement fixture required');
SettlementSchema::install(); ReversalSchema::install();
foreach (['prepare'=>['payments_view','payment_prepare','allocation_prepare','reversal_prepare'], 'validate'=>['payments_view','payment_confirm','allocation_confirm','reversal_confirm'], 'read'=>['payments_view']] as $role=>$actions) {
foreach($actions as $action) DB::table('pa_access')->insert($scope+['user_id'=>$f['actors'][$role],'action'=>$action,'person'=>'sgf-history-person-'.$f['actors'][$role]]);
}
$base=$service->document($enter('read'),$f['source_id']); unset($base['local_id']); $obligations=[]; $sources=[];
for($n=1;$n<=61;$n++) {
$doc=array_replace($base,['document'=>110000+$n,'number'=>'SETTLE-PAGE-SOURCE-'.$n,'payment_count'=>0]); $payload=Payables::canonical($doc);
$id=DB::table('pa_imported_sources')->insertGetId($scope+['payload'=>$payload,'receipt_hash'=>hash('sha256',$payload)]);
$p=$service->engine($enter('prepare'))->command('prepare',$id,'Explicit synthetic source for settlement pages.',(string)Str::uuid());
$o=$service->engine($enter('validate'))->command('validate',$p['proposal_id'],'Independent synthetic recognition.',(string)Str::uuid());
$obligations[]=$o['obligation_id']; $sources[]=$id;
}
$revision=DB::table('pa_obligations')->where('id',$obligations[0])->value('source_revision');
$p=$command('prepare','payment-prepare',['obligation_id'=>$obligations[0],'source_revision'=>$revision,'amount'=>'1.00','currency'=>'MZN','paid_on'=>'2026-10-29','fact_reference'=>'SIM-PAGE-ORIGINAL','evidence_reference'=>'SIM-PAGE-EVIDENCE','reason'=>'Synthetic payment fact for bounded reading.']);
$payment=$p['payment_id'];
$command('validate','payment-decide',['payment_id'=>$payment,'decision'=>'confirmed','evidence_reference'=>'SIM-PAGE-CONFIRM','reason'=>'Independent synthetic payment evidence.']);
$proposalIds=[];
foreach(array_chunk(array_slice($obligations,0,60),30) as $batch=>$ids) {
$plan=[];foreach($ids as $id)$plan[]=['obligation_id'=>$id,'source_revision'=>DB::table('pa_obligations')->where('id',$id)->value('source_revision'),'amount'=>'0.01'];
$p=$command('prepare','allocation-prepare',['payment_id'=>$payment,'reference'=>'SIM-PAGE-PLAN-'.$batch,'allocations'=>$plan,'reason'=>'Synthetic partial allocations across obligations.']);
$command('validate','allocation-decide',['proposal_id'=>$p['proposal_id'],'decision'=>'confirmed','reason'=>'Independent synthetic allocation review.']); $proposalIds[]=$p['proposal_id'];
}
$oldest=DB::table('ps_allocations')->where('payment_id',$payment)->orderBy('id')->first();
$r=$command('prepare','reversal-prepare',['allocation_id'=>$oldest->id,'source_revision'=>$oldest->source_revision,'reason'=>'Synthetic integral correction with original preserved.']);
$command('validate','reversal-confirm',['proposal_id'=>$r['proposal_id'],'reason'=>'Independent integral reversal review.']);
$last=end($obligations); $pending=$command('prepare','allocation-prepare',['payment_id'=>$payment,'reference'=>'SIM-PAGE-PENDING','allocations'=>[['obligation_id'=>$last,'source_revision'=>DB::table('pa_obligations')->where('id',$last)->value('source_revision'),'amount'=>'0.10']],'reason'=>'Pending proposal to test current revision after reading.']);
file_put_contents($s['root'].'/settlement-fixture.json',json_encode(['source_ids'=>$sources,'obligation_ids'=>$obligations,'payment_id'=>$payment,'confirmed_proposals'=>$proposalIds,'pending_proposal_id'=>$pending['proposal_id'],'original_payment_unallocated_minor'=>41,'synthetic_only'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
echo "Fresh synthetic settlement workflow prepared: 61 obligations, one payment, 60 allocations and one reversal.\n"; exit;
}
$g=json_decode(file_get_contents($s['root'].'/settlement-fixture.json'),true,64,JSON_THROW_ON_ERROR);
$u=$enter('read');
if(str_starts_with($phase,'settlement-read-')){require __DIR__.'/settlement-read-proof.php';exit;}
if ($phase === 'settlement-page-seed') {
if(DB::table('ps_payments')->count()!==1)throw new LogicException('Seed cannot reset or replace retained facts');
DB::transaction(function()use($g,$scope,$f){
$template=(array)DB::table('ps_payments')->where('id',$g['payment_id'])->first();unset($template['id']);
$proposal=(array)DB::table('ps_proposals')->where('id',$g['confirmed_proposals'][0])->first();unset($proposal['id']);
$decision=(array)DB::table('ps_payment_decisions')->where('payment_id',$g['payment_id'])->first();unset($decision['id']);
$allocDecision=(array)DB::table('ps_allocation_decisions')->where('proposal_id',$g['confirmed_proposals'][0])->first();unset($allocDecision['id']);
$allocation=(array)DB::table('ps_allocations')->where('payment_id',$g['payment_id'])->orderBy('id')->first();unset($allocation['id']);
$key=fn($v)=>hash('sha256',Payables::canonical([array_intersect_key($scope,array_flip(['tenant_id','company_id'])),$v]));
for($n=1;$n<=999;$n++) {
$fact='SIM-PAGE-CLONE-'.str_pad((string)$n,4,'0',STR_PAD_LEFT);$evidence='SIM-EVID-CLONE-'.$n;
$snapshot=json_decode($template['snapshot'],true,64,JSON_THROW_ON_ERROR);$snapshot['fact_reference']=$fact;$snapshot['evidence_reference']=$evidence;
$row=array_replace($template,['fact_reference'=>$fact,'evidence_reference'=>$evidence,'fact_key'=>$key($fact),'evidence_key'=>$key($evidence),'snapshot'=>Payables::canonical($snapshot),'payload_hash'=>hash('sha256',Payables::canonical($snapshot))]);
$id=DB::table('ps_payments')->insertGetId($row);$state=$n%3===0?'proposed':($n%3===1?'confirmed':'rejected');
if($state!=='proposed')DB::table('ps_payment_decisions')->insert(array_replace($decision,['payment_id'=>$id,'decision'=>$state]));
if($state==='confirmed') {
$reference='SIM-PLAN-CLONE-'.$n;$plan=[['obligation_id'=>$allocation['obligation_id'],'source_revision'=>$allocation['source_revision'],'amount_minor'=>1]];
$pid=DB::table('ps_proposals')->insertGetId(array_replace($proposal,['payment_id'=>$id,'reference'=>$reference,'reference_key'=>$key($reference),'plan'=>Payables::canonical($plan),'plan_hash'=>hash('sha256',Payables::canonical($plan))]));
$did=DB::table('ps_allocation_decisions')->insertGetId(array_replace($allocDecision,['proposal_id'=>$pid]));
DB::table('ps_allocations')->insert(array_replace($allocation,['payment_id'=>$id,'decision_id'=>$did,'amount_minor'=>1]));
}
DB::table('ps_events')->insert($scope+['payment_id'=>$id,'actor_id'=>$f['actors']['prepare'],'person'=>'sgf-history-person-'.$f['actors']['prepare'],'kind'=>'qa.explicit_benchmark_clone','payload'=>'{}','created_at'=>now()]);
}
});
$hash=$fingerprint();file_put_contents($s['root'].'/settlement-volume.json',json_encode(['payments'=>1000,'actual_command_workflow_payments'=>1,'explicit_benchmark_clones'=>999,'indices_added'=>false,'baseline_fingerprint'=>$hash],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
echo "Synthetic volume ready: 1000 payment facts, original command workflow preserved.\n";exit;
}
if ($phase === 'settlement-page-cases') { require __DIR__.'/settlement-page-cases.php'; exit; }
if ($phase === 'settlement-page-nav') {
$request=Request::create('/supplier-payments?'.http_build_query(['source'=>$g['source_ids'][0],'payment_state'=>'confirmed','payments_page'=>2]),'GET');
$request->setUserResolver(fn()=>$u);$request->setLaravelSession(app('session')->driver());
$html=app(SupplierPaymentsController::class)->page($request)->getContent();
if(!preg_match('/href="([^"]+)">Consultar todas as origens/',$html,$match))throw new LogicException('Source exit absent');
parse_str(parse_url(html_entity_decode($match[1]),PHP_URL_QUERY)??'',$query);
if(isset($query['source'])||($query['payment_state']??'')!=='confirmed'||($query['payments_page']??'')!=='2'||($query['page']??'')!=='1')throw new LogicException('Source exit loses independent payment filters');
file_put_contents($s['root'].'/settlement-nav.json',json_encode(['passed'=>true,'source_exit_preserves_payment_filters'=>true,'document_page_reset'=>true],JSON_PRETTY_PRINT));echo "Source exit preserves independent ledger filters.\n";exit;
}
if ($phase === 'settlement-page-bench') {
$strategy=$argv[3]??'';if(!in_array($strategy,['legacy','page25','page100','focus'],true))throw new LogicException('Unknown measurement');
$book=$service->engine($u)->settlements();$book->listingPage(['payment'=>$g['payment_id']]);$queries=0;$active=false;
DB::listen(function()use(&$queries,&$active){if($active)$queries++;});gc_collect_cycles();memory_reset_peak_usage();$baseline=memory_get_usage(true);$active=true;$start=hrtime(true);
$data=$strategy==='legacy'?$book->listing():$book->listingPage($strategy==='focus'?['payment'=>$g['payment_id']]:['payment_per_page'=>$strategy==='page25'?25:100]);
$ms=(hrtime(true)-$start)/1e6;$active=false;$peak=memory_get_peak_usage(true);$windows=[];
foreach(['payments','proposals','allocations','events'] as $table){
$rows=json_decode(json_encode($data[$table],JSON_THROW_ON_ERROR),true,128,JSON_THROW_ON_ERROR);
foreach($rows as &$row)unset($row['local_source_id']);unset($row);
usort($rows,fn($a,$b)=>$b['id']<=>$a['id']);
foreach($strategy==='legacy'?[25,100]:($strategy==='focus'?[]:[$strategy==='page25'?25:100]) as $size)$windows[$table][$size]=hash('sha256',Payables::canonical(array_slice($rows,0,$size)));
}
$original=array_values(array_filter($data['payments'],fn($p)=>$p['id']===$g['payment_id']));
$report=['strategy'=>$strategy,'volume'=>DB::table('ps_payments')->count(),'time_ms'=>round($ms,3),'queries'=>$queries,'php_heap_baseline_bytes'=>$baseline,'php_heap_peak_bytes'=>$peak,'window_fingerprints'=>$windows,'original_payment_fingerprint'=>hash('sha256',Payables::canonical(json_decode(json_encode($original,JSON_THROW_ON_ERROR),true,128,JSON_THROW_ON_ERROR))),'original_unallocated_minor'=>$original[0]['unallocated_minor']??null,'rows'=>array_map('count',array_intersect_key($data,array_flip(['payments','proposals','allocations','events','reversal_proposals','reversals']))),'indices_added'=>false,'php'=>PHP_VERSION,'not_deployment_capacity'=>true];
file_put_contents($s['root'].'/settlement-bench-'.$strategy.'.json',json_encode($report,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode($report)."\n";exit;
}
if ($phase === 'settlement-page-post') {
DB::beginTransaction();DB::statement('SET TRANSACTION READ ONLY');try{
$baseline=json_decode(file_get_contents($s['root'].'/settlement-volume.json'),true,64,JSON_THROW_ON_ERROR);$hash=$fingerprint();
$bundle=json_decode(file_get_contents($s['root'].'/source.json'),true,64,JSON_THROW_ON_ERROR);$doc=$bundle['source'];unset($doc['local_id']);
$original=json_decode(DB::table('pa_imported_sources')->where('id',$f['source_id'])->value('payload'),true,64,JSON_THROW_ON_ERROR);
if($baseline['baseline_fingerprint']!==$hash||Payables::canonical($original)!==Payables::canonical($doc)||DB::table('ps_payments')->count()!==1000)throw new LogicException('Retained synthetic facts changed');
file_put_contents($s['root'].'/settlement-post.json',json_encode(['passed'=>true,'read_only'=>true,'baseline_fingerprint_equal'=>true,'business_fingerprint'=>$hash,'copied_original_receipt_unchanged'=>true,'payment_count'=>1000,'source_databases_and_demos_not_written'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
}finally{DB::rollBack();}echo "Synthetic financial rows unchanged after tests and measurement.\n";exit;
}
throw new LogicException('Unknown settlement page phase');