make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); ServiceReceivables::guard(); $f=json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'),true,32,JSON_THROW_ON_ERROR); $fx=$f+$f['receivables']+['manager'=>\App\Models\User::findOrFail($f['manager_id']),'reader'=>\App\Models\User::findOrFail($f['reader_id'])]; $s=app(ServiceReceivables::class); $manager=$f['manager_id']; $reader=$f['reader_id']; $proof=[]; $select=fn($actor)=>app(CompanyContext::class)->selectFor($actor,$f['tenant'],$f['company'],$f['unit']); $rev=fn($ob)=>DB::table('sr_effects')->where('obligation_id',$ob)->count(); $base=['tenant'=>$f['tenant'],'company'=>$f['company'],'unit'=>$f['unit']]; $pair=function($a,$b)use($base) { $a=new Process([PHP_BINARY,'scripts/receivables-worker.php','--env=testing'],base_path(),null,json_encode($a+$base+['pause'=>true])); $b=new Process([PHP_BINARY,'scripts/receivables-worker.php','--env=testing'],base_path(),null,json_encode($b+$base)); $a->start(); if(!$a->waitUntil(fn($type,$buffer)=>str_contains($buffer,'LOCK_HELD'))) throw new RuntimeException('Overlap lock not established.'); $b->start(); $a->wait(); $b->wait(); $out=[]; foreach([$a,$b] as $p) { if(!$p->isSuccessful()) throw new RuntimeException('Synthetic worker failed.'); $lines=array_values(array_filter(explode("\n",trim($p->getOutput())),fn($v)=>str_starts_with($v,'{'))); $out[]=json_decode(end($lines),true,32,JSON_THROW_ON_ERROR); } return $out; }; $obligation=function()use(&$fx,$s,$select,$manager,$reader) { $fx=\Tests\Support\InvoiceFixture::additionalDraft($fx,'862.07'); $select($manager); $si=app(ServiceInvoicing::class); $review=$si->review($fx['draft'],$fx['series'],$fx['rule'],$manager); $invoice=$si->issue($fx['draft'],$manager,['series_id'=>$fx['series'],'rule_id'=>$fx['rule'],'review_hash'=>$review['review_hash']],(string)Str::uuid())['id']; $ob=$s->propose($manager,['kind'=>'obligation','invoice_id'=>$invoice,'currency'=>'MZN','source'=>'TEST-CONCURRENCY','external_id'=>(string)Str::uuid(),'revision'=>0],(string)Str::uuid())['id']; $select($reader); $s->decide($ob,$reader,['version'=>1,'revision'=>0,'decision'=>'approved','reason'=>'Open synthetic concurrency obligation.'],(string)Str::uuid()); return [$invoice,$ob]; }; $receipt=function($invoice,$ob,$amount,$confirm=true)use($s,$select,$manager,$reader,$rev) { $select($manager); $id=$s->propose($manager,['kind'=>'receipt','invoice_id'=>$invoice,'obligation_id'=>$ob,'amount'=>$amount,'currency'=>'MZN','date'=>now(ServiceInvoicing::ZONE)->format('Y-m-d'),'source'=>'TEST-CONCURRENCY','external_id'=>(string)Str::uuid(),'evidence'=>'Artificial concurrency evidence.','revision'=>$rev($ob)],(string)Str::uuid())['id']; if($confirm) { $select($reader); $s->decide($id,$reader,['version'=>1,'revision'=>$rev($ob),'decision'=>'approved','reason'=>'Synthetic concurrency confirmation.'],(string)Str::uuid()); } return $id; }; $decision=fn($id,$ob)=>['method'=>'decide','id'=>$id,'actor'=>$reader,'key'=>(string)Str::uuid(),'input'=>['version'=>1,'revision'=>$rev($ob),'decision'=>'approved','reason'=>'Concurrent synthetic decision.']]; [$invoice,$ob]=$obligation(); $r=$receipt($invoice,$ob,'400.00',false); $a=$decision($r,$ob); $out=$pair($a,$a); if($out[0]['status']!==200 || $out[1]['status']!==200 || !$out[1]['result']['replayed'] || $out[0]['result']['id']!==$out[1]['result']['id']) throw new RuntimeException('Confirmation replay failed.'); $proof['same_confirmation_one_effect_one_replay']=true; [$invoice,$ob]=$obligation(); $r=$receipt($invoice,$ob,'400.00'); $t=$receipt($invoice,$ob,'700.00'); $allocate=fn($id,$amount,$ob)=>['method'=>'allocate','id'=>$id,'actor'=>$manager,'key'=>(string)Str::uuid(),'input'=>['version'=>1,'revision'=>$rev($ob),'amount'=>$amount]]; $out=$pair($allocate($r,'400.00',$ob),$allocate($t,'700.00',$ob)); if(array_column($out,'status')!==[200,409]) throw new RuntimeException('Document overspend race failed.'); $proof['document_400_plus_700_one_applied_one_conflict']=true; [$invoice,$ob]=$obligation(); $r=$receipt($invoice,$ob,'700.00'); $out=$pair($allocate($r,'400.00',$ob),$allocate($r,'400.00',$ob)); if(array_column($out,'status')!==[200,409]) throw new RuntimeException('Receipt overspend race failed.'); $proof['same_receipt_available_not_double_spent']=true; // Raw SQL writers share the same company lock and aggregate/version invariants. [$invoice,$ob]=$obligation(); $r=$receipt($invoice,$ob,'700.00'); $t=$receipt($invoice,$ob,'400.00'); $sql=fn($id,$amount)=>['method'=>'sql_allocate','actor'=>$manager,'sql'=>['record_id'=>$id,'obligation_id'=>$ob,'receipt_id'=>$id,'kind'=>'allocate','amount'=>$amount,'revision'=>$rev($ob),'data'=>'{}','actor_id'=>$manager,'created_at'=>now()->toIso8601String()]]; $out=$pair($sql($r,'700.00'),$sql($t,'400.00')); if(array_column($out,'status')!==[200,409] || !($out[1]['database_guard']??false)) throw new RuntimeException('SQL aggregate race failed.'); $proof['raw_sql_concurrent_aggregate_guard']=true; [$invoice,$ob]=$obligation(); $r=$receipt($invoice,$ob,'400.00'); $select($manager); $af=$s->allocate($r,$manager,['version'=>1,'revision'=>$rev($ob),'amount'=>'400.00'],(string)Str::uuid())['id']; $obs=$s->propose($manager,['kind'=>'observation','invoice_id'=>$invoice,'obligation_id'=>$ob,'receipt_id'=>$r,'amount'=>'400.00','currency'=>'MZN','date'=>now(ServiceInvoicing::ZONE)->format('Y-m-d'),'source'=>'TEST-CONCURRENCY','external_id'=>(string)Str::uuid(),'evidence'=>'Artificial observation.','revision'=>$rev($ob)],(string)Str::uuid())['id']; $corr=$s->propose($manager,['kind'=>'correction','action'=>'reverse_allocation','target_effect_id'=>$af,'invoice_id'=>$invoice,'obligation_id'=>$ob,'currency'=>'MZN','source'=>'TEST-CONCURRENCY','external_id'=>(string)Str::uuid(),'reason'=>'Explicit reversal.','revision'=>$rev($ob)],(string)Str::uuid())['id']; $out=$pair($decision($corr,$ob),$decision($obs,$ob)); if(array_column($out,'status')!==[200,409]) throw new RuntimeException('Reversal versus reconciliation race failed.'); $proof['reversal_reconciliation_stale_decision_refused']=true; // Two incompatible reductions against exactly the same reviewed revision. [$invoice,$ob]=$obligation(); $select($manager); $corrs=[]; foreach(['A','B'] as $ref) $corrs[]=$s->propose($manager,['kind'=>'correction','action'=>'reduction','amount'=>'100.00','document_ref'=>'TEST-REDUCTION-CONCURRENT-'.Str::uuid(),'line'=>'1','invoice_id'=>$invoice,'obligation_id'=>$ob,'currency'=>'MZN','source'=>'TEST-CONCURRENCY','external_id'=>(string)Str::uuid(),'reason'=>'Synthetic reduction proposal '.$ref,'revision'=>$rev($ob)],(string)Str::uuid())['id']; $out=$pair($decision($corrs[0],$ob),$decision($corrs[1],$ob)); if(array_column($out,'status')!==[200,409]) throw new RuntimeException('Concurrent corrections failed.'); $proof['concurrent_corrections_exact_revision']=true; file_put_contents(base_path('docs/proofs/receivables-concurrency.json'),json_encode(['database'=>'sgf_tempo_test','overlapping_processes_per_case'=>2,'checks'=>$proof,'real_operations'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)); echo "Six real overlapping-process scenarios passed, including raw SQL aggregate protection; sgf_tempo_test only.\n";