77 lines
11 KiB
PHP
77 lines
11 KiB
PHP
<?php
|
|
use App\Modules\Purchasing\SupplierPayables;
|
|
use Illuminate\Support\Str;
|
|
use Tempo\Finance\Payables\Payables;
|
|
$out=$s['root'].'/'.$phase.'.json';if(file_exists($out)||!($g['cycle_enabled']??false))throw new LogicException('Fresh cycle proof required');
|
|
$finance=$cycleFinance??app(SupplierPayables::class);
|
|
$checks=0;$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-state/authority refusal');};
|
|
$fin=fn(string $role,string $action,array $body)=>$finance->engine($enter($role))->settlements()->command($action,$body,(string)Str::uuid());
|
|
$post=function(array $p,string $reason='Apply reviewed synthetic cycle effect.')use($command){$command('validate','validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Independent exact cycle effect review.']);return $command('prepare','post',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>$reason]);};
|
|
$effect=fn(string $type,int $id,string $rule)=>$command('prepare','effect-prepare',['effect_type'=>$type,'source_id'=>$id,'journal_on'=>'2026-10-29','period_key'=>'QA-2026-10','rule_key'=>$rule,'reason'=>'Capture confirmed financial fact without changing debt.']);
|
|
$baseline=$financialFingerprint();$rollback=in_array($phase,['ledger-cycle-cases','native-cycle-cases'],true);if($rollback)DB::beginTransaction();
|
|
try{
|
|
$refuse(fn()=>$command('prepare','period-prepare',['period_key'=>'QA-2026-10','reason'=>'Cannot close unaccounted recognised debt.']));
|
|
$recognition=$post($command('prepare','prepare',$g['body']));
|
|
$sourceRevision=DB::table('pa_obligations')->where('id',$g['obligation_id'])->value('source_revision');
|
|
$payment=$fin('prepare','payment-prepare',['obligation_id'=>$g['obligation_id'],'source_revision'=>$sourceRevision,'amount'=>'20.00','currency'=>'MZN','paid_on'=>'2026-10-29','fact_reference'=>'SIM-CYCLE-PAYMENT-20','evidence_reference'=>'SIM-CYCLE-EVIDENCE-20','reason'=>'Explicit synthetic payment fact; no execution.']);
|
|
$fin('validate','payment-decide',['payment_id'=>$payment['payment_id'],'decision'=>'confirmed','evidence_reference'=>'SIM-CYCLE-CONFIRMED-20','reason'=>'Independent synthetic payment evidence.']);
|
|
$plan=$fin('prepare','allocation-prepare',['payment_id'=>$payment['payment_id'],'reference'=>'SIM-CYCLE-ALLOCATION-20','allocations'=>[['obligation_id'=>$g['obligation_id'],'source_revision'=>$sourceRevision,'amount'=>'20.00']],'reason'=>'Explicit partial allocation of twenty.']);
|
|
$refuse(fn()=>$command('prepare','period-prepare',['period_key'=>'QA-2026-10','reason'=>'Pending financial allocation prevents closing.']));
|
|
$fin('validate','allocation-decide',['proposal_id'=>$plan['proposal_id'],'decision'=>'confirmed','reason'=>'Independent partial allocation confirmation.']);
|
|
$allocation=(int)DB::table('ps_allocations')->where('obligation_id',$g['obligation_id'])->value('id');
|
|
$position=$finance->engine($enter('read'))->positionForSource($g['local_source_id']);$ok($position['principal_minor']===6000&&$position['balance_minor']===4000,'Partial allocation preserves principal and balance40');
|
|
$refuse(fn()=>$command('prepare','period-prepare',['period_key'=>'QA-2026-10','reason'=>'Unaccounted confirmed allocation prevents closing.']));
|
|
$p=$effect('allocation.confirmed',$allocation,'QA-ALLOCATION');
|
|
$refuse(fn()=>$command('read','post',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Reader cannot post financial effect.']));
|
|
$refuse(fn()=>$command('validate','validate',['proposal_id'=>$p['proposal_id'],'revision'=>str_repeat('0',64),'decision'=>'approved','reason'=>'Old revision must refuse.']));
|
|
$allocated=$post($p);$j=DB::table('gl_journals')->where('id',$allocated['journal_id'])->first();$ok((int)$j->debit_minor===2000&&json_decode($j->snapshot,true)['source']['principal_minor']===6000,'Journal amount20 is distinct from principal60');
|
|
$r=$fin('prepare','reversal-prepare',['allocation_id'=>$allocation,'source_revision'=>$sourceRevision,'reason'=>'Reverse the full synthetic allocation.']);
|
|
$fin('validate','reversal-confirm',['proposal_id'=>$r['proposal_id'],'reason'=>'Independent confirmation of integral reversal.']);
|
|
$reversal=(int)DB::table('ps_reversals')->where('allocation_id',$allocation)->value('id');
|
|
$reversed=$post($effect('allocation.reversed',$reversal,'QA-REVERSAL'));
|
|
$position=$finance->engine($enter('read'))->positionForSource($g['local_source_id']);$ok($position['balance_minor']===6000,'Confirmed reversal restores balance60 without changing principal');
|
|
$correction=$command('prepare','correct-prepare',['journal_id'=>$recognition['journal_id'],'journal_on'=>'2026-10-29','period_key'=>'QA-2026-10','reason'=>'Technical correction with unchanged financial debt.']);
|
|
$refuse(fn()=>$command('prepare','correct-post',['correction_id'=>$correction['correction_id'],'revision'=>$correction['revision'],'reason'=>'Cannot apply unreviewed correction.']));
|
|
$command('validate','correct-validate',['correction_id'=>$correction['correction_id'],'revision'=>$correction['revision'],'decision'=>'approved','reason'=>'Independent review of both corrective journal impacts.']);
|
|
$body=['correction_id'=>$correction['correction_id'],'revision'=>$correction['revision'],'reason'=>'Atomically apply full reversal and replacement.'];$key=(string)Str::uuid();
|
|
if($phase==='ledger-cycle-race-prepare'){
|
|
file_put_contents($s['root'].'/ledger-cycle-race-prepared.json',json_encode(['passed'=>true,'checks'=>$checks,'correction_body'=>$body,'recognition_journal_id'=>$recognition['journal_id'],'allocation_id'=>$allocation,'reversal_id'=>$reversal],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo "Prepared independent corrective pair for real concurrent workers.\n";exit;
|
|
}
|
|
if($rollback){
|
|
$n=DB::table('gl_journals')->count();$fault=function($at){if($at==='after_correction_first')throw new RuntimeException('QA pair fault');};
|
|
try{$command('prepare','correct-post',$body,$key,$fault);throw new LogicException('Fault required');}catch(RuntimeException $e){$ok($e->getMessage()==='QA pair fault','Fault reached first corrective journal');}
|
|
$ok(DB::table('gl_journals')->count()===$n&&!DB::table('gl_correction_results')->exists(),'First journal rolled back with whole corrective pair');
|
|
}
|
|
$corrected=$command('prepare','correct-post',$body,$key);$repeat=$command('prepare','correct-post',$body,$key);$other=$command('prepare','correct-post',$body);
|
|
$ok($repeat['replayed']&&$other['result_id']===$corrected['result_id']&&DB::table('gl_correction_results')->count()===1,'Same/different keys preserve one correction pair');
|
|
$original=DB::table('gl_journals')->where('id',$recognition['journal_id'])->first();$ok($original&&DB::table('gl_journals')->count()===5&&DB::table('gl_lines')->count()===10,'Original plus allocation/reversal and atomic corrective pair retained');
|
|
$period=$command('prepare','period-prepare',['period_key'=>'QA-2026-10','reason'=>'Capture complete reconciled artificial period.']);
|
|
$refuse(fn()=>$command('read','period-close',['check_id'=>$period['check_id'],'revision'=>$period['revision'],'reason'=>'Reader cannot close.']));
|
|
if($rollback){
|
|
DB::beginTransaction();try{
|
|
$rule=(array)DB::table('gl_rules')->where('rule_key','QA-ALLOCATION')->first();unset($rule['id']);$rule['version']++;DB::table('gl_rules')->insert($rule);
|
|
$refuse(fn()=>$command('validate','period-validate',['check_id'=>$period['check_id'],'revision'=>$period['revision'],'decision'=>'approved','reason'=>'Changed evidence requires a fresh close conference.']));
|
|
}finally{DB::rollBack();}
|
|
DB::beginTransaction();try{
|
|
if($native??false)DB::table('pa_access')->where($scope)->where(['user_id'=>$f['actors']['validate'],'action'=>'general_ledger.period_validate'])->delete();else DB::table('permission_grants')->where($scope)->where(['user_id'=>$f['actors']['validate'],'permission'=>'general_ledger.period_validate'])->delete();
|
|
$refuse(fn()=>$command('validate','period-validate',['check_id'=>$period['check_id'],'revision'=>$period['revision'],'decision'=>'approved','reason'=>'Revoked period right must refuse.']));
|
|
}finally{DB::rollBack();}
|
|
}
|
|
$command('validate','period-validate',['check_id'=>$period['check_id'],'revision'=>$period['revision'],'decision'=>'approved','reason'=>'Independent complete period evidence and totals review.']);
|
|
$close=['check_id'=>$period['check_id'],'revision'=>$period['revision'],'reason'=>'Close artificial period with preserved evidence.'];$closeKey=(string)Str::uuid();
|
|
if($rollback){
|
|
try{$command('prepare','period-close',$close,$closeKey,function($at){if($at==='after_period_version')throw new RuntimeException('QA close fault');});throw new LogicException('Fault required');}catch(RuntimeException $e){$ok($e->getMessage()==='QA close fault','Close fault reached version append');}
|
|
$ok(DB::table('gl_periods')->count()===1&&!DB::table('gl_period_closures')->exists(),'Failed close leaves open period intact');
|
|
}
|
|
$closed=$command('prepare','period-close',$close,$closeKey);$again=$command('prepare','period-close',$close,$closeKey);$semantic=$command('prepare','period-close',$close);
|
|
$ok($closed['state']==='closed'&&$again['replayed']&&$semantic['closure_id']===$closed['closure_id'],'Closed period recovers receipt without reopening');
|
|
$refuse(fn()=>$effect('allocation.confirmed',$allocation,'QA-ALLOCATION'));
|
|
$refuse(fn()=>$command('prepare','correct-prepare',['journal_id'=>$recognition['journal_id'],'journal_on'=>'2026-10-29','period_key'=>'QA-2026-10','reason'=>'No second correction to same original.']));
|
|
DB::select('SELECT gl_assert_balanced(id) FROM gl_journals');
|
|
$totals=$ledger->listing($enter('read'))['totals'];$ok($totals===['debit_minor'=>22000,'credit_minor'=>22000,'net_minor'=>0],'Five journals reconcile gross debits and credits220');
|
|
$result=['passed'=>true,'checks'=>$checks,'rollback_only'=>$rollback,'principal_minor'=>6000,'allocated_balance_minor'=>4000,'final_balance_minor'=>6000,'debit_minor'=>22000,'credit_minor'=>22000,'journals'=>5,'lines'=>10,'correction_pairs'=>1,'period_closed'=>true,'obligation_id'=>$g['obligation_id'],'recognition_journal_id'=>$recognition['journal_id'],'allocation_id'=>$allocation,'reversal_id'=>$reversal,'correction_id'=>$correction['correction_id'],'period_check_id'=>$period['check_id']];
|
|
}finally{if($rollback)while(DB::transactionLevel()>0)DB::rollBack();}
|
|
if($rollback){$ok($financialFingerprint()===$baseline&&DB::table('gl_journals')->count()===0,'Adversarial cycle fully rolled back');$result['checks']=$checks;}
|
|
file_put_contents($out,json_encode($result,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo "Cycle complete: $checks checks, ".($rollback?'rollback-only':'persisted QA workflow').".\n";
|