$name,'assertions'=>$checks-$n,'passed'=>true];} finally{while(DB::transactionLevel()>0)DB::rollBack();$enter('read');}echo 'Passed settlement read case: '.$name."\n"; }; DB::listen(function($event)use(&$capture,&$queries,&$state){ if(!$capture)return;$queries[]=$event->sql; if(!$state&&str_contains($event->sql,'"ps_payments"')){$pdo=$event->connection->getPdo();$state=['isolation'=>$pdo->query('SHOW transaction_isolation')->fetchColumn(),'read_only'=>$pdo->query('SHOW transaction_read_only')->fetchColumn()];} }); $makeBook=fn()=>$service->engine($enter('read'))->settlements(); $case('standalone-reader-and-old-constructor-contract',function()use($makeBook,$g,$scope,$u,$assert,&$capture,&$queries,&$state):void{ $queries=[];$state=[];$capture=true;$book=$makeBook();$data=$book->listingPage(['payment'=>$g['payment_id']]);$capture=false; $assert($data['payments'][0]['unallocated_minor']===41,'Full-history position preserved'); $assert($state===['isolation'=>'repeatable read','read_only'=>'on'],'Own reader transaction is coherent and read only'); $assert(!array_filter($queries,fn($sql)=>str_contains(strtolower($sql),'for update')),'Read callback never takes writer locks'); $assert(DB::transactionLevel()===0&&DB::selectOne('SHOW default_transaction_read_only')->default_transaction_read_only==='off','Read mode does not stick to later transactions'); $writer=(new ReflectionProperty($book,'authorize'))->getValue($book); $legacy=new Tempo\Finance\Payables\SupplierSettlements(DB::connection(),$u->id,'sgf-history-person-'.$u->id,$scope,$writer,fn()=>throw new LogicException('No source callback required by a read')); $queries=[];$state=[];$capture=true;$old=$legacy->listingPage(['payment'=>$g['payment_id']]);$capture=false; $assert(json_encode($old['payments'],JSON_THROW_ON_ERROR)===json_encode($data['payments'],JSON_THROW_ON_ERROR),'Old constructor retains identical position'); $assert(count(array_filter($queries,fn($sql)=>str_contains(strtolower($sql),'for update')))===2&&$state['read_only']==='off','Without new callback existing writer authorization is retained'); $reads=0;$writes=0; $separate=new Tempo\Finance\Payables\SupplierSettlements(DB::connection(),$u->id,'test-only',$scope, function()use(&$writes){$writes++;abort(403);},fn()=>throw new LogicException('No obligation requested'),function()use(&$reads){$reads++;}); $separate->listingPage(['payment'=>$g['payment_id']]);$refused=false; try{$separate->command('allocation-decide',['proposal_id'=>$g['pending_proposal_id'],'decision'=>'confirmed','reason'=>'Read callback must not authorize a command.'],(string)Illuminate\Support\Str::uuid());} catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$refused=$e->getStatusCode()===403;} $assert($reads===1&&$writes===1&&$refused,'Read authorization cannot replace command authority'); }); $case('rights-identity-and-authenticated-user-revalidated',function()use($makeBook,$g,$scope,$f,$enter,$assert):void{ foreach(['right','person','actor'] as $change){ DB::beginTransaction();try{ $book=$makeBook(); if($change==='right')DB::table('pa_access')->where($scope)->where(['user_id'=>$f['actors']['read'],'action'=>'payments_view'])->delete(); if($change==='person')DB::table('pa_access')->where($scope)->where(['user_id'=>$f['actors']['read'],'action'=>'view'])->update(['person'=>'QA_CHANGED_AFTER_ENGINE_CREATION']); if($change==='actor')$enter('validate'); $status=null;try{$book->listingPage(['payment'=>$g['payment_id']]);}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$status=$e->getStatusCode();} $assert($status===($change==='person'?409:403),'Fresh authority refused change '.$change); }finally{DB::rollBack();$enter('read');} } }); $case('existing-page-and-financial-proofs-remain-equivalent',function()use($makeBook,$s,$g,$assert):void{ foreach([25,100] as $size){ $before=json_decode(file_get_contents($s['root'].'/settlement-bench-page'.$size.'.json'),true,128,JSON_THROW_ON_ERROR); $now=$makeBook()->listingPage(['payment_per_page'=>$size]); foreach(['payments','proposals','allocations','events'] as $key){ $rows=json_decode(json_encode($now[$key],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']); $assert(hash('sha256',Tempo\Finance\Payables\Payables::canonical($rows))===$before['window_fingerprints'][$key][$size],'Earlier facts equal after read authorization change: '.$key.'/'.$size); } } $before=json_decode(file_get_contents($s['root'].'/settlement-bench-focus.json'),true,128,JSON_THROW_ON_ERROR); $payments=$makeBook()->listingPage(['payment'=>$g['payment_id']])['payments']; $assert(hash('sha256',Tempo\Finance\Payables\Payables::canonical(json_decode(json_encode($payments,JSON_THROW_ON_ERROR),true,128,JSON_THROW_ON_ERROR)))===$before['original_payment_fingerprint'],'Full original position identical without repeating old benchmark'); }); $case('read-only-database-guard-and-recovery',function()use($makeBook,$g,$scope,$u,$assert):void{ $armed=true;DB::listen(function($event)use(&$armed,$g,$scope,$u){ if($armed&&str_contains($event->sql,'"ps_payments"')){ $armed=false; $event->connection->table('ps_events')->insert($scope+['payment_id'=>$g['payment_id'],'actor_id'=>$u->id,'person'=>'QA','kind'=>'qa.write_must_fail','payload'=>'{}','created_at'=>now()]); } }); $blocked=false;try{$makeBook()->listingPage(['payment'=>$g['payment_id']]);}catch(Illuminate\Database\QueryException $e){$blocked=($e->errorInfo[0]??null)==='25006';} $assert($blocked&&DB::transactionLevel()===0,'Database refuses accidental write and reader rolls back'); $assert(!DB::table('ps_events')->where('kind','qa.write_must_fail')->exists(),'No accidental event recorded'); $assert($makeBook()->listingPage(['payment'=>$g['payment_id']])['payments'][0]['unallocated_minor']===41,'Next read recovers with no sticky aborted transaction'); }); $baseline=json_decode(file_get_contents($s['root'].'/settlement-volume.json'),true,64,JSON_THROW_ON_ERROR); $assert($fingerprint()===$baseline['baseline_fingerprint'],'New read tests changed financial rows'); file_put_contents($s['root'].'/settlement-read-cases.json',json_encode(['passed'=>true,'cases'=>$cases,'assertions'=>$checks,'financial_fingerprint_equal'=>true,'previous_benchmarks_reused'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)); echo json_encode(['cases'=>count($cases),'assertions'=>$checks,'passed'=>true])."\n";