Files
SGF/scripts/ledger-cost-cases.php
T
2026-11-04 09:37:20 +02:00

25 lines
8.1 KiB
PHP

<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
$output=$s['root'].'/cost-cases.json';if(file_exists($output)||DB::table('gl_proposals')->exists())throw new LogicException('Fresh cost regression book required');$before=$sourceFingerprint();$checks=0;$cases=[];
$ok=function($v,$m)use(&$checks){$checks++;if(!$v)throw new LogicException($m);};
$refuse=function($fn)use($ok){$caught=false;try{$fn();}catch(DomainException|Symfony\Component\HttpKernel\Exception\HttpException|Illuminate\Auth\Access\AuthorizationException){$caught=true;}$ok($caught,'Expected cost source/permission/version refusal');};
$case=function($name,$fn)use(&$cases,&$checks,$enter){$at=$checks;DB::beginTransaction();try{$fn();$cases[]=['name'=>$name,'checks'=>$checks-$at,'passed'=>true];}finally{while(DB::transactionLevel())DB::rollBack();$enter('read');}};
$capture=fn()=>$command('prepare','effect-prepare',$body);$approve=fn($p)=>$command('validate','validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Independent exact cost conference.']);$postBody=fn($p)=>['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Individual actual selected cost posting.'];
$newCost=function($materialAmount=null,$timeAmount=null,$partial=false)use($dc,$enter,$g){
$rates=$g['rate_ids'];$versions=['material'=>1,'time'=>1];foreach(['material'=>$materialAmount,'time'=>$timeAmount] as $kind=>$amount)if($amount!==null){$u=$enter('prepare');$r=$dc->rate($g['order_id'],$u->id,['entry_id'=>$g[$kind.'_id'],'entry_version'=>1,'identifier'=>'QA-COST-'.strtoupper($kind),'version'=>2,'previous_id'=>$rates[$kind],'currency'=>'MZN','unit'=>$kind==='time'?'H':'UN','amount'=>$amount,'evidence'=>'Explicit rollback-only successor rate.'],(string)Str::uuid());$u=$enter('validate');$dc->decide($g['order_id'],$u->id,['kind'=>'rate','id'=>$r['id'],'version'=>2,'decision'=>'approved','reason'=>'Independent successor rate QA review.'],(string)Str::uuid());$rates[$kind]=$r['id'];$versions[$kind]=2;}
$lines=[];foreach(['material','time'] as $kind)$lines[]=['entry_id'=>$g[$kind.'_id'],'entry_version'=>1,'rate_id'=>$partial&&$kind==='material'?null:$rates[$kind],'rate_version'=>$partial&&$kind==='material'?null:$versions[$kind]];$u=$enter('prepare');$r=$dc->calculate($g['order_id'],$u->id,['version'=>2,'previous_id'=>$g['calculation_id'],'currency'=>'MZN','lines'=>$lines],(string)Str::uuid());$u=$enter('validate');$dc->decide($g['order_id'],$u->id,['kind'=>'calculation','id'=>$r['id'],'version'=>2,'decision'=>'approved','reason'=>'Validate selected composition only, preserve incompleteness where applicable.'],(string)Str::uuid());return $r;
};
$case('complete-source-rights-and-explicit-impact',function()use($ledger,$enter,$g,$capture,$command,$approve,$postBody,$ok,$refuse){
$w=$ledger->costs($enter('read'),$g['order_id']);$ok($w['cost']['source']['principal_minor']===27500&&$w['book']['journals']===[],'Approved cost is actual275 and not automatically posted');$p=$capture();$snap=json_decode(DB::table('gl_proposals')->where('id',$p['proposal_id'])->value('snapshot'),true);$ok(array_column($snap['lines'],'account_code')===['QA-CUSTO-DIRECTO','QA-COMPENSACAO-ENSAIO'],'Artificial impact explicit before review');$refuse(fn()=>$command('read','post',$postBody($p)));$refuse(fn()=>$command('prepare','post',$postBody($p)));$approve($p);$j=$command('prepare','post',$postBody($p));$other=$command('prepare','post',$postBody($p));$ok($j['journal_id']===$other['journal_id']&&DB::table('gl_journals')->count()===1,'Different key recovers one source effect');$refuse($capture);$refuse(fn()=>$ledger->costs($enter('read'),999999));
});
$case('partial175-and-non-cent-representable-cost',function()use($newCost,$ledger,$enter,$g,$refuse,$capture,$ok,$command){$newCost(null,null,true);$w=$ledger->costs($enter('read'),$g['order_id']);$ok($w['cost']['facts']['conference']['snapshot']['known_subtotal_exact']==='175'&&$w['cost']['source']===null&&$w['cost']['pending']!==null,'Approved incomplete composition retains175 known and no total');$refuse($capture);$refuse(fn()=>$command('prepare','period-prepare',['period_key'=>'QA-2026-10','reason'=>'Incomplete source cannot close.']));});
$case('exact-rational-no-rounding',function()use($newCost,$capture,$refuse,$ledger,$enter,$g,$ok){$newCost(null,'0.01');$w=$ledger->costs($enter('read'),$g['order_id']);$ok($w['cost']['facts']['conference']['snapshot']['selected_direct_total_exact']==='100.0175','Actual calculator preserves exact non-cent value');$refuse($capture);});
$case('revoked-source-rights-and-old-capture',function()use($capture,$approve,$command,$postBody,$newCost,$refuse,$g){$p=$capture();$approve($p);$newCost('60.00');$refuse(fn()=>$command('prepare','post',$postBody($p)));});
$case('revoked-rights-before-recovery',function()use($capture,$approve,$command,$postBody,$refuse,$scope,$g){$p=$capture();$approve($p);$key=(string)Str::uuid();$command('prepare','post',$postBody($p),$key);DB::table('permission_grants')->where($scope)->where('user_id',$g['actors']['prepare'])->where('permission','general_ledger.post')->delete();$refuse(fn()=>$command('prepare','post',$postBody($p),$key));});
$case('source-access-and-person-independence',function()use($capture,$approve,$refuse,$scope,$g,$enter,$command){$p=$capture();DB::table('pa_access')->where($scope)->where('user_id',$g['actors']['validate'])->update(['person'=>'cost-qa-person-'.$g['actors']['prepare']]);$refuse(fn()=>$approve($p));DB::table('permission_grants')->where($scope)->where('user_id',$g['actors']['prepare'])->where('permission','workshop.view')->delete();$refuse($capture);});
$case('fault-boundaries-atomic-recovery',function()use($capture,$approve,$postBody,$command,$ok){$p=$capture();$approve($p);foreach(['after_header','after_lines','after_event','after_receipt'] as $step){$caught=false;$key=(string)Str::uuid();$count=DB::table('gl_commands')->count();try{$command('prepare','post',$postBody($p),$key,function($name)use($step){if($name===$step)throw new RuntimeException('injected-'.$step);});}catch(RuntimeException $e){$caught=str_starts_with($e->getMessage(),'injected-');}$ok($caught&&!DB::table('gl_journals')->exists()&&!DB::table('gl_lines')->exists()&&DB::table('gl_commands')->count()===$count,'Atomic rollback at '.$step);}$key=(string)Str::uuid();$one=$command('prepare','post',$postBody($p),$key);$two=$command('prepare','post',$postBody($p),$key);$ok($two['replayed']&&$one['journal_id']===$two['journal_id'],'Retry after failures recovers one complete result');});
$case('stable-perimeter-no-second-recognition-and-close-refusal',function()use($capture,$approve,$command,$postBody,$newCost,$refuse,$ok){$p=$capture();$approve($p);$command('prepare','post',$postBody($p));$newCost('60.00');$refuse($capture);$refuse(fn()=>$command('prepare','period-prepare',['period_key'=>'QA-2026-10','reason'=>'Changed already-posted cost requires an explicit future correction scope.']));$ok(DB::table('gl_journals')->count()===1,'Successor calculation cannot create another principal recognition');});
$case('guards-and-perimeter-immutability',function()use($ok){$old=config('cost_ledger.synthetic_enabled');config(['cost_ledger.synthetic_enabled'=>false]);$caught=false;try{App\Shared\Testing\TestingDatabaseGuard::assertSafe();}catch(RuntimeException){$caught=true;}finally{config(['cost_ledger.synthetic_enabled'=>$old]);}$ok($caught,'No QA flag means guard refuses this isolated extension');DB::beginTransaction();try{DB::table('gl_cost_sources')->update(['reason'=>'Cannot replace selected perimeter.']);throw new LogicException('Expected append-only refusal');}catch(Illuminate\Database\QueryException){DB::rollBack();$ok(true,'Mapping immutable');}});
$ok($sourceFingerprint()===$before&&!DB::table('gl_journals')->exists()&&!DB::table('gl_proposals')->exists(),'All original workshop/cost/outbox/rights state restored after rollback-only cases');file_put_contents($output,json_encode(['passed'=>true,'checks'=>$checks,'cases'=>$cases,'rollback_only'=>true,'source_outbox_rights_unchanged'=>true,'no_rounding_or_duplicate_recognition'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo "Cost regression: ".count($cases)." cases/$checks checks; original sources/outbox/rights unchanged.\n";