Files
SGF/scripts/sgf-statements-boundaries.php
2026-11-11 09:00:17 +02:00

16 lines
7.1 KiB
PHP

<?php
require __DIR__.'/sgf-statements-proof-bootstrap.php';
$totals=new App\Modules\Accounting\InternalStatementTotals();$r=['currency'=>'MZN','opening_net_minor'=>'0','debit_minor'=>'100','credit_minor'=>'0','closing_net_minor'=>'-100'];$totals->add($r,'asset');$totals->add(array_replace($r,['closing_net_minor'=>'100']),'liability');$totals->add(array_replace($r,['debit_minor'=>'0','credit_minor'=>'500','closing_net_minor'=>'-500']),'income');$totals->add(array_replace($r,['debit_minor'=>'200','closing_net_minor'=>'200']),'expense');$totals->add(array_replace($r,['debit_minor'=>'0','closing_net_minor'=>'0']),'unclassified');$c=$totals->result()['MZN'];$ok($c['groups']['asset']['presentation_minor']==='-100'&&$c['groups']['liability']['presentation_minor']==='-100','inverted signs preserved without absolute value');$ok($c['groups']['asset']['inverted_accounts']===1&&$c['groups']['liability']['inverted_accounts']===1,'inverted balances flagged');$ok($c['known_result_subtotal_minor']==='300'&&$c['complete_result_minor']===null&&$c['equity_minor']===null,'period subtotal separate from complete result and capital');$totals->add(array_replace($r,['currency'=>'USD','debit_minor'=>'9007199254740993','closing_net_minor'=>'9007199254740993']),'asset');$ok($totals->result()['USD']['groups']['asset']['presentation_minor']==='9007199254740993','large exact amounts and currencies separate');$empty=new App\Modules\Accounting\InternalStatementTotals();$empty->add($r,'unclassified');$ok($empty->result()['MZN']['groups']['income']['presentation_minor']===null,'no classified income is not observed zero');
$scope=$f['scope'];$mapping=['id'=>123,'version'=>2,'data'=>['entries'=>[]]];foreach($fsEntries as$e){$a=(array)$native->table('gl_accounts')->where($scope)->find($e['account_id'],['id','code','label','currency']);$mapping['data']['entries'][]=$e+['account'=>$a,'account_hash'=>hash('sha256',Tempo\Finance\Payables\Payables::canonical($a))];}
$fake=function($size=128,$changed=false){return new class($size,$changed){public array $calls=[];public function __construct(private int $size,private bool $changed){}public function trialBalance($q){$this->calls[]=$q;$rows=[];$start=($q['page']-1)*$q['per_page'];for($i=$start;$i<min($this->size,$start+$q['per_page']);$i++)$rows[]=['account_id'=>900000+$i,'code'=>sprintf('UNIT-READ-%04d',$i),'label'=>'Double de leitura apenas; não conta ou lançamento criado','currency'=>'MZN','opening_net_minor'=>0,'debit_minor'=>$i%2?0:100,'credit_minor'=>$i%2?100:0,'closing_net_minor'=>$i%2?-100:100];return['rows'=>$rows,'basis'=>['scope'=>['tenant_id'=>1,'company_id'=>2,'unit_id'=>2],'through_on'=>$q['to_on'],'journals'=>$this->changed&&$q['page']>1?1:0,'last_journal_id'=>0],'totals'=>['accounts'=>$this->size,'opening_debit_minor'=>0,'opening_credit_minor'=>0,'debit_minor'=>6400,'credit_minor'=>6400,'closing_debit_minor'=>6400,'closing_credit_minor'=>6400],'pagination'=>['last_page'=>(int)ceil($this->size/$q['per_page'])]];}};};
Illuminate\Support\Facades\DB::setDefaultConnection('receipt_native');$capture=new ReflectionMethod($fs,'capture');$data=['from_on'=>'2026-11-10','to_on'=>'2026-11-10','per_page'=>25];$a=$fake();$p25=$capture->invoke($fs,$a,$scope,$mapping,$data);$b=$fake();$p100=$capture->invoke($fs,$b,$scope,$mapping,array_replace($data,['per_page'=>100]));$ok(count($a->calls)===6&&count($b->calls)===2,'capture consumes bounded25/100 pages from report port');$ok($p25===$p100&&$p25['summary']['accounts']===128,'all-page groups/hash independent of page size');$ok($p25['summary']['currencies']['MZN']['unclassified_accounts']===128,'unknown synthetic read accounts never classified automatically');$fsReject(fn()=>$capture->invoke($fs,$fake(changed:true),$scope,$mapping,$data),409,'changed source marker between pages refused');$bad=$mapping;$bad['data']['entries'][0]['account_hash']=str_repeat('0',64);$fsReject(fn()=>$capture->invoke($fs,$fake(),$scope,$bad,$data),409,'changed account fingerprint refused');
Illuminate\Support\Facades\DB::setDefaultConnection('pgsql');$native->beginTransaction();$host->beginTransaction();try{
$bm=(int)$native->table('fs_versions')->where('kind','mapping')->max('version');[$m]=$fsCommand(15,$fsInput('mapping','prepare',$bm,$fsMapping));$review=$fsInput('mapping','review',$bm+1,['decision'=>'approved']);$cut=$fsCall(16,'preview',$review);$nativeId=(int)$native->table('fs_versions')->find($m['id'])->prepared_native_id;
$native->beginTransaction();try{$native->table('pa_access')->where($scope+['user_id'=>$nativeId,'action'=>'general_ledger.view'])->update(['person'=>'QA-CHANGED-IDENTITY']);$fsReject(fn()=>$fsCall(16,'confirm',['preview'=>$cut['value']['preview']]),409,'preparer human identity change refused');}finally{$native->rollBack();}
$host->beginTransaction();try{$host->table('users')->where('id',15)->update(['name'=>'QA altered identity only in savepoint']);$fsReject(fn()=>$fsCall(16,'confirm',['preview'=>$cut['value']['preview']]),409,'preparer host identity change refused');}finally{$host->rollBack();}
$host->beginTransaction();try{$host->table('users')->where('id',16)->update(['name'=>'QA reviewer changed only in savepoint']);$fsReject(fn()=>$fsCall(16,'confirm',['preview'=>$cut['value']['preview']]),409,'reviewer host identity change requires new preview');}finally{$host->rollBack();}
$beforeVersions=$native->table('fs_versions')->count();$beforeAudits=$native->table('audit_logs')->count();$native->unprepared("CREATE FUNCTION fs_test_fail() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN RAISE EXCEPTION 'QA_TEST_FAILURE'; END; \$\$; CREATE TRIGGER fs_test_failure BEFORE INSERT ON fs_commands FOR EACH ROW EXECUTE FUNCTION fs_test_fail();");$failed=false;try{$fsCall(16,'confirm',['preview'=>$cut['value']['preview']]);}catch(Illuminate\Database\QueryException){$failed=true;}$ok($failed&&$native->table('fs_versions')->count()===$beforeVersions&&$native->table('audit_logs')->count()===$beforeAudits,'failed receipt rolls back version and audit');$native->unprepared('DROP TRIGGER fs_test_failure ON fs_commands; DROP FUNCTION fs_test_fail();');$recovered=$fsCall(16,'confirm',['preview'=>$cut['value']['preview']]);$ok($recovered['status']===201,'same confirmation recovers after failure');$again=$fsCall(16,'confirm',['preview'=>$cut['value']['preview']]);$ok($again['status']===200&&$again['value']['replayed'],'recovered receipt unique');
$immutable=false;$native->beginTransaction();try{$native->table('fs_versions')->where('id',$m['id'])->update(['reason'=>'Tentativa de alteração de história']);}catch(Illuminate\Database\QueryException$e){$immutable=str_contains($e->getMessage(),'SGF_INTERNAL_STATEMENTS_APPEND_ONLY');}finally{$native->rollBack();}$ok($immutable,'database refuses version mutation');
}finally{while($native->transactionLevel()>0)$native->rollBack();while($host->transactionLevel()>0)$host->rollBack();}
file_put_contents($work.'/statements-boundaries.json',json_encode(['passed'=>true,'checks'=>$checks,'report_port_double_rows'=>128,'synthetic_accounts_or_journals_inserted'=>0,'fixtures_rolled_back'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/statements-boundaries.json']).PHP_EOL;