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

94 lines
13 KiB
PHP

<?php
use App\Models\User;
use App\Modules\Accounting\GeneralLedger;
use App\Modules\Purchasing\SupplierPayables;
use App\Shared\Context\CompanyContext;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tempo\Finance\GeneralLedger\{LedgerSchema,PayableJournal};
use Tempo\Finance\Payables\Payables;
$f=json_decode(file_get_contents($s['root'].'/fixture.json'),true,64,JSON_THROW_ON_ERROR);$scope=$f['scope'];
config(['general_ledger.synthetic_enabled'=>true,'modules.contracted'=>['purchasing','accounting'],'modules.companies.'.$scope['tenant_id'].':'.$scope['company_id']=>['contracted'=>['purchasing','accounting'],'enabled'=>['purchasing','accounting']]]);
$enter=function(string $role)use($f,$scope):User{$u=User::findOrFail($f['actors'][$role]);auth()->setUser($u);app(CompanyContext::class)->selectFor($u->id,$scope['tenant_id'],$scope['company_id'],$scope['unit_id']);return $u;};
$ledger=app(GeneralLedger::class);
$command=fn(string $role,string $action,array $body,?string $key=null,?Closure $fault=null)=>$ledger->engine($enter($role),$fault)->command($action,$body,$key??(string)Str::uuid());
$financialFingerprint=function():string{$h=hash_init('sha256');foreach(['pa_imported_sources','pa_source_histories','pa_sources','pa_proposals','pa_obligations','pa_history_imports','pa_imported_payments','pa_events','pa_commands','pa_access','permission_grants','company_memberships'] as $table){hash_update($h,$table."\n");$q=DB::table($table);if($table==='company_memberships')$q->orderBy('tenant_id')->orderBy('company_id')->orderBy('user_id');else $q->orderBy('id');foreach($q->cursor() as $r)hash_update($h,Payables::canonical((array)$r)."\n");}return hash_final($h);};
if($phase==='ledger-setup-recover'){
LedgerSchema::assertSynthetic(DB::connection());
foreach(LedgerSchema::TABLES as $table)if(!DB::getSchemaBuilder()->hasTable($table)||DB::table($table)->count()!==(in_array($table,['gl_accounts'],true)?2:(in_array($table,['gl_periods','gl_rules'],true)?1:0)))throw new LogicException('Unexpected setup state; no repair performed');
if(DB::table('permission_grants')->where('permission','like','general_ledger.%')->count()!==6||DB::table('pa_obligations')->count()!==1)throw new LogicException('Unexpected isolated grants/source');
$local=DB::table('pa_imported_sources')->whereRaw("payload->>'number' = ?",['QA-LEDGER-SOURCE-60'])->value('id');$obligation=DB::table('pa_obligations')->first();
$g=['obligation_id'=>$obligation->id,'local_source_id'=>$local,'debit_account'=>DB::table('gl_accounts')->where('code','QA-CONTRAPARTIDA')->value('id'),'credit_account'=>DB::table('gl_accounts')->where('code','QA-FORNECEDOR')->value('id'),'body'=>['obligation_id'=>$obligation->id,'journal_on'=>'2026-10-29','period_key'=>'QA-2026-10','rule_key'=>'QA-PAYABLE-JOURNAL','reason'=>'Individual synthetic journal preparation.'],'financial_fingerprint'=>$financialFingerprint()];
file_put_contents($s['root'].'/ledger-fixture.json',json_encode($g,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo "Existing fresh QA setup checked read-only; fixture manifest recovered without repeating schema, grants or recognition.\n";exit;
}
if(in_array($phase,['ledger-setup','ledger-flow-setup','ledger-cycle-setup'],true)){
LedgerSchema::install(DB::connection());
DB::statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check');
DB::statement("ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK ((unit_id IS NULL AND permission='company.view') OR (unit_id IS NOT NULL AND permission IN ('unit.view','supplier_payables.view','general_ledger.view','general_ledger.prepare','general_ledger.validate','general_ledger.post')))");
foreach(['prepare'=>['view','prepare','post'],'validate'=>['view','validate'],'read'=>['view']] as $role=>$actions)foreach($actions as $action)DB::table('permission_grants')->insert($scope+['user_id'=>$f['actors'][$role],'permission'=>'general_ledger.'.$action,'created_at'=>now(),'updated_at'=>now()]);
$debit=DB::table('gl_accounts')->insertGetId($scope+['code'=>'QA-CONTRAPARTIDA','label'=>'Contrapartida artificial de ensaio','is_movement'=>true,'currency'=>'MZN']);
$credit=DB::table('gl_accounts')->insertGetId($scope+['code'=>'QA-FORNECEDOR','label'=>'Fornecedor artificial de ensaio','is_movement'=>true,'currency'=>'MZN']);
$common=['created_by'=>$f['actors']['prepare'],'reason'=>'Explicitly authorised artificial QA configuration.','created_at'=>now()];
DB::table('gl_periods')->insert($scope+$common+['period_key'=>'QA-2026-10','version'=>1,'starts_on'=>'2026-10-01','ends_on'=>'2026-10-31','status'=>'open']);
DB::table('gl_rules')->insert($scope+$common+['rule_key'=>'QA-PAYABLE-JOURNAL','version'=>1,'policy'=>PayableJournal::POLICY,'effect_type'=>'supplier_payable.recognised','currency'=>'MZN','valid_from'=>'2026-10-01','valid_to'=>'2026-10-31','debit_account_id'=>$debit,'credit_account_id'=>$credit]);
$finance=app(SupplierPayables::class);$doc=$finance->document($enter('prepare'),$f['source_id']);unset($doc['local_id']);$doc['document']=270001;$doc['number']='QA-LEDGER-SOURCE-60';$doc['payment_count']=0;$payload=Payables::canonical($doc);
$local=DB::table('pa_imported_sources')->insertGetId($scope+['payload'=>$payload,'receipt_hash'=>hash('sha256',$payload)]);
$o=['obligation_id'=>null];
if(in_array($phase,['ledger-setup','ledger-cycle-setup'],true)){
$p=$finance->engine($enter('prepare'))->command('prepare',$local,'Recognised source for the first ledger QA scenario.',(string)Str::uuid());
$o=$finance->engine($enter('validate'))->command('validate',$p['proposal_id'],'Independent recognition of explicit synthetic source.',(string)Str::uuid());
}
$g=['integrated_flow'=>$phase==='ledger-flow-setup','obligation_id'=>$o['obligation_id'],'local_source_id'=>$local,'debit_account'=>$debit,'credit_account'=>$credit,'body'=>['obligation_id'=>$o['obligation_id'],'journal_on'=>'2026-10-29','period_key'=>'QA-2026-10','rule_key'=>'QA-PAYABLE-JOURNAL','reason'=>'Individual synthetic journal preparation.'],'financial_fingerprint'=>$financialFingerprint()];
if($phase==='ledger-cycle-setup'){require __DIR__.'/ledger-cycle-setup.php';$g['cycle_enabled']=true;}
file_put_contents($s['root'].'/ledger-fixture.json',json_encode($g,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo "Ledger QA installed in new database: explicit 60 MZN source, two artificial accounts, period and six explicit grants covering four rights. No journal yet.\n";exit;
}
$g=json_decode(file_get_contents($s['root'].'/ledger-fixture.json'),true,64,JSON_THROW_ON_ERROR);
if($phase==='ledger-reports-isolation'){require __DIR__.'/ledger-reports-isolation.php';exit;}
if(in_array($phase,['ledger-reports-seed','ledger-reports-cases'],true)){require __DIR__.'/ledger-reports-proof.php';exit;}
if($g['cycle_enabled']??false)config(['general_ledger.cycle_enabled'=>true]);
if(in_array($phase,['ledger-cycle-cases','ledger-cycle-workflow','ledger-cycle-race-prepare'],true)){require __DIR__.'/ledger-cycle-cases.php';exit;}
if(in_array($phase,['ledger-cycle-race','ledger-cycle-worker'],true)){require __DIR__.'/ledger-cycle-race.php';exit;}
if($phase==='ledger-cycle-security'){require __DIR__.'/ledger-cycle-security.php';exit;}
if($phase==='ledger-cycle-final'){require __DIR__.'/ledger-cycle-final.php';exit;}
if($phase==='ledger-flow-cases'){require __DIR__.'/ledger-flow-cases.php';exit;}
if($phase==='ledger-flow-focus-cases'){require __DIR__.'/ledger-flow-focus-cases.php';exit;}
if($phase==='ledger-cases'){require __DIR__.'/ledger-cases.php';exit;}
if(in_array($phase,['ledger-race','ledger-worker'],true)){require __DIR__.'/ledger-race.php';exit;}
if($phase==='ledger-http'){require __DIR__.'/ledger-http-entry.php';exit;}
if($phase==='ledger-ui-state'){
$data=$ledger->listing($enter('prepare'));$html=view('accounting.general-ledger',['data'=>$data])->render();
if(count($data['proposals'])!==1||!$data['proposals'][0]->journal_id||!str_contains($html,'Aplicada uma vez no lançamento #')||str_contains($html,'data-action="post"'))throw new LogicException('Posted proposal must show its receipt without another apply form');
file_put_contents($s['root'].'/ledger-ui-state.json',json_encode(['passed'=>true,'posted_proposal_has_journal_reference'=>true,'no_duplicate_apply_form'=>true],JSON_PRETTY_PRINT));echo "Posted proposal links its immutable journal and offers no second apply form.\n";exit;
}
if($phase==='ledger-demo-state'){
DB::beginTransaction();DB::statement('SET TRANSACTION READ ONLY');try{
if($financialFingerprint()!==$g['financial_fingerprint']||DB::table('gl_journals')->count()||DB::table('gl_proposals')->count())throw new LogicException('Team fixture must be pristine and separate from regression');
file_put_contents($s['root'].'/ledger-demo-state.json',json_encode(['passed'=>true,'read_only'=>true,'financial_fingerprint_equal'=>true,'journals'=>0,'proposals'=>0,'ready_for_team_workflow'=>true,'qa_grants'=>DB::table('permission_grants')->where('permission','like','general_ledger.%')->count()],JSON_PRETTY_PRINT));
}finally{DB::rollBack();}echo "Independent team fixture retains one recognised source and no proposals/journals.\n";exit;
}
if($phase==='ledger-integrity'){
$data=$ledger->listing($enter('prepare'),['period_key'=>'QA-2026-10']);$html=view('accounting.general-ledger',['data'=>$data])->render();
if(!str_contains($html,'Fotografia imutável da origem, regra e período aplicados')||str_contains($html,'data-action="post"')||$data['totals']!==['debit_minor'=>6000,'credit_minor'=>6000,'net_minor'=>0])throw new LogicException('Final interface or totals differ');
DB::beginTransaction();DB::statement('SET TRANSACTION READ ONLY');try{
$definition=DB::selectOne("SELECT pg_get_functiondef('gl_assert_balanced(bigint)'::regprocedure) AS definition")->definition;
$trigger=DB::selectOne("SELECT tgdeferrable,tginitdeferred FROM pg_trigger WHERE tgname='gl_balanced_journal' AND tgrelid='gl_journals'::regclass");
if(!$trigger->tgdeferrable||!$trigger->tginitdeferred||!str_contains($definition,'GL_REVIEW_OR_SOURCE_MISMATCH')||$financialFingerprint()!==$g['financial_fingerprint'])throw new LogicException('Final schema/source proof differs');
$names=DB::getSchemaBuilder()->getTableListing();$notExercised=['stock'=>!array_filter($names,fn($n)=>str_contains($n,'stock_movements')),'budget'=>!array_filter($names,fn($n)=>str_contains($n,'budget_entries'))];
file_put_contents($s['root'].'/ledger-integrity.json',json_encode(['passed'=>true,'balance_function_sha256'=>hash('sha256',$definition),'deferred_balance_constraint'=>true,'review_source_account_checks_in_database'=>true,'final_blade_rendered'=>true,'financial_fingerprint_equal'=>true,'absent_domain_tables_not_exercised'=>$notExercised],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
}finally{DB::rollBack();}echo "Final schema balance/review guards, immutable source display and original financial fingerprint verified.\n";exit;
}
if($phase==='ledger-http-input'){
$race=json_decode(file_get_contents($s['root'].'/ledger-race.json'),true,64,JSON_THROW_ON_ERROR);
$prepareKey=DB::table('gl_commands')->where('actor_id',$f['actors']['prepare'])->where('result->state','prepared')->value('command_key');$reviewKey=DB::table('gl_commands')->where('actor_id',$f['actors']['validate'])->where('result->state','approved')->value('command_key');
file_put_contents($s['root'].'/ledger-http-input.json',json_encode(['prepare'=>['key'=>$prepareKey,'body'=>$g['body']],'validate'=>['key'=>$reviewKey,'body'=>['proposal_id'=>$race['proposal_id'],'revision'=>$race['revision'],'decision'=>'approved','reason'=>'Independent exact version for concurrency.']],'post'=>['key'=>$race['post_key'],'body'=>$race['post_body']],'journal_id'=>$race['journal_id']],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo "Private recovery inputs prepared from existing receipts.\n";exit;
}
if($phase==='ledger-post'){
DB::beginTransaction();DB::statement('SET TRANSACTION READ ONLY');try{
if($financialFingerprint()!==$g['financial_fingerprint']||DB::table('gl_journals')->count()!==1||DB::table('gl_lines')->count()!==2||DB::table('gl_reviews')->where('decision','approved')->count()!==1||DB::table('gl_events')->where('kind','journal.posted')->count()!==1)throw new LogicException('Ledger preservation or single effect differs');
DB::select('SELECT gl_assert_balanced(id) FROM gl_journals');
$j=DB::table('gl_journals')->first();file_put_contents($s['root'].'/ledger-post.json',json_encode(['passed'=>true,'read_only'=>true,'financial_fingerprint_equal'=>true,'financial_fingerprint'=>$g['financial_fingerprint'],'journals'=>1,'lines'=>2,'debit_minor'=>(int)$j->debit_minor,'credit_minor'=>(int)$j->credit_minor,'posting_events'=>1,'synthetic_rights_only'=>true,'default_flag'=>(require $c['root'].'/config/general_ledger.php')['synthetic_enabled']],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
}finally{DB::rollBack();}echo "Final ledger: one balanced journal, two lines, one posting event; financial source and rights unchanged.\n";exit;
}
throw new LogicException('Unknown ledger phase');