36 lines
7.0 KiB
PHP
36 lines
7.0 KiB
PHP
<?php
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Tempo\Finance\Payables\Payables;
|
|
$out=$s['root'].'/sales-integrity.json';if(file_exists($out))throw new LogicException('Completed integrity proof preserved');$checks=0;$ok=function($v,$why)use(&$checks){$checks++;if(!$v)throw new LogicException($why);};
|
|
$reject=function(Closure $fn)use($ok){$caught=false;try{$fn();}catch(DomainException|Symfony\Component\HttpKernel\Exception\HttpException){$caught=true;}$ok($caught,'Existing source must be refused in another authorised context');};
|
|
$before=hash('sha256',DB::table('si_invoices')->where('id',$g['invoice_id'])->value('snapshot'));$grants=DB::table('permission_grants')->count();
|
|
foreach(['unit','company','tenant'] as $axis){DB::beginTransaction();try{
|
|
$u=$enter('read');$q=DB::table('units');if($axis==='unit')$q->where('company_id',$scope['company_id'])->where('id','<>',$scope['unit_id']);elseif($axis==='company')$q->where('tenant_id',$scope['tenant_id'])->where('company_id','<>',$scope['company_id']);else $q->where('tenant_id','<>',$scope['tenant_id']);$unit=$q->first();
|
|
if(!$unit&&$axis==='tenant'){$company=DB::table('companies')->where('tenant_id','<>',$scope['tenant_id'])->first();$id=DB::table('units')->insertGetId(['tenant_id'=>$company->tenant_id,'company_id'=>$company->id,'name'=>'Foreign QA context probe','created_at'=>now(),'updated_at'=>now()]);$unit=DB::table('units')->where('id',$id)->first();}
|
|
if(!$unit)throw new LogicException('Explicit foreign fixture context missing');$foreign=['tenant_id'=>$unit->tenant_id,'company_id'=>$unit->company_id,'unit_id'=>$unit->id];
|
|
DB::table('company_memberships')->insertOrIgnore(['user_id'=>$u->id,'tenant_id'=>$foreign['tenant_id'],'company_id'=>$foreign['company_id']]);
|
|
foreach(['company.view'=>null,'unit.view'=>$unit->id,'general_ledger.view'=>$unit->id,'service_receivables.view'=>$unit->id,'service_invoices.view'=>$unit->id] as $right=>$which)Tests\Support\IdentityFixture::grant($u->id,$foreign['tenant_id'],$foreign['company_id'],$right,$which);
|
|
DB::table('pa_access')->insert($foreign+['user_id'=>$u->id,'action'=>'view','person'=>'sales-qa-person-'.$u->id]);app(App\Shared\Context\CompanyContext::class)->selectFor($u->id,$foreign['tenant_id'],$foreign['company_id'],$foreign['unit_id']);
|
|
$reject(fn()=>$ledger->sales($u,$g['invoice_id']));
|
|
}finally{DB::rollBack();$enter('read');}}
|
|
foreach(['service_invoices.view','service_receivables.view','general_ledger.view'] as $right){DB::beginTransaction();try{DB::table('permission_grants')->where($scope)->where(['user_id'=>$g['actors']['read'],'permission'=>$right])->delete();$reject(fn()=>$ledger->sales($enter('read'),$g['invoice_id']));}finally{DB::rollBack();}}
|
|
DB::beginTransaction();try{
|
|
$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'=>'Rollback-only integrity opening.'],(string)Str::uuid());$ob=$r['id'];$sr->decide($ob,$enter('validate')->id,['version'=>1,'revision'=>0,'decision'=>'approved','reason'=>'Independent integrity opening.'],(string)Str::uuid());
|
|
$make=function($kind,$id,$rule)use($command){$p=$command('prepare','effect-prepare',['effect_type'=>$kind,'source_id'=>$id,'journal_on'=>'2026-10-29','period_key'=>'QA-2026-10','rule_key'=>$rule,'reason'=>'Exact source for transaction-boundary integrity.']);$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 reviewed integrity source.']);};
|
|
$invoice=$make('sales.invoice',$g['invoice_id'],'QA-SALES');
|
|
$r=$sr->propose($enter('prepare')->id,['kind'=>'receipt','invoice_id'=>$g['invoice_id'],'obligation_id'=>$ob,'currency'=>'MZN','amount'=>'400.00','date'=>'2026-10-29','evidence'=>'Rollback-only receipt without money.','source'=>'TEST-MANUAL','external_id'=>(string)Str::uuid(),'revision'=>1,'reason'=>'Individual integrity receipt.'],(string)Str::uuid());$sr->decide($r['id'],$enter('validate')->id,['version'=>1,'revision'=>1,'decision'=>'approved','reason'=>'Independent integrity receipt confirmation.'],(string)Str::uuid());$a=$sr->allocate($r['id'],$enter('prepare')->id,['version'=>1,'revision'=>2,'amount'=>'400.00'],(string)Str::uuid());$allocation=$make('receivable.allocated',$a['id'],'QA-RECEIVABLE');
|
|
DB::beginTransaction();try{
|
|
DB::table('gl_lines')->insert($scope+['journal_id'=>$allocation['journal_id'],'line_no'=>3,'account_id'=>$g['accounts']['QA-VENDAS'],'debit_minor'=>1,'credit_minor'=>0]);$caught=false;try{DB::statement('SET CONSTRAINTS gl_sales_line_balance IMMEDIATE');}catch(Illuminate\Database\QueryException $e){$caught=str_contains($e->getMessage(),'GL_UNBALANCED_JOURNAL');}$ok($caught,'A third line cannot be appended to a completed two-line journal');
|
|
}finally{DB::rollBack();}
|
|
DB::beginTransaction();try{$caught=false;try{DB::table('gl_lines')->insert($scope+['journal_id'=>$invoice['journal_id'],'line_no'=>4,'account_id'=>$g['accounts']['QA-CLIENTES'],'debit_minor'=>1,'credit_minor'=>0]);}catch(Illuminate\Database\QueryException $e){$caught=$e->getCode()==='23514';}$ok($caught,'Fourth line outside authorised contract refused');}finally{DB::rollBack();}
|
|
$ok(DB::table('gl_lines')->count()===5,'Failed additions leave the reviewed lines intact');
|
|
// More events than journals: every event page must remain reachable and bounded.
|
|
for($n=0;$n<60;$n++)DB::table('gl_events')->insert($scope+['actor_id'=>$g['actors']['prepare'],'person'=>'qa-pagination','kind'=>'journal.prepared','entity_id'=>DB::table('gl_proposals')->where('obligation_id',$ob)->value('id'),'reason'=>'Explicit rollback-only history fixture.','previous'=>'{}','subsequent'=>'{}','created_at'=>now()]);
|
|
$data=$ledger->sales($enter('read'),$g['invoice_id'],2);$ok(count($data['book']['events'])===25&&$data['book']['evidence_pagination']['events']['last_page']>=3,'Long focused evidence is bounded and has further pages');
|
|
$html=view('accounting.sales-ledger',['data'=>$data])->render();$ok(str_contains($html,'page=3'),'Navigation reaches source evidence after last journal page');
|
|
DB::select('SELECT gl_assert_balanced(id) FROM gl_journals');
|
|
}finally{while(DB::transactionLevel()>0)DB::rollBack();}
|
|
$ok(hash('sha256',DB::table('si_invoices')->where('id',$g['invoice_id'])->value('snapshot'))===$before&&DB::table('permission_grants')->count()===$grants&&!DB::table('sr_effects')->exists()&&!DB::table('gl_journals')->exists(),'Invoice, rights and empty business state preserved after all probes');
|
|
file_put_contents($out,json_encode(['passed'=>true,'checks'=>$checks,'authorised_foreign_contexts_refused'=>['unit','company','tenant'],'revoked_rights_refused'=>true,'late_third_line_refused_by_database'=>true,'fourth_line_refused'=>true,'history_pagination_bounded'=>true,'rollback_only'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo "Sales integrity: $checks checks; invoice/rights preserved and all probes rolled back.\n";
|