Files
SGF/scripts/sgf-fiscal-rules-proof.php
T
2026-11-11 09:00:17 +02:00

19 lines
7.9 KiB
PHP

<?php
use Illuminate\Support\Facades\{DB,Crypt};
require __DIR__.'/sgf-fiscal-rules-proof-bootstrap.php';$before=[];foreach(['fr_events','fr_commands','fr_calculations','fx_versions','fx_commands','si_invoices','sr_effects','cash_events']as$t)$before[$t]=$host->table($t)->orderBy('id')->get()->toJson();$nativeBefore=$native->table('gl_journals')->orderBy('id')->get()->toJson();$host->beginTransaction();try{
$revision=(int)$host->table('fr_events')->where('rule_key','QA-RATE-TRANSVERSAL')->max('revision');$p=$rulePreview(13,$ruleInput('prepare',$revision,$def(0)));$ok($host->table('fr_events')->orderBy('id')->get()->toJson()===$before['fr_events'],'preview fully reverted');$reject(fn()=>$rulePreview(17,$ruleInput('prepare',$revision,$def(0))),403,'read-only writer refused');$reject(fn()=>$rulePreview(13,$ruleInput('prepare',$revision,array_replace($def(0),['source_sha256'=>str_repeat('0',64)]))),422,'checked source hash mismatch refused');$bad=$def(0);$bad['expression']['nodes']['base']=['op'=>'execute','code'=>'phpinfo()'];$reject(fn()=>$rulePreview(13,$ruleInput('prepare',$revision,$bad)),422,'executable expression rejected');
$selectRule(13);$prepared=$rules->confirm(13,$p['preview']);$replay=$rules->confirm(13,$p['preview']);$ok($replay['replayed']&&$replay['id']===$prepared['id'],'rule key repetition no duplicate');$pending=$rulePreview(13,$calcInput($prepared['id']));$ok($pending['impact']['result']['state']==='pending','unreviewed rule never executable');$reject(fn()=>$rulePreview(13,$ruleInput('review',$prepared['revision'],['decision'=>'reviewed'],$prepared['id'])),403,'preparer cannot review');
$reviewA=$rulePreview(14,$ruleInput('review',$prepared['revision'],['decision'=>'reviewed'],$prepared['id']));$reviewB=$rulePreview(14,$ruleInput('review',$prepared['revision'],['decision'=>'reviewed'],$prepared['id']));$selectRule(14);$v1=$rules->confirm(14,$reviewA['preview']);$reject(fn()=>$rules->confirm(14,$reviewB['preview']),409,'stale review version refused');
[$c1,$cut1]=$ruleCommand(13,$calcInput($v1['id']));$selectRule(17);$detail=$rules->detail(17,$c1['id'],'calculation');$ok($detail['result']['tax_exact']==='160'&&$detail['result']['tax_document']==='160.00'&&$detail['result']['total']==='1160.00','version1 Nov30 exact160 total1160');$ok(count($detail['result']['steps'])===3&&!$detail['result']['legal_use'],'reused declarative engine trace without legal claim');
[$prep2]=$ruleCommand(13,$ruleInput('prepare',$v1['revision'],$def(1)));[$v2]=$ruleCommand(14,$ruleInput('review',$prep2['revision'],['decision'=>'reviewed'],$prep2['id']));$after=$rulePreview(13,$calcInput($v2['id'],'2026-12-01'));$ok($after['impact']['result']['tax_document']==='170.00'&&$after['impact']['result']['total']==='1170.00','version2 Dec1 exact170 total1170');$old=$rulePreview(13,$calcInput($v1['id']));$ok($old['impact']['result']['tax_document']==='160.00','successor does not replace earlier effective version');$late=$rulePreview(13,$calcInput($v1['id'],'2026-12-01'));$ok($late['impact']['result']['state']==='pending'&&$late['impact']['result']['tax_document']===null,'out of validity indeterminate never zero');$early=$rulePreview(13,$calcInput($v2['id'],'2026-11-30'));$ok($early['impact']['result']['state']==='pending','future rule not applied early');$missing=$rulePreview(13,$calcInput(null));$ok($missing['impact']['result']['state']==='pending','no selected rule no automatic choice');$fx=$rulePreview(13,$calcInput($v1['id'],'2026-11-30','1000','USD'));$ok($fx['impact']['result']['state']==='pending','wrong currency pending');
$precise=$rulePreview(13,$calcInput($v1['id'],'2026-11-30','1000.03'));$r=$precise['impact']['result'];$ok($r['tax_exact']==='160.0048'&&$r['tax_document']==='160.00'&&$r['difference']==='0.0048'&&$r['total']==='1160.03','accepted SGF truncate2 case reused exactly');$eight=$rulePreview(13,$calcInput($v1['id'],'2026-11-30','1000.00000001'));$ok($eight['impact']['result']['state']==='pending_representation'&&$eight['impact']['result']['total']===null,'eight decimal input not silently truncated');
$metadata=$def(0);$metadata['family']='irps';$metadata['operation']='metadata_only';$metadata['source_status']='pending';$metadata['source_sha256']=null;$metadata['parameters']=[];$metadata['expression']=[];[$mp]=$ruleCommand(13,$ruleInput('prepare',0,$metadata,null,'QA-SOURCE-PENDING'));[$mr]=$ruleCommand(14,$ruleInput('review',1,['decision'=>'reviewed'],$mp['id'],'QA-SOURCE-PENDING'));$m=$rulePreview(13,$calcInput($mr['id']));$ok($m['impact']['result']['state']==='pending','reviewed source inventory not a legal tax engine');
[$conflict]=$ruleCommand(13,$ruleInput('prepare',0,$def(0),null,'QA-CONFLICT'));[$conflictReview]=$ruleCommand(14,$ruleInput('review',1,['decision'=>'reviewed'],$conflict['id'],'QA-CONFLICT'));$conf=$rulePreview(13,$calcInput($v1['id']));$ok(str_contains(implode(' ',$conf['impact']['result']['pending']),'Conflito'),'same priority overlap explicit pending');[$withdraw]=$ruleCommand(14,$ruleInput('withdraw',2,[],$conflictReview['id'],'QA-CONFLICT'));$recovered=$rulePreview(13,$calcInput($v1['id']));$ok($recovered['impact']['result']['state']==='determined_qa','withdraw conflict restores applicability without deleting history');
// An in-flight calculation must be checked against rule changes again.
$cut=$rulePreview(13,$calcInput($v1['id']));[$withdrawV1]=$ruleCommand(14,$ruleInput('withdraw',$v2['revision'],[],$v1['id']));$selectRule(13);$reject(fn()=>$rules->confirm(13,$cut['preview']),409,'changed rules invalidate prepared calculation');$selectRule(17);$historical=$rules->detail(17,$c1['id'],'calculation');$ok($historical['result']===$detail['result']&&$historical['current_evaluation']['state']==='pending','withdrawal preserves historic amount and exposes current pending');
$selectRule(13);$key=$host->table('fr_commands')->where('actor_id',13)->whereRaw("result->>'type'='calculation'")->orderByDesc('id')->value('command_key');$ok($rules->receipt(13,$key)['operation_executed']===false,'historical own receipt is read-only');$selectRule(14);$reject(fn()=>$rules->receipt(14,$key),404,'another actor cannot recover command key');
$selectRule(17);$listing=$rules->listing(17,['view'=>'rules','per_page'=>100]);$ok($listing['records']['total']>=4&&!$listing['capabilities']['prepare']&&!$listing['capabilities']['review'],'catalogue paged with explicit read-only capabilities');$reject(fn()=>$rules->listing(17,['tenant_id'=>2]),422,'client scope injection refused');$reject(fn()=>$rules->listing(17,['per_page'=>500]),422,'unbounded catalogue page refused');
// Failure after an event but before command receipt is atomic and recoverable.
$p=$rulePreview(13,$ruleInput('prepare',0,$def(1),null,'QA-RECOVERY'));$host->beginTransaction();try{$host->statement("CREATE FUNCTION fr_fail_test() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'Injected rule failure'; END; $$");$host->statement('CREATE TRIGGER fr_fail_test BEFORE INSERT ON fr_commands FOR EACH ROW EXECUTE FUNCTION fr_fail_test()');$n=$host->table('fr_events')->count();$selectRule(13);$failed=false;try{$rules->confirm(13,$p['preview']);}catch(Illuminate\Database\QueryException){$failed=true;}$ok($failed&&$host->table('fr_events')->count()===$n,'command failure rolls back inserted rule');}finally{$host->rollBack();}$selectRule(13);$success=$rules->confirm(13,$p['preview']);$ok($success['state']==='prepared','same command recovers after failure');
}finally{$host->rollBack();}foreach($before as$t=>$json)$ok($host->table($t)->orderBy('id')->get()->toJson()===$json,'unchanged '.$t);$ok($native->table('gl_journals')->orderBy('id')->get()->toJson()===$nativeBefore,'native ledger untouched');file_put_contents($work.'/fiscal-rules-result.json',json_encode(['passed'=>true,'checks'=>$checks,'rolled_back'=>true,'new_journals'=>0,'new_demo'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/fiscal-rules-result.json']).PHP_EOL;