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

19 lines
11 KiB
PHP

<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use App\Shared\Context\CompanyContext;
$output=$s['root'].'/industrial-cases.json';if(file_exists($output)||DB::table('gl_journals')->where('snapshot->source->effect_type','industry.cost.validated')->exists())throw new LogicException('Fresh industrial regression fixture required');$before=$fingerprint();$checks=0;$cases=[];$ok=function($v,$m)use(&$checks){$checks++;if(!$v)throw new LogicException($m);};
$refuse=function($fn,$text=null)use($ok){$caught=false;try{$fn();}catch(DomainException|Symfony\Component\HttpKernel\Exception\HttpException|Illuminate\Auth\Access\AuthorizationException $e){$caught=$text===null||str_contains($e->getMessage(),$text);}$ok($caught,'Expected industrial source/version/access refusal'.($text?' '.$text:''));};
$case=function($name,$fn)use(&$checks,&$cases,$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 industrial source and impact review.']);$postBody=fn($p)=>['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Explicit once-only industrial effect.'];
$successor=function($amount='50',$approve=true)use($i,$ic,$enter){$r=DB::table('ic_inputs')->find($i['refs']['material_rate']['id']);$data=json_decode($r->data,true);$data['amount']=$amount;$v=$ic->input($i['lot_id'],$enter('prepare')->id,['kind'=>'material_rate','source_ref'=>$r->source_ref,'version'=>2,'previous_id'=>$r->id,'data'=>$data,'evidence'=>'Rollback-only or race industrial successor rate.'],(string)Str::uuid());if($approve)$ic->decide($i['lot_id'],$enter('validate')->id,['type'=>'input','id'=>$v['id'],'version'=>2,'decision'=>'approved','reason'=>'Distinct exact successor industrial rate.'],(string)Str::uuid());return $v;};
$case('exact-owner-impact-and-no-automatic-residual',function()use($provider,$enter,$i,$ledger,$ok,$capture){$v=$provider->source($enter('read'),$i['lot_id']);$ok($v['principal_minor']===180000&&$v['good_units']==='10'&&$v['unit_cost_exact']==='180'&&$v['residual_exact']==='300','Actual1800/10/180/residual300');$p=$capture();$snap=json_decode(DB::table('gl_proposals')->where('id',$p['proposal_id'])->value('snapshot'),true);$ok(count($snap['lines'])===2&&array_column($snap['lines'],'account_code')===['QA-CUSTO-INDUSTRIAL','QA-COMPENSACAO-INDUSTRIAL']&&$snap['impact']['debit_minor']===180000,'Two-line exact impact excludes residual');$d=$ledger->industrial($enter('read'),$i['lot_id']);$html=view('accounting.industrial-ledger',['data'=>$d])->render();$ok(str_contains($html,'Residual 300 MZN')&&!str_contains($html,'data-action='),'Reader sees residual and no decisions');$ok($ledger->industrial($enter('read'))['book']['journals']===[],'No auto-selection or unrelated source mislabelling');});
$case('incomplete-result-and-current-dependencies',function()use($ic,$i,$enter,$provider,$refuse,$ok,$capture){$refs=$i['refs'];$refs['material_rate']=null;$r=$ic->calculate($i['lot_id'],$enter('prepare')->id,['version'=>2,'previous_id'=>$i['result_id'],'refs'=>$refs,'currency'=>'MZN'],(string)Str::uuid());$ic->decide($i['lot_id'],$enter('validate')->id,['type'=>'result','id'=>$r['id'],'version'=>2,'decision'=>'approved','reason'=>'Approve explicitly partial known800, not a complete industrial total.'],(string)Str::uuid());$c=$ic->conference($i['lot_id'],$enter('read')->id);$ok($c['snapshot']['known_cost_exact']==='800'&&$c['snapshot']['selected_total_exact']===null,'Incomplete result retains800 known, no total');$refuse($capture);});
$case('same-amount-new-version-stales-capture',function()use($capture,$approve,$successor,$refuse,$command,$postBody,$ic,$i,$enter,$provider,$ok){$p=$capture();$approve($p);$old=$provider->source($enter('read'),$i['lot_id']);$v=$successor();$refuse(fn()=>$command('prepare','post',$postBody($p)));$refs=$i['refs'];$refs['material_rate']=['id'=>$v['id'],'version'=>2];$r=$ic->calculate($i['lot_id'],$enter('prepare')->id,['version'=>2,'previous_id'=>$i['result_id'],'refs'=>$refs,'currency'=>'MZN'],(string)Str::uuid());$ic->decide($i['lot_id'],$enter('validate')->id,['type'=>'result','id'=>$r['id'],'version'=>2,'decision'=>'approved','reason'=>'Same amount1800, new exact input/result versions.'],(string)Str::uuid());$new=$provider->source($enter('read'),$i['lot_id']);$ok($old['origin_key']===$new['origin_key']&&$old['recognition_revision']!==$new['recognition_revision']&&$new['principal_minor']===180000,'Stable origin, different exact revision, same amount');$refuse(fn()=>$command('prepare','post',$postBody($p)));});
$case('live-permissions-person-and-config',function()use($scope,$g,$capture,$approve,$command,$postBody,$refuse,$provider,$enter,$i){foreach(['industrial_costs.view','general_ledger.view','general_ledger.prepare'] as $right){DB::beginTransaction();try{DB::table('permission_grants')->where($scope)->where('user_id',$g['actors']['prepare'])->where('permission',$right)->delete();$refuse($capture);}finally{DB::rollBack();}}$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));config(['industrial_ledger.synthetic_enabled'=>false]);try{$refuse(fn()=>$provider->source($enter('read'),$i['lot_id']));}finally{config(['industrial_ledger.synthetic_enabled'=>true]);}});
$case('eligible-foreign-tenant-company-unit',function()use($scope,$g,$provider,$enter,$i,$refuse){foreach(['unit','company','tenant'] as $kind){DB::beginTransaction();try{$u=$enter('read');$company=$kind==='unit'?DB::table('companies')->where('id',$scope['company_id'])->first():($kind==='company'?DB::table('companies')->where('tenant_id',$scope['tenant_id'])->where('id','<>',$scope['company_id'])->first():DB::table('companies')->where('tenant_id','<>',$scope['tenant_id'])->first());$unit=DB::table('units')->insertGetId(['tenant_id'=>$company->tenant_id,'company_id'=>$company->id,'name'=>'SQL-ONLY foreign industrial '.$kind,'created_at'=>now(),'updated_at'=>now()]);if(!DB::table('company_memberships')->where(['tenant_id'=>$company->tenant_id,'company_id'=>$company->id,'user_id'=>$u->id])->exists())DB::table('company_memberships')->insert(['tenant_id'=>$company->tenant_id,'company_id'=>$company->id,'user_id'=>$u->id]);foreach(['company.view'=>null,'unit.view'=>$unit,'industrial_costs.view'=>$unit] as $right=>$v)Tests\Support\IdentityFixture::grant($u->id,$company->tenant_id,$company->id,$right,$v);app(CompanyContext::class)->selectFor($u->id,$company->tenant_id,$company->id,$unit);$refuse(fn()=>$provider->source($u,$i['lot_id']));}finally{DB::rollBack();}}});
$case('atomic-faults-recovery-and-no-second-recognition',function()use($capture,$approve,$command,$postBody,$ok,$refuse){$p=$capture();$approve($p);$journals=DB::table('gl_journals')->count();foreach(['after_header','after_lines','after_event','after_receipt'] as $step){$caught=false;$commands=DB::table('gl_commands')->count();try{$command('prepare','post',$postBody($p),null,function($name)use($step){if($name===$step)throw new RuntimeException('injected-industrial');});}catch(RuntimeException $e){$caught=$e->getMessage()==='injected-industrial';}$ok($caught&&DB::table('gl_journals')->count()===$journals&&DB::table('gl_commands')->count()===$commands,'Atomic industrial rollback at '.$step);}$key=(string)Str::uuid();$j=$command('prepare','post',$postBody($p),$key);$r=$command('prepare','post',$postBody($p),$key);$other=$command('prepare','post',$postBody($p));$ok($r['replayed']&&$other['journal_id']===$j['journal_id'],'Same/other key no duplicate after fault');$refuse($capture);$refuse(fn()=>$command('prepare','correct-prepare',['journal_id'=>$j['journal_id'],'journal_on'=>'2026-10-29','period_key'=>'QA-2026-10','reason'=>'Industrial correction deliberately not authorised.']));});
$case('direct-origin-overlap-and-perimeter-immutability',function()use($i,$g,$provider,$enter,$refuse,$ok){$o=(array)DB::table('wo_orders')->where('id',$g['order_id'])->first();unset($o['id']);$o['reference']='SQL-ONLY-INDUSTRIAL-OVERLAP';$order=DB::table('wo_orders')->insertGetId($o);$m=(array)DB::table('gl_cost_sources')->where('order_id',$g['order_id'])->first();unset($m['id']);$m['order_id']=$order;$m['material_origin']=DB::table('ic_inputs')->where('id',$i['refs']['material']['id'])->value('source_ref');DB::table('gl_cost_sources')->insert($m);$refuse(fn()=>$provider->source($enter('read'),$i['lot_id']),'custo directo');DB::beginTransaction();$caught=false;try{DB::table('gl_industrial_sources')->where('lot_id',$i['lot_id'])->update(['reason'=>'No rewrite']);}catch(Illuminate\Database\QueryException){$caught=true;}finally{DB::rollBack();}$ok($caught,'Industrial mapping append-only');});
$case('bounded-industrial-cards-and-hidden-panel',function()use($i,$scope,$g,$enter,$ok){$lot=(array)DB::table('ic_lots')->where('id',$i['lot_id'])->first();$m=(array)DB::table('gl_industrial_sources')->where('lot_id',$i['lot_id'])->first();unset($lot['id'],$m['id']);for($n=0;$n<25;$n++){$lot['reference']='SQL-ONLY-IND-PAGE-'.$n;$id=DB::table('ic_lots')->insertGetId($lot);DB::table('gl_industrial_sources')->insert(array_merge($m,['lot_id'=>$id]));}$w=app(App\Modules\Accounting\LedgerComposition::class);$db=DB::connection();$db->enableQueryLog();$db->flushQueryLog();$a=$w->workspace($enter('read'));$qa=count($db->getQueryLog());$db->flushQueryLog();$b=$w->workspace($enter('read'),['industrial_page'=>2]);$qb=count($db->getQueryLog());$db->disableQueryLog();$ok(count($a['panels']['industrial']['rows'])===25&&count($b['panels']['industrial']['rows'])===1&&$b['panels']['industrial']['pagination']['total']===26,'Bounded industrial pages25+1');$ok($qa===$qb&&$a['panels']['sales']===$b['panels']['sales'],'No per-card queries or other page drift');$ok($b['panels']['industrial']['rows'][0]['result_id']===null,'No result never shown as zero/approved');DB::table('permission_grants')->where($scope)->where('user_id',$g['actors']['prepare'])->where('permission','industrial_costs.view')->delete();$d=$w->workspace($enter('prepare'));$ok($d['panels']['industrial']===['allowed'=>false]&&!str_contains(view('accounting.ledger-composition',['data'=>$d])->render(),'data-action='),'No industrial metadata/count or period forms without source right');});
$ok($fingerprint()===$before&&!DB::table('gl_journals')->where('snapshot->source->effect_type','industry.cost.validated')->exists(),'Original industrial evidence/outbox/rights unchanged after rollback-only probes');file_put_contents($output,json_encode(['passed'=>true,'checks'=>$checks,'cases'=>$cases,'rollback_only'=>true,'source_outbox_rights_unchanged'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo 'Industrial regression: '.count($cases)." cases/$checks checks.\n";