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

48 lines
8.3 KiB
PHP

<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use App\Modules\Services\ServiceReceivables;
use App\Shared\Testing\TestingDatabaseGuard;
use Tempo\Finance\Payables\Payables;
$out=$s['root'].'/sales-cases.json';if(file_exists($out))throw new LogicException('Completed sales proof preserved');$checks=0;$cases=[];
$ok=function(bool $v,string $why)use(&$checks){$checks++;if(!$v)throw new LogicException($why);};
$refuse=function(Closure $fn)use($ok){$caught=false;try{$fn();}catch(DomainException|Symfony\Component\HttpKernel\Exception\HttpException $e){$caught=true;}$ok($caught,'Expected current source/right/version refusal');};
$fingerprint=function(){ $h=hash_init('sha256');foreach(['si_invoices','si_proposals','si_decisions','si_commands','sr_records','sr_effects','sr_commands','sr_events','gl_proposals','gl_reviews','gl_journals','gl_lines','gl_commands','gl_events','permission_grants','pa_access'] as $t){hash_update($h,$t);foreach(DB::table($t)->orderBy('id')->cursor() as $r)hash_update($h,Payables::canonical((array)$r));}return hash_final($h);};$before=$fingerprint();
$case=function($name,Closure $fn)use(&$cases,&$checks,$enter){$n=$checks;DB::beginTransaction();try{$fn();$cases[]=['name'=>$name,'checks'=>$checks-$n,'passed'=>true];}finally{while(DB::transactionLevel()>0)DB::rollBack();$enter('read');}echo "Passed sales case: $name\n";};
$body=fn($kind,$id,$rule)=>['effect_type'=>$kind,'source_id'=>$id,'journal_on'=>'2026-10-29','period_key'=>'QA-2026-10','rule_key'=>$rule,'reason'=>'Explicit exact sales test source capture.'];
$open=function()use($sr,$enter,$g){$r=$sr->propose($enter('prepare')->id,['kind'=>'obligation','invoice_id'=>$g['invoice_id'],'currency'=>'MZN','source'=>'TEST-MANUAL','external_id'=>(string)Str::uuid(),'revision'=>0,'reason'=>'Individual documentary opening proposal.'],(string)Str::uuid());$sr->decide($r['id'],$enter('validate')->id,['version'=>1,'revision'=>0,'decision'=>'approved','reason'=>'Independent opening approval.'],(string)Str::uuid());return $r['id'];};
$post=function($p)use($command){$command('validate','validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Independent exact source review.']);return $command('prepare','post',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Post the exact approved source once.']);};
$case('source-opening-rights-independence-and-three-line-recovery',function()use($open,$post,$body,$command,$enter,$ledger,$g,$scope,$ok,$refuse){
$refuse(fn()=>$command('prepare','effect-prepare',$body('sales.invoice',$g['invoice_id'],'QA-SALES')));$ob=$open();
$refuse(fn()=>$command('prepare','effect-prepare',$body('sales.invoice',999999,'QA-SALES')));
foreach(['service_invoices.view','service_receivables.view'] as $right){DB::beginTransaction();try{DB::table('permission_grants')->where($scope)->where(['user_id'=>$g['actors']['prepare'],'permission'=>$right])->delete();$refuse(fn()=>$command('prepare','effect-prepare',$body('sales.invoice',$g['invoice_id'],'QA-SALES')));}finally{DB::rollBack();}}
$p=$command('prepare','effect-prepare',$body('sales.invoice',$g['invoice_id'],'QA-SALES'));
$refuse(fn()=>$command('read','post',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Reader cannot post.']));
DB::beginTransaction();try{DB::table('permission_grants')->insert($scope+['user_id'=>$g['actors']['prepare'],'permission'=>'general_ledger.validate','created_at'=>now(),'updated_at'=>now()]);$refuse(fn()=>$command('prepare','validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Own review must refuse.']));}finally{DB::rollBack();}
DB::beginTransaction();try{DB::table('pa_access')->where($scope)->where(['user_id'=>$g['actors']['validate'],'action'=>'view'])->update(['person'=>'sales-qa-person-'.$g['actors']['prepare']]);$refuse(fn()=>$command('validate','validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Same person with another account must refuse.']));}finally{DB::rollBack();}
$refuse(fn()=>$command('validate','validate',['proposal_id'=>$p['proposal_id'],'revision'=>str_repeat('0',64),'decision'=>'approved','reason'=>'Stale revision refused.']));
$command('validate','validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Independent invoice impact approval.']);$d=['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Apply three-line invoice exactly once.'];$key=(string)Str::uuid();
foreach(['after_header','after_lines','after_event','after_receipt'] as $boundary){try{$command('prepare','post',$d,$key,function($at)use($boundary){if($at===$boundary)throw new RuntimeException('QA '.$boundary);});throw new LogicException('Fault required');}catch(RuntimeException $e){$ok($e->getMessage()==='QA '.$boundary,'Failure reached '.$boundary);}$ok(!DB::table('gl_journals')->exists()&&!DB::table('gl_lines')->exists(),'No partial three-line journal');}
$j=$command('prepare','post',$d,$key);$again=$command('prepare','post',$d,$key);$other=$command('prepare','post',$d);$ok($again['replayed']&&$other['journal_id']===$j['journal_id']&&DB::table('gl_journals')->count()===1&&DB::table('gl_lines')->count()===3,'Same/different keys cannot duplicate invoice');
$refuse(fn()=>$command('prepare','post',array_replace($d,['reason'=>'Different reason content.']),$key));
$data=$ledger->listing($enter('read'),['sales_invoice_id'=>$g['invoice_id']]);$ok($data['totals']['debit_minor']===100000&&array_map(fn($l)=>(int)$l->credit_minor,$data['lines'])===[0,86207,13793],'Documentary tax is preserved exactly, not recomputed');
});
$case('current-rule-period-and-scope-revalidation',function()use($open,$body,$command,$g,$scope,$refuse,$ok){
$open();$p=$command('prepare','effect-prepare',$body('sales.invoice',$g['invoice_id'],'QA-SALES'));
foreach(['rule','period'] as $kind){DB::beginTransaction();try{
if($kind==='rule'){$r=(array)DB::table('gl_rules')->where('rule_key','QA-SALES')->first();unset($r['id']);$r['version']++;$r['reason']='Explicit QA changed rule';DB::table('gl_rules')->insert($r);}else{$r=(array)DB::table('gl_periods')->first();unset($r['id']);$r['version']++;$r['reason']='Explicit QA period version change';DB::table('gl_periods')->insert($r);}
$refuse(fn()=>$command('validate','validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Changed source configuration must refuse.']));
}finally{DB::rollBack();}}
$refuse(fn()=>$command('prepare','effect-prepare',$body('sales.invoice',$g['invoice_id'],'QA-RECEIVABLE')));
$q=DB::table('si_invoices')->where('id',$g['invoice_id'])->first();$ok($q->tenant_id===$scope['tenant_id']&&$q->unit_id===$scope['unit_id'],'Exact source scope recorded');
$refuse(fn()=>$command('prepare','period-prepare',['period_key'=>'QA-2026-10','reason'=>'Unaccounted issued invoice prevents closing.']));
});
$case('guard-negative-and-page-query-contract',function()use($ledger,$enter,$g,$ok,$refuse){
$data=$ledger->sales($enter('read'),$g['invoice_id']);$ok($data['commercial']['position']===null&&!$data['book']['journals'],'Issued invoice alone is not an approved debt or posted journal');
$refuse(fn()=>$ledger->sales($enter('read'),999999));
$old=config('sales_ledger.synthetic_enabled');config(['sales_ledger.synthetic_enabled'=>false]);$caught=false;try{TestingDatabaseGuard::assertSafe();}catch(RuntimeException){$caught=true;}finally{config(['sales_ledger.synthetic_enabled'=>$old]);}$ok($caught,'A testing environment alone cannot enable owned QA extension');
$old=config('database.connections.pgsql.port');config(['database.connections.pgsql.port'=>'55432']);$caught=false;try{TestingDatabaseGuard::assertSafe();}catch(RuntimeException){$caught=true;}finally{config(['database.connections.pgsql.port'=>$old]);}$ok($caught,'Wrong configured cluster refused without switching connection');
});
$ok($fingerprint()===$before&&!DB::table('sr_effects')->exists()&&!DB::table('gl_journals')->exists(),'All probes rolled back; emitted document and fixture access unchanged');
file_put_contents($out,json_encode(['passed'=>true,'cases'=>$cases,'checks'=>$checks,'rollback_only'=>true,'issued_document_and_rights_preserved'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo "Sales regression: ".count($cases)." cases, $checks checks.\n";