commit melhorias1

This commit is contained in:
2026-11-11 15:04:01 +02:00
parent 1d11a5e57b
commit cd9897df05
145 changed files with 10950 additions and 84 deletions
+2
View File
@@ -0,0 +1,2 @@
<?php
require __DIR__.'/sgf-single-bootstrap.php';use App\Modules\Accounting\AssetDepreciation as A;$checks=[];$ok=function($v,$n)use(&$checks){if(!$v)throw new LogicException($n);$checks[]=$n;};$reject=function($fn,$n)use($ok){try{$fn();throw new LogicException('Expected rejection');}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface$e){$ok($e->getStatusCode()===422,$n);}};$a=A::quota('1200.00','0.00',12,1);$ok($a['quota_cents']==='10000'&&$a['carrying_cents']==='110000','proposed1200/12 first quota100 carrying1100');$b=A::quota('1200.00','0.00',12,12);$ok($b['accumulated_cents']==='120000'&&$b['carrying_cents']==='0','last month exhausts depreciable amount exactly');$one=A::quota('100.00','0.00',3,1);$last=A::quota('100.00','0.00',3,3);$ok($one['quota_cents']==='3333'&&$one['difference_cents']==='1/3'&&$last['quota_cents']==='3334'&&$last['difference_cents']==='-2/3','cent adjustment and signed rational differences preserved');$sum=0;for($i=1;$i<=3;$i++)$sum+=(int)A::quota('100.00','0.00',3,$i)['quota_cents'];$ok($sum===10000,'sum of monthly quotas equals basis');$ok(A::quota('1200.00','200.00',12,12)['carrying_cents']==='20000','residual value preserved');$ok(A::quota('1200.00','1200.00',12,1)['quota_cents']==='0','explicit zero depreciable basis not unknown');foreach([[null,'0.00',12],['1200.00',null,12],['1200.00','0.00',null]]as[$c,$r,$m]){$q=A::quota($c,$r,$m,1);$ok(!$q['complete']&&$q['quota_cents']===null,'missing input remains unknown '.count($checks));}$reject(fn()=>A::quota('100.00','101.00',12,1),'residual greater than cost refused');$reject(fn()=>A::quota('1e3','0.00',12,1),'non documentary amount refused');$reject(fn()=>A::quota('100.00','0.00',12,13),'month beyond declared life refused');$ok($a['legal_tax_treatment']===null,'no legal depreciation rate or tax deduction asserted');file_put_contents($work.'/asset-depreciation-core.json',json_encode(['passed'=>true,'checks'=>$checks,'pure_arithmetic_only'=>true,'new_assets'=>0,'new_grants'=>0,'new_journals'=>0,'installed'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/asset-depreciation-core.json']).PHP_EOL;
+2
View File
@@ -0,0 +1,2 @@
<?php
$book=$argv[1]??'8085';require __DIR__.'/sgf-single-bootstrap.php';$rights=['fixed_assets.view','fixed_assets.prepare','fixed_assets.review'];$rows=$host->table('permission_grants')->whereIn('permission',$rights)->get(['permission','user_id','tenant_id','company_id','unit_id'])->all();if($rows!==[])throw new LogicException('Reconcile existing asset rights');$old=$host->table('permission_grants')->where(['tenant_id'=>1,'company_id'=>2,'unit_id'=>2])->whereIn('user_id',[15,16,17])->where('permission','like','general_ledger.%')->orderBy('user_id')->orderBy('permission')->get(['user_id','permission'])->all();file_put_contents($work.'/assets-access-readonly.json',json_encode(['passed'=>true,'book'=>$book,'proposed_rights'=>$rights,'existing_asset_grants'=>$rows,'existing_ledger_grants'=>$old,'new_grants'=>0,'database_writes'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'book'=>$book,'missing_asset_rights'=>3,'proof'=>$work.'/assets-access-readonly.json']).PHP_EOL;
+2
View File
@@ -0,0 +1,2 @@
<?php
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$prior=json_decode(file_get_contents($s['root'].'/single-integration/20261111-095619-e3fff98c/hp-readable-installation.json'),true);$qualified=array_unique(array_merge($prior['identical_original_tables'],array_map(fn($t)=>'native.'.$t,['gl_accounts','gl_rules','gl_proposals','gl_reviews','gl_journals','gl_lines','gl_commands','gl_events','gl_corrections','gl_correction_results','gl_period_checks','gl_period_reviews','gl_period_closures'])));$before=[];$order=fn($t)=>in_array($t,['sr_outbox','sr_deliveries'],true)?'event_id':(in_array($t,['sc_catalogue_meta','pd_due_meta','fo_meta','fs_meta','rm_meta','hp_meta'],true)?'policy':(in_array($t,['hr_qa_profiles','sgf_presentation_profiles'],true)?'profile_key':(in_array($t,['fs_results','fs_result_lines'],true)?'snapshot_key':($t==='company_memberships'?'user_id':'id'))));foreach($qualified as$k){[$n,$t]=explode('.',$k);$db=$n==='host'?$host:$native;$before[$n][$t]=$db->table($t)->orderBy($order($t))->when($t==='fs_result_lines',fn($q)=>$q->orderBy('account_id'))->get()->toJson();}foreach(['binding.json','single-bindings.json','stock-binding.json']as$f)$before['files'][$f]=hash_file('sha256',$s['root'].'/'.$f);file_put_contents($work.'/preservation-private.json',json_encode($before));$files=['packages/general-ledger/bootstrap.php','packages/general-ledger/src/PayableJournal.php','packages/general-ledger/src/LedgerCycle.php','app/Modules/Accounting/ComposedLedger.php'];foreach($files as$f){$to=$work.'/before/'.$f;if(!is_dir(dirname($to)))mkdir(dirname($to),0700,true);copy($repo.'/'.$f,$to);}file_put_contents($work.'/gl-balance-function-private.sql',$native->selectOne("SELECT pg_get_functiondef('gl_assert_balanced(bigint)'::regprocedure) definition")->definition);file_put_contents($s['root'].'/assets-backup-pointer.json',json_encode(['root'=>$work]));echo json_encode(['passed'=>true,'backup'=>$work,'original_tables'=>count($qualified),'database_writes'=>0]).PHP_EOL;
+19
View File
@@ -0,0 +1,19 @@
<?php
require __DIR__.'/sgf-assets-proof-bootstrap.php';$cls=App\Modules\Accounting\FixedAssets::class;$base=['operation'=>'establish','code'=>'QA-BOUNDARY-ASSET','description'=>'Equipamento fictício de fronteiras','origin_reference'=>'QA-BOUNDARY-OPENING','cost'=>'100.00','residual'=>'0.00','available_on'=>'2026-10-01','location'=>'QA-SALA-A','custodian'=>'QA-CUSTODIA-A','evidence'=>'Dados sintéticos de fronteiras revertidos.'];
$host->beginTransaction();try{
$key=(string)Str::uuid();$in=$faInput('asset',$key,0,$base);$op=(string)Str::uuid();$cut=$faCall(15,'preview',$in,$op)['value'];$ok($host->table('fa_versions')->where('record_key',$key)->count()===0,'preview has no administrative write');$reject(fn()=>$faCall(16,'confirm',['preview'=>$cut['preview']]),403,'preview tied to person');$reject(fn()=>$faCall(15,'confirm',['preview'=>$cut['preview']],null,'other-generation'),409,'preview tied to context generation');$reject(fn()=>$faCall(15,'confirm',['preview'=>'tampered']),409,'tampered preview denied');$out=$faCall(15,'confirm',['preview'=>$cut['preview']])['value'];$again=$faCall(15,'confirm',['preview'=>$cut['preview']])['value'];$ok($again['replayed']&&$again['id']===$out['id']&&$host->table('fa_versions')->where('record_key',$key)->count()===1,'same confirmation one version and receipt');$receipt=$faCall(15,'receipt',[],$op)['value'];$ok($receipt['historical_receipt_only']&&$receipt['result']['id']===$out['id'],'receipt recovers historical decision without execution');$reject(fn()=>$faCall(16,'receipt',[],$op),404,'receipt isolated by actor');$reject(fn()=>$faCall(15,'preview',$in),409,'outdated version denied');
$review=$faInput('asset',$key,1,['decision'=>'approved'],'review');$cut=$faCall(16,'preview',$review)['value'];$host->table('permission_grants')->where($f['scope']+['user_id'=>15,'permission'=>'fixed_assets.prepare'])->delete();$reject(fn()=>$faCall(16,'confirm',['preview'=>$cut['preview']]),403,'preparer rights revalidated at review');
}finally{$host->rollBack();}
$host->beginTransaction();try{
$key=(string)Str::uuid();$in=$faInput('asset',$key,0,$base);$cut=$faCall(15,'preview',$in)['value'];$host->unprepared("CREATE FUNCTION public.fa_test_failure() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN RAISE EXCEPTION 'QA_ASSET_RECEIPT_FAILURE'; END \$\$; CREATE TRIGGER fa_test_failure BEFORE INSERT ON fa_commands FOR EACH ROW EXECUTE FUNCTION fa_test_failure()");$failed=false;try{$faCall(15,'confirm',['preview'=>$cut['preview']]);}catch(Illuminate\Database\QueryException$e){$failed=str_contains($e->getMessage(),'QA_ASSET_RECEIPT_FAILURE');}$ok($failed&&$host->table('fa_versions')->where('record_key',$key)->count()===0,'failure between version and receipt rolls back version and audit');$host->statement('DROP TRIGGER fa_test_failure ON fa_commands');$out=$faCall(15,'confirm',['preview'=>$cut['preview']]);$ok($out['status']===200&&$host->table('fa_versions')->where('record_key',$key)->count()===1,'same confirmation recovers after injected failure');
[$ar]=$faCommand(16,$faInput('asset',$key,1,['decision'=>'approved'],'review'));$planKey=(string)Str::uuid();$plan=['operation'=>'establish','asset_key'=>$key,'life_months'=>null,'status'=>'active','evidence'=>'Vida desconhecida explicitamente na regressão.'];$faCommand(15,$faInput('plan',$planKey,0,$plan));$faCommand(16,$faInput('plan',$planKey,1,['decision'=>'approved'],'review'));$chargeKey=(string)Str::uuid();$charge=['operation'=>'establish','asset_key'=>$key,'plan_key'=>$planKey,'month'=>'2026-10','evidence'=>'Quota indeterminada não pode ser aprovada.'];[$cp]=$faCommand(15,$faInput('charge',$chargeKey,0,$charge));$ok($cp['impact']['after']['proposal']['calculation']['quota_cents']===null,'unknown life remains unknown not zero');$reject(fn()=>$faCall(16,'preview',$faInput('charge',$chargeKey,1,['decision'=>'approved'],'review')),409,'unknown quota approval denied');
$life=$plan;$life['operation']='amend';$life['life_months']=12;$faCommand(15,$faInput('plan',$planKey,2,$life));$faCommand(16,$faInput('plan',$planKey,3,['decision'=>'approved'],'review'));$reject(fn()=>$faCall(16,'preview',$faInput('charge',$chargeKey,1,['decision'=>'approved'],'review')),409,'changed dependency demands new preparation');
$suspended=$life;$suspended['status']='suspended';$faCommand(15,$faInput('plan',$planKey,4,$suspended));$faCommand(16,$faInput('plan',$planKey,5,['decision'=>'approved'],'review'));$reject(fn()=>$faCall(15,'preview',$faInput('charge',(string)Str::uuid(),0,$charge)),409,'suspended plan denies quota');$faCommand(15,$faInput('plan',$planKey,6,$life));$faCommand(16,$faInput('plan',$planKey,7,['decision'=>'approved'],'review'));$ok(true,'resume plan separately reviewed');
$amend=$base;$amend['operation']='amend';$amend['location']='QA-SALA-B';$amend['custodian']='QA-CUSTODIA-B';$faCommand(15,$faInput('asset',$key,2,$amend));$x=$faCommand(16,$faInput('asset',$key,3,['decision'=>'approved'],'review'))[0];$ok($x['impact']['after']['proposal']['input']['location']==='QA-SALA-B'&&$x['impact']['journals']===0,'custody change reviewed without stock or GL effect');
for($n=0;$n<27;$n++){$v=$base;$v['code']='QA-PAGE-'.$n;$v['origin_reference']='QA-PAGE-OPEN-'.$n;$faCommand(15,$faInput('asset',(string)Str::uuid(),0,$v));}
$one=$faCall(17,'index',['kind'=>'asset','search'=>'QA-PAGE-','per_page'=>25,'page'=>1])['value']['records'];$two=$faCall(17,'index',['kind'=>'asset','search'=>'QA-PAGE-','per_page'=>25,'page'=>2])['value']['records'];$all=$faCall(17,'index',['kind'=>'asset','search'=>'QA-PAGE-','per_page'=>100])['value']['records'];$ok(count($one['data'])===25&&count($two['data'])===2&&$all['total']===27,'SQL pagination25 and100 filters same27 fixtures');$ok(!array_intersect(array_column($one['data'],'id'),array_column($two['data'],'id')),'no overlap between pages');$literal=$faCall(17,'index',['kind'=>'asset','search'=>'%','per_page'=>25])['value']['records'];$ok($literal['total']===0,'wildcard search treated literally');
$foreign=$base;$foreign['code']='QA-FOREIGN-ASSET';$foreign['origin_reference']='QA-FOREIGN-OPEN';$foreignKey=(string)Str::uuid();$faCommand(15,$faInput('asset',$foreignKey,0,$foreign));$row=$host->table('fa_versions')->where('record_key',$foreignKey)->first();$host->unprepared('ALTER TABLE fa_versions DISABLE TRIGGER fa_versions_immutable');$host->table('fa_versions')->where('id',$row->id)->update(['unit_id'=>1]);$host->unprepared('ALTER TABLE fa_versions ENABLE TRIGGER fa_versions_immutable');$reject(fn()=>$faCall(17,'index',['kind'=>'asset','record_key'=>$foreignKey]),403,'foreign unit record denied even known UUID');
}finally{$host->rollBack();}
$native->beginTransaction();try{$before=$native->table('gl_journals')->count();$native->table('gl_periods')->insert($f['scope']+['period_key'=>'QA-2026-INTEGRADO','version'=>2,'starts_on'=>'2026-01-01','ends_on'=>'2026-12-31','status'=>'closed','created_by'=>15,'reason'=>'Fecho artificial revertido para guarda de período.','created_at'=>now()->utc()]);$p=json_decode(file_get_contents($s['root'].'/assets-regression-progress-private.json'),true);$reject(fn()=>$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>'assets.admitted','source_id'=>$p['steps']['asset_review']['id'],'journal_on'=>'2026-10-01','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'QA-ASSET-ADMISSION','reason'=>'Período fechado deve recusar sem efeitos.'],(string)Str::uuid())),409,'closed period refuses current source');$ok($native->table('gl_journals')->count()===$before,'closed period refusal creates no journal');}finally{$native->rollBack();}
file_put_contents($work.'/assets-boundaries.json',json_encode(['passed'=>true,'checks'=>$checks,'fixtures_rolled_back'=>true,'cache_cleared'=>false,'new_demo'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/assets-boundaries.json']).PHP_EOL;
+2
View File
@@ -0,0 +1,2 @@
import fs from 'node:fs';import assert from 'node:assert/strict';import {spawn} from 'node:child_process';
const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-b4093c6f6707',directory=root+'/assets-concurrency-'+Date.now();fs.mkdirSync(directory);const start=mode=>{const child=spawn('C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',['scripts/sgf-assets-concurrency.php',mode,directory],{windowsHide:true});let output='';child.stdout.on('data',v=>output+=v);child.stderr.on('data',v=>output+=v);return{child,done:new Promise(resolve=>child.on('close',code=>resolve({code,output})))};};let result=await start('prepare').done;assert.equal(result.code,0,result.output);const first=start('first');try{const end=Date.now()+8000;while(!fs.existsSync(directory+'/locked.json')&&Date.now()<end)await new Promise(r=>setTimeout(r,30));assert(fs.existsSync(directory+'/locked.json'));const waiting=start('second');await new Promise(r=>setTimeout(r,900));const inspected=await start('inspect').done;assert.equal(inspected.code,0,inspected.output);const second=await waiting.done;assert.equal(second.code,0,second.output);const busy=JSON.parse(fs.readFileSync(directory+'/second.json'));assert.equal(busy.status,503);fs.writeFileSync(directory+'/release','1');result=await first.done;assert.equal(result.code,0,result.output);result=await start('recover').done;assert.equal(result.code,0,result.output);const recovery=JSON.parse(fs.readFileSync(directory+'/recovery.json'));assert(recovery.passed,result.output);const blocking=JSON.parse(fs.readFileSync(directory+'/blocking.json'));assert(blocking.waiters.some(w=>w.wait_event_type==='Lock'));const proof=directory+'/result.json';fs.writeFileSync(proof,JSON.stringify({...recovery,during_lock:503,seconds_waiting:busy.seconds,blocking},null,2));console.log(JSON.stringify({passed:true,proof}));}finally{fs.writeFileSync(directory+'/release','1');await first.done;}
+8
View File
@@ -0,0 +1,8 @@
<?php
require __DIR__.'/sgf-assets-proof-bootstrap.php';$mode=$argv[1]??'';$directory=$argv[2]??'';if($book!=='regression'||!str_starts_with(str_replace('\\','/',$directory),str_replace('\\','/',$s['root']).'/assets-concurrency-'))throw new LogicException('Regression only');
$http=function($token)use($faCall){try{return$faCall(16,'confirm',['preview'=>$token]);}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface$e){return['status'=>$e->getStatusCode()];}};
if($mode==='prepare'){$key=(string)Str::uuid();$a=['operation'=>'establish','code'=>'QA-CONCURRENT-'.strtoupper(substr($key,0,8)),'description'=>'Activo artificial de concurrência sem lançamento','origin_reference'=>'QA-CONCURRENT-OPEN-'.strtoupper(substr($key,0,8)),'cost'=>'1200.00','residual'=>'0.00','available_on'=>'2026-10-01','location'=>'QA-SALA-A','custodian'=>'QA-CUSTODIA-A','evidence'=>'Conferência sintética simultânea do mesmo cadastro.'];$faCommand(15,$faInput('asset',$key,0,$a));$first=$faCall(16,'preview',$faInput('asset',$key,1,['decision'=>'approved'],'review'))['value']['preview'];$second=$faCall(16,'preview',$faInput('asset',$key,1,['decision'=>'refused'],'review'))['value']['preview'];file_put_contents($directory.'/cuts-private.json',json_encode(['first'=>$first,'second'=>$second,'record_key'=>$key]));exit;}
if($mode==='inspect'){$pid=json_decode(file_get_contents($directory.'/locked.json'),true)['host_pid'];$rows=$host->select('SELECT pid,wait_event_type,wait_event,pg_blocking_pids(pid) blockers FROM pg_stat_activity WHERE ? = ANY(pg_blocking_pids(pid))',[$pid]);file_put_contents($directory.'/blocking.json',json_encode(['blocker_host_pid'=>$pid,'waiters'=>$rows,'company_for_update'=>true]));exit;}$cuts=json_decode(file_get_contents($directory.'/cuts-private.json'),true);
if($mode==='first'){$host->beginTransaction();try{$out=$http($cuts['first']);if($out['status']!==200)throw new LogicException('First review failed');file_put_contents($directory.'/locked.json',json_encode(['host_pid'=>$host->selectOne('SELECT pg_backend_pid() pid')->pid]));$end=microtime(true)+25;while(!is_file($directory.'/release')&&microtime(true)<$end)usleep(50000);$host->commit();}finally{while($host->transactionLevel()>0)$host->rollBack();}exit;}
if($mode==='second'){$start=microtime(true);$out=$http($cuts['second']);file_put_contents($directory.'/second.json',json_encode(['status'=>$out['status'],'seconds'=>microtime(true)-$start]));exit;}
if($mode==='recover'){$second=$http($cuts['second']);$replay=$http($cuts['first']);$versions=$host->table('fa_versions')->where('record_key',$cuts['record_key'])->count();$commands=$host->table('fa_commands')->whereRaw("input->>'record_key'=?",[$cuts['record_key']])->count();$out=['passed'=>$second['status']===409&&$replay['status']===200&&$replay['value']['replayed']&&$versions===2&&$commands===2,'after_release'=>$second['status'],'original_replay'=>$replay['status'],'versions'=>$versions,'receipts'=>$commands,'regression_only'=>true];file_put_contents($directory.'/recovery.json',json_encode($out));echo json_encode($out).PHP_EOL;}
+18
View File
@@ -0,0 +1,18 @@
<?php
require __DIR__.'/sgf-assets-proof-bootstrap.php';$progress=$s['root'].'/assets-regression-progress-private.json';$p=json_decode(file_get_contents($progress),true);$save=function()use(&$p,$progress){file_put_contents($progress,json_encode($p));};$step=function($name,Closure $fn)use(&$p,$save,$ok){if(!isset($p['steps'][$name])){$p['steps'][$name]=$fn();$save();}$ok(true,$name.' recorded once');return$p['steps'][$name];};$cmd=function($name,$actor,$data)use($step,$faCommand){return$step($name,fn()=>$faCommand($actor,$data)[0]);};$gl=function($name,$role,$action,$data)use($step,$call){return$step($name,fn()=>$call($role,$action==='effect-prepare'?'prepare':str_replace('-','_',$action),fn($e)=>$e->command($action,$data,(string)Str::uuid())));};
$old=App\Modules\Accounting\FixedAssets::class;$plan=['operation'=>'correct_plan','asset_key'=>$p['asset'],'life_months'=>10,'status'=>'active','evidence'=>'Correcção técnica artificial do plano12 para10 explicitamente autorizada na regressão.'];
if(!isset($p['steps']['plan_correct_prepare'])){$bad=$plan;$bad['operation']='amend';$reject(fn()=>$faCall(15,'preview',$faInput('plan',$p['plan'],2,$bad)),409,'ordinary life change with posted quotas denied');}
$cmd('plan_correct_prepare',15,$faInput('plan',$p['plan'],2,$plan));$cmd('plan_correct_review',16,$faInput('plan',$p['plan'],3,['decision'=>'approved'],'review'));
$charge=['operation'=>'amend','asset_key'=>$p['asset'],'plan_key'=>$p['plan'],'month'=>'2026-10','evidence'=>'Nova quota120 calculada a partir do plano10 revisto na regressão.'];$cmd('charge_correct_prepare',15,$faInput('charge',$p['charge'],2,$charge));$cr=$cmd('charge_correct_review',16,$faInput('charge',$p['charge'],3,['decision'=>'approved'],'review'));
$group=$gl('correction_prepare','accounting_prepare','correct-prepare',['journal_id'=>$p['steps']['charge_ledger_post']['journal_id'],'journal_on'=>'2026-10-31','period_key'=>'QA-2026-INTEGRADO','reason'=>'Par técnico explícito estorno100 e substituição120.']);
$gl('correction_review','accounting_review','correct-validate',['correction_id'=>$group['correction_id'],'revision'=>$group['revision'],'decision'=>'approved','reason'=>'Revisão distinta do par técnico100 para120 artificial.']);
$input=['correction_id'=>$group['correction_id'],'revision'=>$group['revision'],'reason'=>'Aplicar par indivisível estorno100 e substituição120.'];
if(!isset($p['steps']['correction_post'])){
$before=$native->table('gl_journals')->count();$failed=false;try{$call('accounting_prepare','correct_post',function($e)use($input){$r=new ReflectionProperty($e,'checkpoint');$r->setValue($e,function($stage){if($stage==='after_correction_first')throw new RuntimeException('injected_asset_pair_failure');});return$e->command('correct-post',$input,(string)Str::uuid());});}catch(RuntimeException$e){if($e->getMessage()!=='injected_asset_pair_failure')throw$e;$failed=true;}$ok($failed&&$native->table('gl_journals')->count()===$before,'failure after first correction header rolls back pair');
}
$out=$gl('correction_post','accounting_prepare','correct-post',$input);
$balance=app($old)->balance($f['scope'],$p['asset'],$native);if(!isset($p['steps']['writeoff_post']))$ok($balance['accumulated_minor']===12000&&$balance['carrying_minor']===108000,'pair gives accumulated120 carrying1080');
$disposal=['operation'=>'establish','asset_key'=>$p['asset'],'effect_on'=>'2026-11-01','evidence'=>'Abate artificial sem contraprestação só no livro de regressão.'];$cmd('disposal_prepare',15,$faInput('disposal',$p['disposal'],0,$disposal));$dr=$cmd('disposal_review',16,$faInput('disposal',$p['disposal'],1,['decision'=>'approved'],'review'));
$dp=$gl('writeoff_prepare','accounting_prepare','effect-prepare',['effect_type'=>'assets.writeoff','source_id'=>$dr['id'],'journal_on'=>'2026-11-01','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'QA-ASSET-WRITEOFF','reason'=>'Abate artificial sem contraprestação em três linhas.']);$gl('writeoff_review','accounting_review','validate',['proposal_id'=>$dp['proposal_id'],'revision'=>$dp['revision'],'decision'=>'approved','reason'=>'Revisão distinta do abate artificial em três linhas.']);$dj=$gl('writeoff_post','accounting_prepare','post',['proposal_id'=>$dp['proposal_id'],'revision'=>$dp['revision'],'reason'=>'Aplicar abate artificial após revisão distinta.']);$lines=$native->table('gl_lines')->where('journal_id',$dj['journal_id'])->orderBy('line_no')->get();$ok($lines->count()===3&&array_sum($lines->pluck('debit_minor')->all())===120000&&array_sum($lines->pluck('credit_minor')->all())===120000,'three line writeoff120 accumulated and1080 loss balanced1200');$ok(app($old)->balance($f['scope'],$p['asset'],$native)['carrying_minor']===0,'written off carrying zero');
$reject(fn()=>$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>'assets.depreciation','source_id'=>$cr['id'],'journal_on'=>'2026-10-31','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'QA-ASSET-DEPRECIATION','reason'=>'Tentativa indevida de duplicar quota já aplicada.'],(string)Str::uuid())),409,'duplicate depreciation after writeoff denied');
file_put_contents($work.'/assets-correction.json',json_encode(['passed'=>true,'checks'=>$checks,'regression_only'=>true,'pair_atomic'=>true,'writeoff_three_lines'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/assets-correction.json']).PHP_EOL;
+2
View File
@@ -0,0 +1,2 @@
<?php
$book='8085';require __DIR__.'/sgf-assets-proof-bootstrap.php';config(['sales_ledger.synthetic_enabled'=>true,'cost_ledger.synthetic_enabled'=>true,'industrial_ledger.synthetic_enabled'=>true,'cost_ledger.correction_enabled'=>true,'industrial_ledger.correction_enabled'=>true,'cash_qa.enabled'=>true,'cash_qa.binding_sha256'=>hash_file('sha256',$s['root'].'/cash-binding.json')]);try{$out=$call('read_only','view',function($e){$method=new ReflectionMethod($e,'periodSnapshot');return$method->invoke($e,'QA-2026-INTEGRADO');});}catch(Throwable $e){echo json_encode(['error'=>get_class($e),'file'=>$e->getFile(),'line'=>$e->getLine(),'trace'=>array_map(fn($t)=>array_intersect_key($t,array_flip(['file','line','class','function'])),array_slice($e->getTrace(),0,9))]);exit(1);}file_put_contents($work.'/assets-current-period-private.json',json_encode($out));$pending=$out['pending']??$out['pending_facts']??null;$keys=array_keys($out);echo json_encode(['keys'=>$keys,'pending'=>$pending,'facts'=>$out['facts']??null,'proof'=>$work.'/assets-current-period-private.json']).PHP_EOL;
+1
View File
@@ -0,0 +1 @@
import fs from'node:fs';import assert from'node:assert/strict';import{session}from'./sgf-single-http.mjs';const checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);};const s=await session('sgf:finance_review','accounting');let r=await s.get('/api/service-fiscal');assert.equal(r.status,200);const c=(await r.json()).coverage,a=c.management.find(v=>v.id==='accounting');ok(c.version==='SGF-FISCAL-COVERAGE-v4'&&a.available.includes('activos QA integrados'),'coverage recognises delivered assets QA');ok(a.pending.includes('tratamento fiscal')&&!c.legal_validation&&!c.official_submission_enabled,'legal fiscal and official gates remain closed');const reader=await session('sgf:read_only','accounting'),root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',p=JSON.parse(fs.readFileSync(root+'/assets-http-progress-private.json'));r=await reader.get('/api/fixed-assets/ledger?asset_key='+p.asset);const ledger=await r.json();ok(r.status===200&&ledger.records.data.filter(p=>p.journal).length===2&&ledger.balance.carrying_minor===110000,'same preserved two asset journals visible after source version guard');const proof=root+'/assets-final-read-http-20261111.json';fs.writeFileSync(proof,JSON.stringify({passed:true,checks,business_commands:0,new_demo:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof}));
+14 -2
View File
@@ -1,2 +1,14 @@
import fs from 'node:fs';import assert from 'node:assert/strict';import crypto from 'node:crypto';
const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',s=JSON.parse(fs.readFileSync(root+'/state.json')),checks=[];const names=['sgf-entry-menu','sgf-stock-final','sgf-stock-completion','sgf-stock-catalogue','sgf-service-documents','sgf-cash','sgf-iva','sgf-fiscal-rules','sgf-bank','sgf-indicators'];for(const name of names){const path='/'+name+'.mjs',r=await fetch('http://127.0.0.1:8085'+path),bytes=Buffer.from(await r.arrayBuffer());assert.equal(r.status,200,path);assert(/javascript|ecmascript/.test(r.headers.get('content-type')),path+' JavaScript MIME');assert.equal(crypto.createHash('sha256').update(bytes).digest('hex'),crypto.createHash('sha256').update(fs.readFileSync(s.app+'/public'+path)).digest('hex'),path+' published bytes');checks.push(path+' actual JavaScript MIME and exact bytes');}const queried=await fetch('http://127.0.0.1:8085/sgf-stock-final.mjs?check=1');assert(/javascript/.test(queried.headers.get('content-type')));checks.push('query string preserves module delivery');const missing=await fetch('http://127.0.0.1:8085/unknown-sgf-module.mjs');assert.equal(missing.status,404);assert(!/html/.test(missing.headers.get('content-type')));checks.push('missing module404 not application HTML');const old=await fetch('http://127.0.0.1:8085/stock-operations-workspace.mjs');assert.equal(old.status,200);assert(/javascript/.test(old.headers.get('content-type')));checks.push('existing physical operations script still served');const file=root+'/assets-http-20261110.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,business_commands:0,database_writes:0,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
import fs from'node:fs';import assert from'node:assert/strict';import crypto from'node:crypto';import{session}from'./sgf-single-http.mjs';
const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',file=root+'/assets-http-progress-private.json',saved=fs.existsSync(file)?JSON.parse(fs.readFileSync(file)):{asset:crypto.randomUUID(),plan:crypto.randomUUID(),charge:crypto.randomUUID(),steps:{}},save=()=>fs.writeFileSync(file,JSON.stringify(saved));save();const checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);},prep=await session('sgf:accounting_prepare','accounting'),review=await session('sgf:accounting_review','accounting'),read=await session('sgf:read_only','accounting');
ok(prep.identity.context.fixed_assets_enabled===true,'same installation asset feature enabled');const get=async(path,s=read)=>{const r=await s.get(path);assert.equal(r.status,200,path);return r.json();};
async function command(name,s,body,domain='admin'){const step=saved.steps[name]??={key:crypto.randomUUID(),body};save();assert.equal(JSON.stringify(step.body),JSON.stringify(body),'stable saved command');if(step.result)return step.result;const receiptPath=domain==='ledger'?'/api/general-ledger/receipts/':'/api/fixed-assets/receipts/';let r=await s.get(receiptPath+step.key);if(r.status===200){step.result=(await r.json()).result;save();return step.result;}assert.equal(r.status,404);const base='/api/fixed-assets'+(domain==='ledger'?'/ledger':'');const cut=await s.command(base+'/preview',body,step.key);assert.equal(cut.status,200,cut.value.message);step.cut=cut.value;save();const out=await s.command(base+'/confirm',{preview:cut.value.preview},step.key);assert.equal(out.status,200,out.value.message);step.result=out.value;save();ok(true,name+' once HTTP preview confirm');return out.value;}
const input=(kind,key,version,data,action='prepare')=>({kind,record_key:key,version,action,data,reason:'Conferência individual do ciclo de activos QA aprovado, sem operação real.'});
const asset={operation:'establish',code:'QA-ACTIVO-01',description:'Equipamento fictício de apresentação do ciclo de activos','origin_reference':'QA-ABERTURA-ACTIVO-01',cost:'1200.00',residual:'0.00',available_on:'2026-10-01',location:'QA-SALA-A',custodian:'QA-RESPONSAVEL-ACTIVO-01',evidence:'Abertura exclusivamente de ensaio aprovada; sem aquisição, dívida ou Stock.'};
await command('assetPrepare',prep,input('asset',saved.asset,0,asset));const ar=await command('assetReview',review,input('asset',saved.asset,1,{decision:'approved'},'review'));
const gl=async(name,kind,source,on,rule)=>{const p=await command(name+'Prepare',prep,{action:'effect-prepare',data:{effect_type:kind,source_id:source,journal_on:on,period_key:'QA-2026-INTEGRADO',rule_key:rule,reason:'Preparação individual QA da origem revista de activos.'}},'ledger');await command(name+'Review',review,{action:'validate',data:{proposal_id:p.proposal_id,revision:p.revision,decision:'approved',reason:'Revisão independente QA da origem de activos e contas.'}},'ledger');return command(name+'Post',prep,{action:'post',data:{proposal_id:p.proposal_id,revision:p.revision,reason:'Aplicação individual QA do impacto de activos conferido.'}},'ledger');};
await gl('admission','assets.admitted',ar.id,'2026-10-01','QA-ASSET-ADMISSION');await command('planPrepare',prep,input('plan',saved.plan,0,{operation:'establish',asset_key:saved.asset,life_months:12,status:'active',evidence:'Vida artificial12 meses e residual0 explícitos; sem tabela fiscal legal.'}));await command('planReview',review,input('plan',saved.plan,1,{decision:'approved'},'review'));await command('chargePrepare',prep,input('charge',saved.charge,0,{operation:'establish',asset_key:saved.asset,plan_key:saved.plan,month:'2026-10',evidence:'Quota Outubro100 calculada pelo plano revisto, decisão individual sem automatismo.'}));const cr=await command('chargeReview',review,input('charge',saved.charge,1,{decision:'approved'},'review'));await gl('depreciation','assets.depreciation',cr.id,'2026-10-31','QA-ASSET-DEPRECIATION');
const state=await get('/api/fixed-assets/ledger?asset_key='+saved.asset);ok(state.balance.cost_minor===120000&&state.balance.accumulated_minor===10000&&state.balance.carrying_minor===110000&&!state.balance.disposed,'actual8085 cost1200 accumulated100 carrying1100');ok(state.records.data.filter(p=>p.journal).length===2,'two actual journals for explicit new asset');const list=await get('/api/fixed-assets?kind=asset&search=QA-ACTIVO-01&per_page=25');ok(list.records.data.length===1&&list.records.data[0].effective.version===2,'single new reviewed asset visible');const history=await get('/api/fixed-assets?kind=asset&record_key='+saved.asset);ok(history.records.data.length===2,'preparation and review history preserved');
if(!saved.replayed){const st=saved.steps.depreciationPost;const out=await prep.command('/api/fixed-assets/ledger/confirm',{preview:st.cut.preview},st.key);assert.equal(out.status,200,out.value.message);assert(out.value.replayed);saved.replayed=true;save();}ok(saved.replayed,'same post confirmation recovered without duplication');
const denied=await read.command('/api/fixed-assets/preview',input('asset',crypto.randomUUID(),0,asset));ok(denied.status===403,'read only cannot prepare');const finance=await session('sgf:finance_prepare','finance').catch(()=>null);if(finance){const r=await finance.get('/api/fixed-assets?kind=asset');ok([403,409].includes(r.status),'finance profile cannot enter assets');}let r=await read.get('/api/fixed-assets?kind=asset&tenant_id=2');ok(r.status===422,'tenant override denied');r=await review.get('/api/fixed-assets/receipts/'+saved.steps.assetPrepare.key);ok(r.status===404,'other actor receipt denied');r=await read.get('/api/fixed-assets?kind=asset&per_page=1000');ok(r.status===422,'unbounded page denied');
const manifest=JSON.parse(fs.readFileSync(root+'/app/public/build/manifest.json'));for(const e of Object.values(manifest).filter(e=>e.isEntry||/FixedAssetsWorkspace/.test(e.file))){r=await fetch('http://127.0.0.1:8085/build/'+e.file);const b=Buffer.from(await r.arrayBuffer());ok(r.status===200&&/javascript/.test(r.headers.get('content-type'))&&crypto.createHash('sha256').update(b).digest('hex')===crypto.createHash('sha256').update(fs.readFileSync(root+'/app/public/build/'+e.file)).digest('hex'),'served MIME and SHA '+e.file);}
const proof=root+'/assets-http-20261111.json';fs.writeFileSync(proof,JSON.stringify({passed:true,checks,administrative_commands:6,journal_commands:6,new_journals:2,new_demo:false,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof}));
+2
View File
@@ -0,0 +1,2 @@
<?php
require __DIR__.'/sgf-single-bootstrap.php';echo $native->selectOne("SELECT pg_get_functiondef('gl_cycle_pair_integrity()'::regprocedure) d")->d;echo PHP_EOL;echo json_encode(array_keys(json_decode(file_get_contents($s['root'].'/assets-regression-progress-private.json'),true)['steps']));
+12
View File
@@ -0,0 +1,12 @@
<?php
require __DIR__.'/sgf-assets-proof-bootstrap.php';$host->beginTransaction();$native->beginTransaction();try{
$key=(string)Str::uuid();$a=['operation'=>'establish','code'=>'QA-WRAPPER-'.strtoupper(substr($key,0,8)),'description'=>'Equipamento de ensaio HTTP revertido','origin_reference'=>'QA-WRAPPER-OPEN-'.strtoupper(substr($key,0,8)),'cost'=>'1200.00','residual'=>'0.00','available_on'=>'2026-10-01','location'=>'QA-SALA-A','custodian'=>'QA-CUSTODIA-A','evidence'=>'Fixture do controlador de conferência contabilística.'];$faCommand(15,$faInput('asset',$key,0,$a));$ar=$faCommand(16,$faInput('asset',$key,1,['decision'=>'approved'],'review'))[0];
$jc=app(App\Modules\Accounting\FixedAssetJournalController::class);$select=function($actor)use($faCall){$faCall($actor,'index',['kind'=>'asset']);app(App\Shared\Context\CompanyContext::class)->selectFor($actor,1,2,2);};$invoke=function($actor,$method,$input,$key=null)use($select,$jc){$select($actor);request()->replace($input);request()->headers->set('Idempotency-Key',$key??(string)Str::uuid());$out=$jc->$method(request());return['status'=>$out->getStatusCode(),'value'=>$out->getData(true)];};
$d=['action'=>'effect-prepare','data'=>['effect_type'=>'assets.admitted','source_id'=>$ar['id'],'journal_on'=>'2026-10-01','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'QA-ASSET-ADMISSION','reason'=>'Prévia individual do escritor contabilístico original.']];$count=$native->table('gl_proposals')->count();$key=(string)Str::uuid();$cut=$invoke(15,'preview',$d,$key)['value'];$ok($native->table('gl_proposals')->count()===$count&&count($cut['impact']['snapshots'][0]['lines'])===2,'ledger preview rollback and two typed lines');$out=$invoke(15,'confirm',['preview'=>$cut['preview']])['value'];$again=$invoke(15,'confirm',['preview'=>$cut['preview']])['value'];$ok($again['replayed']&&$again['proposal_id']===$out['proposal_id'],'ledger confirmation receipt no duplicate');$reject(fn()=>$invoke(17,'preview',$d),403,'ledger reader cannot prepare');
$rv=['action'=>'validate','data'=>['proposal_id'=>$out['proposal_id'],'revision'=>$out['revision'],'decision'=>'approved','reason'=>'Revisão independente da captura do activo.']];$cut=$invoke(16,'preview',$rv)['value'];$ok($native->table('gl_reviews')->where('proposal_id',$out['proposal_id'])->count()===0,'review preview also rolls back');$invoke(16,'confirm',['preview'=>$cut['preview']]);$post=['action'=>'post','data'=>['proposal_id'=>$out['proposal_id'],'revision'=>$out['revision'],'reason'=>'Aplicar captura de activo após conferência independente.']];$cut=$invoke(15,'preview',$post)['value'];$ok($cut['impact']['journal_count']===1&&$native->table('gl_journals')->where('proposal_id',$out['proposal_id'])->count()===0,'post preview shows impact without journal');$j=$invoke(15,'confirm',['preview'=>$cut['preview']])['value'];$ok($native->table('gl_journals')->where('id',$j['journal_id'])->value('debit_minor')===120000,'wrapper posts through original journal');
$bad=$a;$bad['operation']='amend';$bad['cost']='1300.00';$reject(fn()=>$faCall(15,'preview',$faInput('asset',$aKey=$ar['record_key'],2,$bad)),409,'posted cost cannot silently change');$legacy=$native->table('gl_proposals')->whereRaw("snapshot->'source'->>'effect_type' NOT LIKE 'assets.%'")->value('id');if($legacy)$reject(fn()=>$invoke(16,'preview',['action'=>'validate','data'=>['proposal_id'=>$legacy,'revision'=>str_repeat('a',64),'decision'=>'approved','reason'=>'Tentativa de acesso a outra família pelo adaptador.']]),403,'asset wrapper denies legacy proposal');
$badType=$d;$badType['data']['effect_type']='sales.invoice';$reject(fn()=>$invoke(15,'preview',$badType),422,'asset wrapper cannot prepare another family');
}finally{$native->rollBack();$host->rollBack();}
// New function retains every previous branch and validates pre-existing balanced journals read-only.
$ids=$native->table('gl_journals')->whereRaw("snapshot->'source'->>'effect_type' NOT LIKE 'assets.%'")->orderBy('id')->pluck('id');foreach($ids as$id)$native->select('SELECT gl_assert_balanced(?)',[$id]);$ok(true,$ids->count()?'existing journal balance constraints remain satisfied':'regression has no legacy journals; verify27 on8085 read-only separately');
file_put_contents($work.'/assets-journal-boundaries.json',json_encode(['passed'=>true,'checks'=>$checks,'legacy_balanced_journals'=>$ids->count(),'fixtures_rolled_back'=>true,'same_writer'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/assets-journal-boundaries.json']).PHP_EOL;
@@ -0,0 +1,2 @@
<?php
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$periods=$native->table('gl_periods')->where(['tenant_id'=>1,'company_id'=>2,'unit_id'=>2])->orderBy('id')->get(['period_key','version','starts_on','ends_on','status'])->all();file_put_contents($work.'/assets-ledger-context-private.json',json_encode(['periods'=>$periods,'journals'=>$native->table('gl_journals')->count(),'accounts'=>$native->table('gl_accounts')->where(['tenant_id'=>1,'company_id'=>2,'unit_id'=>2])->count()],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'periods'=>$periods,'journals'=>$native->table('gl_journals')->count(),'accounts'=>$native->table('gl_accounts')->where(['tenant_id'=>1,'company_id'=>2,'unit_id'=>2])->count(),'proof'=>$work.'/assets-ledger-context-private.json']).PHP_EOL;
+2
View File
@@ -0,0 +1,2 @@
<?php
require __DIR__.'/sgf-assets-proof-bootstrap.php';$out=$call('read_only','view',function($e,$actor,$binding,$origin,$scope,$stock,$hostRead)use($native){return$hostRead(fn()=>app(App\Modules\Accounting\FixedAssetLedgerSource::class)->period(App\Models\User::findOrFail(17),['period_key'=>'QA-2026-INTEGRADO','starts_on'=>'2026-01-01','ends_on'=>'2026-12-31'],$native,$scope));});$ok($out['facts']===4&&$out['pending']===1,'period recognises written off corrected cycle and pending concurrent admission');$ok(strlen($out['hash'])===64,'period dependencies fingerprinted');file_put_contents($work.'/assets-period.json',json_encode(['passed'=>true,'checks'=>$checks,'asset_facts'=>$out['facts'],'pending'=>$out['pending'],'reason'=>'One explicitly approved concurrency support asset has no admission journal. Main corrected and written-off cycle resolved.','new_commands'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/assets-period.json']).PHP_EOL;
+8
View File
@@ -0,0 +1,8 @@
<?php
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$backup=json_decode(file_get_contents($s['root'].'/assets-backup-pointer.json'),true)['root'];$before=json_decode(file_get_contents($backup.'/preservation-private.json'),true);$expected=['host.permission_grants'=>5,'host.identity_audit'=>11,'native.gl_accounts'=>5,'native.gl_rules'=>6,'native.gl_proposals'=>2,'native.gl_reviews'=>2,'native.gl_journals'=>2,'native.gl_lines'=>4,'native.gl_commands'=>6,'native.gl_events'=>6,'native.fs_versions'=>4,'native.fs_commands'=>4,'native.fs_results'=>1,'native.fs_result_lines'=>23,'native.audit_logs'=>4];$checks=[];$changes=[];$canonical=fn($r)=>Tempo\Finance\Payables\Payables::canonical((array)$r);
foreach(['host','native']as$name){$db=$name==='host'?$host:$native;foreach($before[$name]as$table=>$json){$key=$name.'.'.$table;$old=json_decode($json,true);$current=$db->table($table)->get()->map(fn($r)=>(array)$r)->all();$seen=[];foreach($current as$r)$seen[$canonical($r)]=true;foreach($old as$r)if(!isset($seen[$canonical($r)]))throw new LogicException('Original row changed: '.$key);$delta=count($current)-count($old);if($delta!==($expected[$key]??0))throw new LogicException('Unexpected append '.$key.' '.$delta.' expected '.($expected[$key]??0));$checks[]=$key;if($delta)$changes[$key]=$delta;}}
foreach($before['files']as$file=>$sha){if(hash_file('sha256',$s['root'].'/'.$file)!==$sha)throw new LogicException('Binding changed '.$file);$checks[]=$file;}
$scope=['tenant_id'=>1,'company_id'=>2,'unit_id'=>2];$grants=$host->table('permission_grants')->where($scope)->where('permission','like','fixed_assets.%')->orderBy('id')->get(['user_id','permission'])->map(fn($r)=>(array)$r)->all();$p=json_decode(file_get_contents($repo.'/docs/integration/sgf-fixed-assets-access-proposal.json'),true);if($grants!==$p['grants_per_book'])throw new LogicException('Grant perimeter differs');if($host->table('fa_versions')->count()!==6||$host->table('fa_commands')->count()!==6)throw new LogicException('Expected single asset plan quota decisions');if($native->table('gl_journals')->count()!==29||$native->table('gl_accounts')->count()!==24)throw new LogicException('Journal/chart count mismatch');if($native->table('gl_periods')->where($scope)->orderByDesc('version')->value('status')!=='open')throw new LogicException('Period must remain open');
$oldSnapshot=file_get_contents($s['root'].'/statements-frozen-export-private.csv');$oldFs=json_decode(file_get_contents($beforeFile=$s['root'].'/statements-http-progress.json'),true)['resultReview']['result']['id'];$oldData=json_decode($native->table('fs_versions')->where('id',$oldFs)->value('data'),true);if(hash('sha256',$oldSnapshot)!==$oldData['csv_sha256'])throw new LogicException('Old snapshot SHA mismatch');
$public=$host->selectOne("SELECT EXISTS(SELECT 1 FROM information_schema.table_privileges WHERE grantee='PUBLIC' AND table_name IN('fa_meta','fa_versions','fa_commands')) p")->p;if($public)throw new LogicException('Public administrative access prohibited');$checks[]='new_rights_exact';$checks[]='6administrative_versions_receipts';$checks[]='29journals24accounts';$checks[]='period_remains_open';$checks[]='old_statement_sha_preserved';$checks[]='no_public_assets_tables';
file_put_contents($work.'/assets-preservation.json',json_encode(['passed'=>true,'checks'=>$checks,'checked_original_sets'=>count($before['host'])+count($before['native']),'original_rows_identical'=>true,'approved_appends'=>$changes,'bindings_unchanged'=>3,'new_demo'=>false,'cache_cleared'=>false,'protected_records_unchanged'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'original_sets'=>count($before['host'])+count($before['native']),'proof'=>$work.'/assets-preservation.json']).PHP_EOL;
+9
View File
@@ -0,0 +1,9 @@
<?php
require __DIR__.'/sgf-ledger-proof-bootstrap.php';
config(['fixed_assets.enabled'=>true,'fixed_assets.schema_sha256'=>hash_file('sha256',$repo.'/database/sql/fixed_assets.sql'),'fixed_assets.native_schema_sha256'=>hash_file('sha256',$repo.'/database/sql/fixed_assets_native.sql')]);
$faCall=function(int $actor,string $method,array $input=[],?string $key=null,string $generation='assets-proof',string $module='accounting',?array $scope=null)use($app,$f){
Illuminate\Support\Facades\Auth::forgetGuards();Illuminate\Support\Facades\Auth::shouldUse('web');$u=App\Models\User::findOrFail($actor);Illuminate\Support\Facades\Auth::guard('web')->setUser($u);$r=Illuminate\Http\Request::create('/api/fixed-assets',$method==='index'?'GET':'POST',$input);$r->headers->set('Idempotency-Key',$key??(string)Illuminate\Support\Str::uuid());$r->attributes->set('sgf_module',$module);$r->setUserResolver(fn()=>$u);$session=app('session')->driver();$session->start();$session->put('company_context',($scope??$f['scope'])+['module_id'=>$module,'token'=>$generation.'-'.$actor]);$r->setLaravelSession($session);$app->instance('request',$r);$ctx=app(App\Shared\Context\CompanyContext::class);$v=$scope??$f['scope'];$ctx->selectFor($actor,$v['tenant_id'],$v['company_id'],$v['unit_id']);try{$c=app(App\Modules\Accounting\FixedAssetsController::class);$out=$method==='receipt'?$c->receipt($r,app(App\Modules\Accounting\FixedAssets::class),$key):$c->$method($r,app(App\Modules\Accounting\FixedAssets::class));return['status'=>$out->getStatusCode(),'value'=>$out->getData(true)];}finally{$ctx->clear();}
};
$faInput=fn($kind,$key,$version,$data,$action='prepare')=>['kind'=>$kind,'record_key'=>$key,'version'=>$version,'action'=>$action,'data'=>$data,'reason'=>'Conferência individual do ciclo de activos artificiais autorizado.'];
$faCommand=function($actor,$input,$key=null)use($faCall){$key??=(string)Illuminate\Support\Str::uuid();$cut=$faCall($actor,'preview',$input,$key);if($cut['status']!==200)throw new LogicException(json_encode($cut));$out=$faCall($actor,'confirm',['preview'=>$cut['value']['preview']]);if($out['status']!==200)throw new LogicException(json_encode($out));return[$out['value'],$cut['value'],$key];};
$reject=function($fn,$status,$label)use($ok){$actual=null;try{$v=$fn();$actual=$v['status']??null;}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface$e){$actual=$e->getStatusCode();}catch(Illuminate\Validation\ValidationException){$actual=422;}catch(DomainException){$actual=409;}$ok($actual===$status,$label);};
+19
View File
@@ -0,0 +1,19 @@
<?php
require __DIR__.'/sgf-assets-proof-bootstrap.php';
$progress=$s['root'].'/assets-regression-progress-private.json';$p=is_file($progress)?json_decode(file_get_contents($progress),true):['asset'=>(string)Str::uuid(),'plan'=>(string)Str::uuid(),'charge'=>(string)Str::uuid(),'disposal'=>(string)Str::uuid(),'steps'=>[]];$save=function()use(&$p,$progress){file_put_contents($progress,json_encode($p));};$save();
$step=function($name,Closure $fn)use(&$p,$save,$ok){if(!isset($p['steps'][$name])){$p['steps'][$name]=$fn();$save();}$ok(true,$name.' recorded once');return$p['steps'][$name];};
$cmd=function($name,$actor,$data)use($step,$faCommand){return $step($name,fn()=>$faCommand($actor,$data)[0]);};
$a=['operation'=>'establish','code'=>'QA-ACTIVO-REG-01','description'=>'Equipamento artificial de regressão integrado','origin_reference'=>'QA-ABERTURA-ACTIVO-REG-01','cost'=>'1200.00','residual'=>'0.00','available_on'=>'2026-10-01','location'=>'QA-SALA-A','custodian'=>'QA-RESPONSAVEL-ACTIVO-01','evidence'=>'Origem exclusivamente artificial do ciclo aprovado.'];
$reject(fn()=>$faCall(17,'preview',$faInput('asset',$p['asset'],0,$a)),403,'reader cannot prepare');$reject(fn()=>$faCall(15,'preview',$faInput('asset',$p['asset'],0,$a),null,'assets-proof','finance'),403,'wrong module denied');$reject(fn()=>$faCall(13,'index',['kind'=>'asset']),403,'finance actor denied');
$ap=$cmd('asset_prepare',15,$faInput('asset',$p['asset'],0,$a));
if(!isset($p['steps']['asset_review']))$reject(fn()=>$faCall(15,'preview',$faInput('asset',$p['asset'],1,['decision'=>'approved'],'review')),403,'own review denied');
$ar=$cmd('asset_review',16,$faInput('asset',$p['asset'],1,['decision'=>'approved'],'review'));
$gl=function($name,$role,$action,$data)use($step,$call){return$step($name,fn()=>$call($role,$action==='effect-prepare'?'prepare':str_replace('-','_',$action),fn($e)=>$e->command($action,$data,(string)Str::uuid())));};
$ad=$gl('admission_prepare','accounting_prepare','effect-prepare',['effect_type'=>'assets.admitted','source_id'=>$ar['id'],'journal_on'=>'2026-10-01','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'QA-ASSET-ADMISSION','reason'=>'Admissão individual artificial revista na regressão.']);
$gl('admission_review','accounting_review','validate',['proposal_id'=>$ad['proposal_id'],'revision'=>$ad['revision'],'decision'=>'approved','reason'=>'Revisão distinta da admissão artificial na regressão.']);
$aj=$gl('admission_post','accounting_prepare','post',['proposal_id'=>$ad['proposal_id'],'revision'=>$ad['revision'],'reason'=>'Aplicação individual da admissão artificial revista.']);
$plan=['operation'=>'establish','asset_key'=>$p['asset'],'life_months'=>12,'status'=>'active','evidence'=>'Plano linear artificial12 meses aprovado explicitamente.'];$cmd('plan_prepare',15,$faInput('plan',$p['plan'],0,$plan));$cmd('plan_review',16,$faInput('plan',$p['plan'],1,['decision'=>'approved'],'review'));
$charge=['operation'=>'establish','asset_key'=>$p['asset'],'plan_key'=>$p['plan'],'month'=>'2026-10','evidence'=>'Quota artificial Outubro após admissão, sem automatismo.'];$cmd('charge_prepare',15,$faInput('charge',$p['charge'],0,$charge));$cr=$cmd('charge_review',16,$faInput('charge',$p['charge'],1,['decision'=>'approved'],'review'));
$cp=$gl('charge_ledger_prepare','accounting_prepare','effect-prepare',['effect_type'=>'assets.depreciation','source_id'=>$cr['id'],'journal_on'=>'2026-10-31','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'QA-ASSET-DEPRECIATION','reason'=>'Quota Outubro100 artificial revista individualmente.']);$gl('charge_ledger_review','accounting_review','validate',['proposal_id'=>$cp['proposal_id'],'revision'=>$cp['revision'],'decision'=>'approved','reason'=>'Revisão distinta da quota Outubro100 artificial.']);$cj=$gl('charge_ledger_post','accounting_prepare','post',['proposal_id'=>$cp['proposal_id'],'revision'=>$cp['revision'],'reason'=>'Aplicação da quota100 após revisão distinta.']);
$balance=app(App\Modules\Accounting\FixedAssets::class)->balance($f['scope'],$p['asset'],$native);if(!isset($p['steps']['correction_post']))$ok($balance['cost_minor']===120000&&$balance['accumulated_minor']===10000&&$balance['carrying_minor']===110000,'cost1200 accumulated100 carrying1100');
file_put_contents($work.'/assets-functional.json',json_encode(['passed'=>true,'checks'=>$checks,'steps'=>array_keys($p['steps']),'new_demo'=>false,'old_suites_reused'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/assets-functional.json']).PHP_EOL;
+9
View File
@@ -0,0 +1,9 @@
<?php
require __DIR__.'/sgf-assets-proof-bootstrap.php';$host->beginTransaction();$native->beginTransaction();try{
$keys=[];foreach(['asset','plan','charge','disposal']as$k)$keys[$k]=(string)Str::uuid();$a=['operation'=>'establish','code'=>'QA-WRITEOFF-ROLLBACK','description'=>'Activo artificial para abate100 e1100 revertido','origin_reference'=>'QA-WRITEOFF-OPEN-ROLLBACK','cost'=>'1200.00','residual'=>'0.00','available_on'=>'2026-10-01','location'=>'QA-SALA-A','custodian'=>'QA-CUSTODIA-A','evidence'=>'Abate100 acumulado e1100 perda, fixture revertida.'];$admin=function($kind,$input)use($faCommand,$faInput,$keys){$faCommand(15,$faInput($kind,$keys[$kind],0,$input));return$faCommand(16,$faInput($kind,$keys[$kind],1,['decision'=>'approved'],'review'))[0];};$gl=function($kind,$id,$on,$rule)use($call){$p=$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>$kind,'source_id'=>$id,'journal_on'=>$on,'period_key'=>'QA-2026-INTEGRADO','rule_key'=>$rule,'reason'=>'Conferência de activo no recorte QA revertido.'],(string)Str::uuid()));$call('accounting_review','validate',fn($e)=>$e->command('validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Revisão distinta do activo no recorte QA revertido.'],(string)Str::uuid()));return$call('accounting_prepare','post',fn($e)=>$e->command('post',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Aplicação do activo no recorte QA revertido.'],(string)Str::uuid()));};$ar=$admin('asset',$a);$gl('assets.admitted',$ar['id'],'2026-10-01','QA-ASSET-ADMISSION');$admin('plan',['operation'=>'establish','asset_key'=>$keys['asset'],'life_months'=>12,'status'=>'active','evidence'=>'Plano linear artificial de12 meses para abate.']);$cr=$admin('charge',['operation'=>'establish','asset_key'=>$keys['asset'],'plan_key'=>$keys['plan'],'month'=>'2026-10','evidence'=>'Quota100 artificial Outubro para abate.']);$gl('assets.depreciation',$cr['id'],'2026-10-31','QA-ASSET-DEPRECIATION');
$dr=$admin('disposal',['operation'=>'establish','asset_key'=>$keys['asset'],'effect_on'=>'2026-11-01','evidence'=>'Abate sintético que deve ser bloqueado se as quotas mudarem.']);$prepareWriteoff=fn()=>$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>'assets.writeoff','source_id'=>$dr['id'],'journal_on'=>'2026-11-01','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'QA-ASSET-WRITEOFF','reason'=>'Conferir abate contra todas as quotas actuais.'],(string)Str::uuid()));$wp=$prepareWriteoff();$ok(isset($wp['proposal_id']),'consistent quota permits writeoff conference');
$plan=['operation'=>'correct_plan','asset_key'=>$keys['asset'],'life_months'=>10,'status'=>'active','evidence'=>'Plano de correcção explicitamente ainda sem nova quota aplicada.'];$faCommand(15,$faInput('plan',$keys['plan'],2,$plan));$reject($prepareWriteoff,409,'pending plan decision blocks writeoff');$faCommand(16,$faInput('plan',$keys['plan'],3,['decision'=>'approved'],'review'));$reject($prepareWriteoff,409,'unresolved changed quota blocks writeoff');
$plan['life_months']=12;$faCommand(15,$faInput('plan',$keys['plan'],4,$plan));$faCommand(16,$faInput('plan',$keys['plan'],5,['decision'=>'approved'],'review'));
$dec=(string)Str::uuid();$d=['operation'=>'establish','asset_key'=>$keys['asset'],'plan_key'=>$keys['plan'],'month'=>'2026-12','evidence'=>'Competência Dezembro sem Novembro aplicado.'];$faCommand(15,$faInput('charge',$dec,0,$d));$ar=$faCommand(16,$faInput('charge',$dec,1,['decision'=>'approved'],'review'))[0];$reject(fn()=>$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>'assets.depreciation','source_id'=>$ar['id'],'journal_on'=>'2026-12-31','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'QA-ASSET-DEPRECIATION','reason'=>'Não saltar competências nem presumir o acumulado.'],(string)Str::uuid())),409,'missing preceding quota blocks excessive out of order depreciation');
$nov=(string)Str::uuid();$d['month']='2026-11';$faCommand(15,$faInput('charge',$nov,0,$d));$nr=$faCommand(16,$faInput('charge',$nov,1,['decision'=>'approved'],'review'))[0];$gl('assets.depreciation',$nr['id'],'2026-11-30','QA-ASSET-DEPRECIATION');$balance=app(App\Modules\Accounting\FixedAssets::class)->balance($f['scope'],$keys['asset'],$native);$ok($balance['accumulated_minor']===20000,'two coherent sequential quotas total200');$plan['life_months']=1;$reject(fn()=>$faCall(15,'preview',$faInput('plan',$keys['plan'],6,$plan)),409,'multiple posted quotas block single quota correction shortcut');
}finally{$native->rollBack();$host->rollBack();}file_put_contents($work.'/assets-sequence-guards.json',json_encode(['passed'=>true,'checks'=>$checks,'all_fixtures_rolled_back'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/assets-sequence-guards.json']).PHP_EOL;
+17
View File
@@ -0,0 +1,17 @@
<?php
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';
$p=json_decode(file_get_contents($repo.'/docs/integration/sgf-fixed-assets-access-proposal.json'),true);$scope=$p['scope'];if(($p['approved']??false)!==true||$p['authorization']['decision']!=='Autorizar o ciclo integrado de activos QA'||$scope!==['tenant_id'=>1,'company_id'=>2,'unit_id'=>2]||$p['grants_per_book']!==array_merge(array_map(fn($u)=>['user_id'=>$u,'permission'=>'fixed_assets.view'],[15,16,17]),[['user_id'=>15,'permission'=>'fixed_assets.prepare'],['user_id'=>16,'permission'=>'fixed_assets.review']]))throw new LogicException('Exact approval required');if($book==='8085'&&!is_file($s['root'].'/assets-backup-pointer.json'))throw new LogicException('Backup first');
$sql=file_get_contents($repo.'/database/sql/fixed_assets.sql');$sha=hash('sha256',$sql);$grants=0;$accounts=0;$rules=0;
$host->transaction(function()use($host,$p,$sql,$sha,$tag,&$grants){if(!$host->getSchemaBuilder()->hasTable('fa_meta')){$host->unprepared($sql);$host->table('fa_meta')->insert(['policy'=>App\Modules\Accounting\FixedAssets::POLICY,'composition'=>$tag,'source_hash'=>$sha]);}else{$m=$host->table('fa_meta')->first();if(!$m||$m->source_hash!==$sha||$m->composition!==$tag)throw new LogicException('Schema differs');}
$host->statement('REVOKE ALL ON TABLE fa_meta,fa_versions,fa_commands FROM PUBLIC');$host->statement('REVOKE ALL ON SEQUENCE fa_versions_id_seq,fa_commands_id_seq FROM PUBLIC');$host->statement('REVOKE ALL ON FUNCTION fa_append_only() FROM PUBLIC');$old=$host->selectOne("SELECT pg_get_constraintdef(oid) d FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check'")->d;if(!str_contains($old,'fixed_assets.view')){if(!preg_match('/^CHECK \((.*)\)$/s',$old,$m))throw new LogicException('Unknown scope check');$host->statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check');$host->statement('ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK (('.$m[1].") OR (unit_id IS NOT NULL AND permission IN('fixed_assets.view','fixed_assets.prepare','fixed_assets.review')))");}
foreach($p['grants_per_book']as$g){$g=$p['scope']+$g;if($host->table('permission_grants')->where($g)->exists())continue;$host->table('permission_grants')->insert($g);App\Modules\Identity\Services\Audit::record('access.granted_on_creation',null,1,2,2,$g['user_id'],['permission'=>$g['permission'],'reason'=>$p['authorization'],'limits'=>'Activos artificiais QA; sem operações reais.']);$grants++;}});
$nativeSql=file_get_contents($repo.'/database/sql/fixed_assets_native.sql');$nativeSha=hash('sha256',$nativeSql);
$native->transaction(function()use($native,$host,$scope,$p,$nativeSql,$nativeSha,&$accounts,&$rules,$work){Tempo\Finance\GeneralLedger\LedgerSchema::assertSynthetic($native);if(!$native->getSchemaBuilder()->hasTable('gl_asset_books')){
$original=$native->selectOne("SELECT pg_get_functiondef('gl_assert_balanced(bigint)'::regprocedure) d")->d;file_put_contents($work.'/gl-balanced-before-private.sql',$original);$needle="WHEN j.snapshot->'source'->>'effect_type'='sales.invoice' THEN 3 ELSE 2 END";if(substr_count($original,$needle)!==1)throw new LogicException('Unrecognised balance function');$replacement="WHEN j.snapshot->'source'->>'effect_type'='sales.invoice' THEN 3 WHEN j.snapshot->'source'->>'effect_type'='assets.writeoff' THEN jsonb_array_length(j.snapshot->'lines') ELSE 2 END";$native->unprepared(str_replace($needle,$replacement,$original));$native->unprepared($nativeSql);$native->table('gl_asset_books')->insert($scope+['policy'=>'QA-ASSET-JOURNAL-v1','host_database'=>$host->getDatabaseName(),'schema_sha256'=>$nativeSha]);
}else{$m=$native->table('gl_asset_books')->where($scope)->first();if(!$m||!in_array($m->schema_sha256,[$nativeSha,'4d0a0dae2a843979ce61c17c619070f67723b235b22989c1820acfe0686a21de'],true)||$m->host_database!==$host->getDatabaseName())throw new LogicException('Native schema mismatch');if(!$native->getSchemaBuilder()->hasTable('gl_asset_extension_versions'))$native->unprepared(substr($nativeSql,strpos($nativeSql,'-- ASSET_PAIR_EXTENSION_v1')));}
$extension=$native->table('gl_asset_extension_versions')->where('version',1)->first();if($extension){if($extension->source_hash!==$nativeSha)throw new LogicException('Asset extension mismatch');}else $native->table('gl_asset_extension_versions')->insert(['version'=>1,'source_hash'=>$nativeSha]);
$cycle=$native->selectOne("SELECT pg_get_functiondef('gl_cycle_pair_integrity()'::regprocedure) d")->d;if(!str_contains($cycle,'gl_asset_pair_assert')){$needle=" IF TG_TABLE_NAME='gl_correction_results' THEN";if(!str_contains($cycle,$needle))throw new LogicException('Unknown cycle guard');file_put_contents($work.'/gl-cycle-before-private.sql',$cycle);$at=strpos($cycle,$needle);$branch=" IF TG_TABLE_NAME='gl_correction_results' THEN IF EXISTS(SELECT 1 FROM gl_journals WHERE id=NEW.reverse_journal_id AND snapshot->'source'->>'effect_type'='assets.depreciation.reversal') THEN PERFORM gl_asset_pair_assert(NEW.correction_id); RETURN NEW; END IF; END IF;\n";$native->unprepared(substr($cycle,0,$at).$branch.substr($cycle,$at));}
foreach($p['new_accounts_per_book']as$a){if($native->table('gl_accounts')->where($scope+['code'=>$a['code']])->exists())continue;$native->table('gl_accounts')->insert($scope+['code'=>$a['code'],'label'=>$a['purpose'],'is_movement'=>true,'currency'=>'MZN']);$accounts++;}
$codes=$native->table('gl_accounts')->where($scope)->pluck('id','code');$plans=[['ADMISSION','assets.admitted','CUSTO','ORIGEM-QA'],['DEPRECIATION','assets.depreciation','GASTO-DEP','DEP-ACUM'],['WRITEOFF','assets.writeoff','DEP-ACUM','CUSTO'],['WRITEOFF-NO-DEP','assets.writeoff','GASTO-ABATE','CUSTO'],['CORRECTION-REVERSE','assets.depreciation.reversal','DEP-ACUM','GASTO-DEP'],['CORRECTION-REPLACE','assets.depreciation.replacement','GASTO-DEP','DEP-ACUM']];foreach($plans as[$key,$type,$debit,$credit]){if($native->table('gl_rules')->where($scope+['rule_key'=>'QA-ASSET-'.$key])->exists())continue;$native->table('gl_rules')->insert($scope+['rule_key'=>'QA-ASSET-'.$key,'version'=>1,'policy'=>'QA-ASSET-JOURNAL-v1','effect_type'=>$type,'currency'=>'MZN','valid_from'=>'2026-01-01','valid_to'=>'2026-12-31','debit_account_id'=>$codes['QA-ACTIVO-'.$debit],'credit_account_id'=>$codes['QA-ACTIVO-'.$credit],'created_by'=>15,'reason'=>'Regra artificial do ciclo de activos expressamente autorizado.','created_at'=>now()->utc()]);$rules++;}
});
file_put_contents($work.'/assets-setup.json',json_encode(['passed'=>true,'book'=>$book,'new_grants'=>$grants,'accounts'=>$accounts,'rules'=>$rules,'new_demo'=>false,'schema_sha256'=>$sha,'native_sha256'=>$nativeSha],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'book'=>$book,'grants'=>$grants,'accounts'=>$accounts,'rules'=>$rules,'proof'=>$work.'/assets-setup.json']).PHP_EOL;
@@ -0,0 +1,2 @@
<?php
require __DIR__.'/sgf-assets-proof-bootstrap.php';$host->beginTransaction();$native->beginTransaction();try{$key=(string)Str::uuid();$a=['operation'=>'establish','code'=>'QA-REVISION-'.strtoupper(substr($key,0,8)),'description'=>'Activo para versão administrativa explícita','origin_reference'=>'QA-REVISION-OPEN-'.strtoupper(substr($key,0,8)),'cost'=>'1200.00','residual'=>'0.00','available_on'=>'2026-10-01','location'=>'QA-SALA-A','custodian'=>'QA-CUSTODIA-A','evidence'=>'Fixture de versão administrativa revertida.'];$faCommand(15,$faInput('asset',$key,0,$a));$ar=$faCommand(16,$faInput('asset',$key,1,['decision'=>'approved'],'review'))[0];$data=['effect_type'=>'assets.admitted','source_id'=>$ar['id'],'journal_on'=>'2026-10-01','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'QA-ASSET-ADMISSION','reason'=>'Captura anterior a uma nova preparação administrativa.'];$p=$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',$data,(string)Str::uuid()));$old=json_decode($native->table('gl_proposals')->where('id',$p['proposal_id'])->value('snapshot'),true)['source'];$a['operation']='amend';$a['location']='QA-SALA-B';$faCommand(15,$faInput('asset',$key,2,$a));$reject(fn()=>$call('accounting_review','validate',fn($e)=>$e->command('validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Não aceitar captura anterior a versão administrativa.'],(string)Str::uuid())),409,'administrative version change invalidates pending journal review');$new=$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',$data,(string)Str::uuid()));$source=json_decode($native->table('gl_proposals')->where('id',$new['proposal_id'])->value('snapshot'),true)['source'];$ok($new['revision']!==$p['revision']&&$source['administrative_revision']!==$old['administrative_revision'],'fresh capture observes new administrative version');$ok($source['recognition_revision']===$old['recognition_revision'],'custody preparation does not change financial recognition or require reversal of applied admission');}finally{$native->rollBack();$host->rollBack();}file_put_contents($work.'/assets-source-version.json',json_encode(['passed'=>true,'checks'=>$checks,'fixtures_rolled_back'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/assets-source-version.json']).PHP_EOL;
+8
View File
@@ -0,0 +1,8 @@
import fs from 'node:fs';import assert from 'node:assert/strict';import crypto from 'node:crypto';import {session} from './sgf-single-http.mjs';import {checkedStatements} from '../resources/js/internal-statements-view.js';
const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',progress=root+'/assets-statements-http-progress-private.json',saved=fs.existsSync(progress)?JSON.parse(fs.readFileSync(progress)):{},save=()=>fs.writeFileSync(progress,JSON.stringify(saved)),checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);},base='/api/general-ledger/statements';
const prep=await session('sgf:accounting_prepare','accounting'),review=await session('sgf:accounting_review','accounting'),read=await session('sgf:read_only','accounting');const get=async(q={view:'history',page:1,per_page:25},s=read)=>{const r=await s.get(base+'?'+new URLSearchParams(q));assert.equal(r.status,200);return checkedStatements(await r.json(),q);};let state=await get();ok(prep.identity.context.internal_statements_enabled===true,'selected installation feature enabled');const names=s=>s.identity.context.grants.filter(g=>g.permission.startsWith('general_ledger.statements.')).map(g=>g.permission);ok(JSON.stringify(names(prep))===JSON.stringify(['general_ledger.statements.prepare'])&&JSON.stringify(names(review))===JSON.stringify(['general_ledger.statements.review'])&&names(read).length===0,'exact approved preparation review rights only');
const accounts=await get({view:'accounts',page:1,per_page:100},prep),categories={'QA-CAIXA':'asset','QA-CLIENTES':'asset','QA-STOCK':'asset','QA-FORNECEDOR':'liability','QA-VENDAS':'income','QA-CONSUMO-STOCK':'expense','QA-CUSTO-DIRECTO':'expense','QA-CUSTO-INDUSTRIAL':'expense','QA-DESPESA-CAIXA':'expense','QA-DIFERENCA-INVENTARIO':'expense','QA-COMPENSACAO-ENSAIO':'unclassified','QA-COMPENSACAO-INDUSTRIAL':'unclassified','QA-COMPENSACAO-STOCK':'unclassified','QA-CONTRAPARTIDA':'unclassified','QA-DEVOLUCAO-FORNECEDOR':'unclassified','QA-IMPOSTO-TESTE':'unclassified','QA-LIQUIDACAO':'unclassified','QA-LIQUIDACAO-CLIENTE':'unclassified','QA-REDUCAO-CLIENTE':'unclassified','QA-ACTIVO-CUSTO':'asset','QA-ACTIVO-DEP-ACUM':'asset','QA-ACTIVO-GASTO-DEP':'expense','QA-ACTIVO-ORIGEM-QA':'unclassified','QA-ACTIVO-GASTO-ABATE':'expense'};const entries=Object.entries(categories).map(([code,category])=>{const row=accounts.records.data.find(a=>a.code===code);assert(row,'Explicit approved QA account missing '+code);return{account_id:row.id,category};});ok(entries.length===24&&entries.filter(e=>e.category==='unclassified').length===10,'explicit24 approved choices ten pending');const input=(kind,action,version,data)=>({kind,action,version,data,reason:'Decisão individual QA de demonstrações internas parciais, sem classificação legal ou novos lançamentos.'});
async function perform(name,s,body){saved[name]??={key:crypto.randomUUID()};save();const step=saved[name];if(step.result)return step.result;const old=await s.get(base+'/receipts/'+step.key);if(old.status===200){step.result=(await old.json()).result;save();return step.result;}assert.equal(old.status,404);const cut=await s.command(base+'/preview',body,step.key);assert.equal(cut.status,200,JSON.stringify(cut.value));step.cut=cut.value;save();const out=await s.command(base+'/confirm',{preview:cut.value.preview},step.key);assert.equal(out.status,201,JSON.stringify(out.value));step.result=out.value;save();return out.value;}
const m1=await perform('mappingPrepare',prep,input('mapping','prepare',2,{entries,evidence:'Conserva19 correspondências anteriores e acrescenta as cinco contas artificiais de activos explicitamente aprovadas.'}));ok(m1.version===3,'new mapping prepared version3');const m2=await perform('mappingReview',review,input('mapping','review',3,{decision:'approved'}));ok(m2.version===4,'new mapping independently reviewed version4');const r1=await perform('resultPrepare',prep,input('result','prepare',2,{mapping_id:m2.id,from_on:'2026-01-01',to_on:'2026-11-11',per_page:25,evidence:'Resultado interno parcial anual até11Nov;29 lançamentos QA incluindo admissão1200/quota100 e dez contas por classificar.'}));const r2=await perform('resultReview',review,input('result','review',3,{decision:'approved'}));ok(r1.version===3&&r2.version===4,'new result independently prepared reviewed versions3/4');const query={view:'result',version_id:r2.id,page:1,per_page:25},report=await get(query),summary=report.snapshot.summary,c=summary.currencies.MZN;ok(summary.plan.accounts===24&&summary.accounts===23&&summary.basis.journals===29&&report.current_source_matches,'24 chart accounts23 accounts with movements29 journals current source');ok(c.groups.asset.presentation_minor==='325500'&&c.groups.liability.presentation_minor==='10000','classified assets3255 supplier100');ok(c.groups.expense.presentation_minor==='230000'&&c.known_result_subtotal_minor==='28621','year-to-date expense2300 known subtotal286.21');ok(c.unclassified_accounts===10&&c.complete_result_minor===null&&c.equity_minor===null&&!c.complete_balance_sheet,'ten unclassified complete result and equity unknown');const larger=await get({...query,per_page:100});ok(JSON.stringify(larger.records.data)===JSON.stringify(report.records.data),'same frozen23 movement rows pages25/100');
const old=JSON.parse(fs.readFileSync(root+'/statements-http-progress.json')).resultReview.result;let response=await read.get(base+'/exports/'+old.id);const oldBytes=Buffer.from(await response.arrayBuffer());ok(response.status===200&&oldBytes.equals(fs.readFileSync(root+'/statements-frozen-export-private.csv')),'old reviewed export byte identical');const historical=await get({...query,version_id:old.id});ok(historical.snapshot.summary.accounts===19&&historical.snapshot.summary.basis.journals===27&&historical.current_source_matches===false,'old19/27 snapshot explicit historical');response=await read.get(base+'/exports/'+r2.id);const bytes=Buffer.from(await response.arrayBuffer());ok(response.status===200&&crypto.createHash('sha256').update(bytes).digest('hex')===report.snapshot.csv_sha256,'new partial result export and SHA');fs.writeFileSync(root+'/assets-statements-frozen-private.csv',bytes);fs.writeFileSync(root+'/assets-statements-sample-private.json',JSON.stringify(report));const proof=root+'/assets-statements-http-20261111.json';fs.writeFileSync(proof,JSON.stringify({passed:true,checks,administrative_commands:4,old_snapshot_preserved:true,new_demo:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof}));
+11
View File
@@ -0,0 +1,11 @@
import fs from'node:fs';import assert from'node:assert/strict';import{pathToFileURL}from'node:url';import{transformWithOxc}from'vite';const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);};let slots=[],cursor=0,effects=[],tree,calls=[],rootComponent,props;const same=(a,b)=>a&&b&&a.length===b.length&&a.every((v,i)=>v===b[i]);const R={createElement:(type,props,...children)=>({type,props:props??{},children}),Fragment:Symbol('fragment'),Suspense:Symbol('suspense'),lazy:()=>Symbol('lazy'),useRef(initial){const i=cursor++;slots[i]??={value:{current:initial}};return slots[i].value;},useState(initial){const i=cursor++;slots[i]??={value:initial};return[slots[i].value,v=>{slots[i].value=typeof v==='function'?v(slots[i].value):v;}];},useMemo(fn,deps){const i=cursor++;if(!same(slots[i]?.deps,deps))slots[i]={value:fn(),deps};return slots[i].value;},useEffect(fn,deps){const i=cursor++;if(!same(slots[i]?.deps,deps)){slots[i]?.cleanup?.();slots[i]={deps};effects.push(()=>slots[i].cleanup=fn());}}};globalThis.__hrHooks=R;const stub='const r=globalThis.__hrHooks;export default r;export const useState=r.useState,useMemo=r.useMemo,useEffect=r.useEffect,useRef=r.useRef;',reactUrl='data:text/javascript;base64,'+Buffer.from(stub).toString('base64'),compile=async(file,imports)=>{let s=fs.readFileSync('resources/js/'+file,'utf8').replace(/from\s*['"]react['"]/, 'from '+JSON.stringify(reactUrl));for(const f of imports)s=s.replace(new RegExp('from\\s*[\'"]\\./'+f.replaceAll('.','\\.')+'[\'"]'),'from '+JSON.stringify(pathToFileURL(process.cwd()+'/resources/js/'+f).href));const c=await transformWithOxc(s,file,{jsx:{runtime:'classic'}});return import('data:text/javascript;base64,'+Buffer.from(c.code).toString('base64'));};
const{default:Workspace}=await compile('FixedAssetsWorkspace.jsx',[]);const{navigationGroups}=await compile('WorkspaceNavigation.jsx',['billing-menu.js','workshop-menu.js','stock-menu.js','accounting-menu.js']);const{moduleGroups}=await compile('ModuleNavigation.jsx',[]);
const row={id:7,kind:'asset',record_key:'00000000-0000-4000-8000-000000000007',version:2,state:'approved',actor_id:16,created_at:'QA',reason:'Revisão artificial',data:{status:'active',input:{code:'QA-UI-ASSET',description:'Fixture UI',origin_reference:'QA-UI-OPEN',cost:'1200.00',residual:'0.00',available_on:'2026-10-01',location:'QA-A',custodian:'QA-A',evidence:'Evidência artificial de UI'}},effective:null};row.effective={...row};let fail=true,posted=[],previewCalls=[];
const api=async(path,method,body,unused,key)=>{calls.push({path,method,body,key});if(method==='POST'&&path.endsWith('/preview')){previewCalls.push({body,key});return{preview:'opaque',impact:{before:{version:0},after:{version:1,state:'prepared',proposal:{input:body.data}}}};}if(method==='POST'){posted.push({path,body,key});if(fail){const e=Error('Resultado incerto503');e.status=503;throw e;}return{state:'prepared',version:1};}if(path.includes('/receipts/'))return{result:{state:'prepared',version:1}};if(path.includes('/ledger?'))return{balance:{cost_minor:120000,accumulated_minor:10000,carrying_minor:110000,disposed:false},records:{data:[],last_page:1},corrections:[]};const q=new URL('http://qa'+path).searchParams;return{kind:q.get('kind'),records:{data:q.get('kind')==='asset'?[row]:[],per_page:Number(q.get('per_page')),total:26,last_page:2}};};
const render=()=>{cursor=0;tree=rootComponent(props);const jobs=effects;effects=[];jobs.forEach(f=>f());return tree;},tick=async()=>{await new Promise(r=>setTimeout(r,0));render();},descend=n=>Array.isArray(n)?n.flatMap(descend):n&&typeof n==='object'?[n,...descend(n.children)]:[],nodes=(n=tree)=>descend(n),text=n=>Array.isArray(n)?n.map(text).join(''):n&&typeof n==='object'?text(n.children):String(n??''),find=(tag,label)=>nodes().find(n=>n.type===tag&&text(n).includes(label));
rootComponent=Workspace;props={api,context:{token:'qa',unit:{id:2}},can:()=>true,busy:false,perform:async(fn)=>{try{await fn();}catch{}}};render();await tick();ok(calls.length===1&&calls[0].path.includes('kind=asset'),'bounded initial asset list only');ok(!calls.some(c=>c.path.includes('/ledger')),'no implicit first asset selection');find('button','Seguinte').props.onClick();render();await tick();ok(calls.at(-1).path.includes('page=2'),'page changes server query');find('button','QA-UI-ASSET').props.onClick();render();await tick();ok(calls.at(-1).path.includes('asset_key='+row.record_key),'ledger only for explicit asset');ok(text(tree).includes('Diário do activo seleccionado'),'asset and GL integrated detail');find('button','Novo registo').props.onClick();render();ok(!find('h2','Versão seleccionada'),'new record clears old selection');const field=(label,value)=>{find('label',label).children.find(n=>['input','textarea'].includes(n?.type)).props.onChange({target:{value}});render();};field('Código QA','QA-NEW-UI');field('Descrição','Equipamento artificial UI');field('Referência da abertura','QA-NEW-OPEN');field('Custo MZN','1200.00');field('Residual MZN','0.00');field('Entrada em uso','2026-10-01');field('Localização interna','QA-SALA-A');field('Custódia interna','QA-CUSTODIA-A');field('Evidência','Evidência artificial UI');field('Fundamento da decisão','Fundamento artificial UI');render();await nodes().find(n=>n.type==='form').props.onSubmit({preventDefault(){}});render();ok(previewCalls.length===1&&previewCalls[0].body.data.cost==='1200.00'&&previewCalls[0].body.version===0,'exact documentary amount and explicit first version');ok(Boolean(find('h2','Impacto antes da confirmação')),'impact before commit');ok(find('button','Planos mensais').props.disabled,'preview blocks navigation');await find('button','Confirmar só este impacto').props.onClick();render();ok(find('button','Planos mensais').props.disabled&&Boolean(find('button','Recuperar comprovativo')),'uncertain failure retains same conference');await find('button','Recuperar comprovativo').props.onClick();render();await tick();ok(posted.length===1&&calls.some(c=>c.path.endsWith('/receipts/'+previewCalls[0].key)),'receipt recovery does not duplicate POST');find('button','Planos mensais').props.onClick();render();await tick();const picker=nodes().find(n=>typeof n.type==='function'&&n.type.name==='ReferencePicker');ok(picker&&picker.props.value==='','dependent asset explicitly unselected');const groups=navigationGroups(r=>r.startsWith('fixed_assets.'),2,{fixedAssets:true});ok(moduleGroups(groups,'accounting').some(g=>g.items.some(i=>i.id==='fixed_assets')),'accounting dropdown with exact asset right');ok(moduleGroups(groups,'finance').length===0&&moduleGroups(groups,'human_resources').length===0,'asset right adds no other module');const hidden=navigationGroups(()=>false,2,{fixedAssets:true});ok(!hidden.flatMap(g=>g.items).some(i=>i.id==='fixed_assets'),'no asset menu without permission');
const file=root+'/assets-ui-20261111.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,api_mocked:true,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
@@ -0,0 +1,9 @@
<?php
require __DIR__.'/sgf-assets-proof-bootstrap.php';$host->beginTransaction();$native->beginTransaction();try{
$keys=[];foreach(['asset','plan','charge','disposal']as$k)$keys[$k]=(string)Str::uuid();$a=['operation'=>'establish','code'=>'QA-WRITEOFF-ROLLBACK','description'=>'Activo artificial para abate100 e1100 revertido','origin_reference'=>'QA-WRITEOFF-OPEN-ROLLBACK','cost'=>'1200.00','residual'=>'0.00','available_on'=>'2026-10-01','location'=>'QA-SALA-A','custodian'=>'QA-CUSTODIA-A','evidence'=>'Abate100 acumulado e1100 perda, fixture revertida.'];$admin=function($kind,$input)use($faCommand,$faInput,$keys){$faCommand(15,$faInput($kind,$keys[$kind],0,$input));return$faCommand(16,$faInput($kind,$keys[$kind],1,['decision'=>'approved'],'review'))[0];};$gl=function($kind,$id,$on,$rule)use($call){$p=$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>$kind,'source_id'=>$id,'journal_on'=>$on,'period_key'=>'QA-2026-INTEGRADO','rule_key'=>$rule,'reason'=>'Conferência de activo no recorte QA revertido.'],(string)Str::uuid()));$call('accounting_review','validate',fn($e)=>$e->command('validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Revisão distinta do activo no recorte QA revertido.'],(string)Str::uuid()));return$call('accounting_prepare','post',fn($e)=>$e->command('post',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Aplicação do activo no recorte QA revertido.'],(string)Str::uuid()));};$ar=$admin('asset',$a);$gl('assets.admitted',$ar['id'],'2026-10-01','QA-ASSET-ADMISSION');$admin('plan',['operation'=>'establish','asset_key'=>$keys['asset'],'life_months'=>12,'status'=>'active','evidence'=>'Plano linear artificial de12 meses para abate.']);$cr=$admin('charge',['operation'=>'establish','asset_key'=>$keys['asset'],'plan_key'=>$keys['plan'],'month'=>'2026-10','evidence'=>'Quota100 artificial Outubro para abate.']);$gl('assets.depreciation',$cr['id'],'2026-10-31','QA-ASSET-DEPRECIATION');$dr=$admin('disposal',['operation'=>'establish','asset_key'=>$keys['asset'],'effect_on'=>'2026-11-01','evidence'=>'Abate artificial sem contraprestação, saldo1100.']);$dj=$gl('assets.writeoff',$dr['id'],'2026-11-01','QA-ASSET-WRITEOFF');$lines=$native->table('gl_lines')->where('journal_id',$dj['journal_id'])->orderBy('line_no')->get();$ok($lines->count()===3&&$lines[0]->debit_minor===10000&&$lines[1]->debit_minor===110000&&$lines[2]->credit_minor===120000,'specified writeoff accumulated100 loss1100 cost1200');foreach($native->table('gl_journals')->where('snapshot->source->asset_key',$keys['asset'])->pluck('id')as$id)$native->select('SELECT gl_assert_balanced(?)',[$id]);$ok(true,'all three synthetic journals balanced');
// Isolate this asset conference from unrelated financial fixtures in the regression harness only.
// Neither production bridge nor existing financial source mappings are changed.
$concurrency=json_decode(file_get_contents($s['root'].'/assets-concurrency-1794388203631/cuts-private.json'),true);$r=$host->table('fa_versions')->where('record_key',$concurrency['record_key'])->where('state','approved')->first();$d=json_decode($r->data,true)['input'];$d['operation']='cancel';$faCommand(15,$faInput('asset',$r->record_key,2,$d));$faCommand(16,$faInput('asset',$r->record_key,3,['decision'=>'approved'],'review'));
$run=function($role,$right,$command,$input)use($call,$native){return$call($role,$right,function($e,$actor,$b,$o,$scope,$stock,$hostRead)use($native,$command,$input){(new ReflectionProperty($e,'financialScope'))->setValue($e,$scope);(new ReflectionProperty($e,'additionalPeriod'))->setValue($e,fn($p)=>$hostRead(fn()=>app(App\Modules\Accounting\FixedAssetLedgerSource::class)->period(App\Models\User::findOrFail($o['sgf_host_user']),$p,$native,$scope)));return$e->command($command,$input,(string)Str::uuid());});};
$p=$run('accounting_prepare','period_prepare','period-prepare',['period_key'=>'QA-2026-INTEGRADO','reason'=>'Conferência do recorte de activos QA no harness revertido.']);$run('accounting_review','period_validate','period-validate',['check_id'=>$p['check_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Revisão distinta do recorte de activos no harness.']);$out=$run('accounting_prepare','period_close','period-close',['check_id'=>$p['check_id'],'revision'=>$p['revision'],'reason'=>'Fecho apenas do recorte sintético de activos revertido.']);$ok($out['state']==='closed','asset scope period can close after distinct review');$ok($native->table('gl_periods')->where($f['scope'])->orderByDesc('version')->value('status')==='closed','closed version observable inside rollback');
}finally{$native->rollBack();$host->rollBack();}$ok($native->table('gl_periods')->where($f['scope'])->orderByDesc('version')->value('status')==='open','regression period remains open after rollback');file_put_contents($work.'/assets-writeoff-close.json',json_encode(['passed'=>true,'checks'=>$checks,'regression_only'=>true,'all_fixtures_rolled_back'=>true,'period_harness'=>'asset-only additional period callback and qualified empty financial fixture scope; not full application closure','real_http_closure'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/assets-writeoff-close.json']).PHP_EOL;
@@ -0,0 +1 @@
import fs from'node:fs';import assert from'node:assert/strict';import{session}from'./sgf-single-http.mjs';const checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);},s=await session('sgf:finance_review','accounting');let r=await s.get('/api/service-fiscal');ok(r.status===200&&r.headers.get('cache-control').includes('no-store'),'existing authorized fiscal reader private response');const d=await r.json(),c=d.coverage;ok(c.version==='SGF-FISCAL-COVERAGE-v3'&&c.taxes.length===7,'updated seven-family capability metadata');for(const id of['irps','inss']){const t=c.taxes.find(t=>t.id===id);ok(t.state==='partial'&&t.available&&t.legal_use===false,'actual internal coverage and legal gap '+id);}ok(c.management.some(m=>m.id==='human_resources'&&m.available.includes('férias')&&m.available.includes('autónomo')),'HR autonomous delivered capabilities visible');ok(c.management.find(m=>m.id==='accounting').pending.includes('Activos')&&!c.legal_validation&&!c.official_submission_enabled,'assets and legal activation remain pending');ok(!JSON.stringify(c).includes('QA-COLABORADOR-01')&&Array.isArray(d.proposals)&&d.migration_pending===false,'metadata does not reveal worker records or alter fiscal proposal list');const hr=await session('hr:hr_read','human_resources');r=await hr.get('/api/service-fiscal');ok([403,409].includes(r.status),'metadata reconciliation grants no fiscal access to HR');const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',file=root+'/coverage-reconciliation-http-20261111.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,business_commands:0,new_grants:0,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
@@ -0,0 +1,3 @@
<?php
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$baseline=$s['root'].'/single-integration/20261111-103940-12c6c0e7';$before=json_decode(file_get_contents($baseline.'/preservation-private.json'),true);$installation=json_decode(file_get_contents($baseline.'/coverage-reconciliation-installation.json'),true);$same=[];$order=fn($t)=>in_array($t,['sr_outbox','sr_deliveries'],true)?'event_id':(in_array($t,['sc_catalogue_meta','pd_due_meta','fo_meta','fs_meta','rm_meta','hp_meta'],true)?'policy':(in_array($t,['hr_qa_profiles','sgf_presentation_profiles'],true)?'profile_key':(in_array($t,['fs_results','fs_result_lines'],true)?'snapshot_key':($t==='company_memberships'?'user_id':'id'))));foreach($before as$name=>$tables){$db=$name==='host'?$host:$native;foreach($tables as$table=>$json){if($db->table($table)->orderBy($order($table))->when($table==='fs_result_lines',fn($q)=>$q->orderBy('account_id'))->get()->toJson()!==$json)throw new LogicException('Accepted facts changed '.$table);$same[]=$name.'.'.$table;}}
foreach($installation['hashes']as$file=>$sha)if(hash_file('sha256',$s['app'].'/'.$file)!==$sha)throw new LogicException('Installed file changed');$older=json_decode(file_get_contents(json_decode(file_get_contents($s['root'].'/remunerations-backup-pointer.json'),true)['root'].'/preservation-private.json'),true);foreach(['binding'=>'binding.json','single_binding'=>'single-bindings.json','stock_binding'=>'stock-binding.json']as$key=>$file)if(hash_file('sha256',$s['root'].'/'.$file)!==$older[$key])throw new LogicException('Binding changed');if($host->table('hp_versions')->count()!==16||$host->table('hp_commands')->count()!==16||$host->table('rm_versions')->count()!==6||$native->table('gl_journals')->count()!==27)throw new LogicException('Original counts changed');file_put_contents($work.'/coverage-reconciliation-preservation.json',json_encode(['passed'=>true,'identical_original_tables'=>$same,'all_installed_hashes_verified'=>true,'three_bindings_unchanged'=>true,'business_commands_8085'=>0,'new_grants'=>0,'new_profiles'=>0,'new_schema'=>false,'cache_cleared'=>false,'protected_six_targeted'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'identical_tables'=>count($same),'proof'=>$work.'/coverage-reconciliation-preservation.json']).PHP_EOL;
@@ -0,0 +1,2 @@
<?php
require __DIR__.'/sgf-single-bootstrap.php';$c=App\Modules\Services\FiscalCoverage::describe();$checks=[];$ok=function($v,$n)use(&$checks){if(!$v)throw new LogicException($n);$checks[]=$n;};$tax=collect($c['taxes'])->keyBy('id');foreach(['irps','inss']as$key){$ok($tax[$key]['state']==='partial'&&is_string($tax[$key]['available']),'existing internal capability acknowledged '.$key);$ok($tax[$key]['legal_use']===false&&preg_match('/leg(?:al|ais)/u',$tax[$key]['pending'])===1,'legal gap preserved '.$key);}$management=collect($c['management'])->keyBy('id');$ok(str_contains($management['human_resources']['available'],'autónomo')&&str_contains($management['human_resources']['available'],'férias'),'autonomous HR delivered operations in coverage');$ok(str_contains($management['accounting']['available'],'parciais')&&str_contains($management['accounting']['pending'],'Activos'),'partial statements present assets explicitly absent');$ok($tax['irpc']['state']==='pending'&&$tax['ice']['state']==='pending'&&!$c['legal_validation']&&!$c['official_submission_enabled'],'no unimplemented engine or legal activation inferred');file_put_contents($work.'/coverage-reconciliation.json',json_encode(['passed'=>true,'checks'=>$checks,'database_writes'=>0,'new_grants'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/coverage-reconciliation.json']).PHP_EOL;
@@ -0,0 +1,4 @@
<?php
require __DIR__.'/sgf-hr-personnel-proof-bootstrap.php';$n=$host->table('hp_versions')->count();$host->beginTransaction();try{$contract=$host->table('hp_versions')->where(['kind'=>'contract','state'=>'approved'])->first();$real=$host->table('hp_versions')->where(['kind'=>'entitlement','state'=>'approved'])->first();$source=json_decode($real->data,true);$unknownKey=null;for($i=0;$i<26;$i++){$key=$uuid();if($i===0)$unknownKey=$key;$data=$source;$data['input']['minutes']=$i===0?null:60;$data['result']=['available_minutes'=>$i===0?null:60];$host->table('hp_versions')->insert(['tenant_id'=>1,'company_id'=>2,'unit_id'=>2,'kind'=>'entitlement','record_key'=>$key,'version'=>1,'action'=>'review','state'=>'approved','prepared_by'=>$hrActor(13),'actor_id'=>$hrActor(14),'preparer_identity'=>str_repeat('0',64),'data'=>json_encode($data),'source_basis'=>str_repeat('0',64),'impact'=>'{}','reason'=>'Fixture SQL de consulta conjunta de saldos, sem condição laboral real.','created_at'=>now()]);}
$counts=[];foreach([25,100]as$size){$host->enableQueryLog();$host->flushQueryLog();$page=$hpList('entitlement',['worker_key'=>$workerKey,'per_page'=>$size,'page'=>1]);$queries=$host->getQueryLog();$host->disableQueryLog();$counts[$size]=count(array_filter($queries,fn($r)=>str_contains($r['query'],'"hp_versions"')));$ok($counts[$size]===4,'four personnel SQL projections independent of page'.$size);$ok(count($page['records']['data'])===min($size,27),'correct current balance page'.$size);if($size===100){$rows=collect($page['records']['data'])->keyBy('record_key');$ok($rows[$real->record_key]['current_balance']['available_minutes']===0&&$rows[$real->record_key]['current_balance']['reserved_minutes']===480,'actual reviewed reservation projected into current balance');$ok($rows[$real->record_key]['data']['result']['available_minutes']===480,'conserved original condition result unchanged');$ok($rows[$unknownKey]['current_balance']['available_minutes']===null,'unknown initial balance not zero in projection');}}
}finally{$host->rollBack();}$ok($host->table('hp_versions')->count()===$n,'all26 read-only volume fixtures rolled back');file_put_contents($work.'/hp-current-balances.json',json_encode(['passed'=>true,'checks'=>$checks,'sql_counts'=>$counts,'all_fixtures_rolled_back'=>true,'not_load_benchmark'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/hp-current-balances.json']).PHP_EOL;
@@ -0,0 +1 @@
import fs from'node:fs';import assert from'node:assert/strict';import crypto from'node:crypto';const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',m=JSON.parse(fs.readFileSync(root+'/app/public/build/manifest.json')),checks=[];for(const e of Object.values(m).filter(e=>e.isEntry||/PersonnelWorkspace|RemunerationsWorkspace/.test(e.file))){const r=await fetch('http://127.0.0.1:8085/build/'+e.file),b=Buffer.from(await r.arrayBuffer());assert(r.status===200&&/javascript/.test(r.headers.get('content-type'))&&crypto.createHash('sha256').update(b).digest('hex')===crypto.createHash('sha256').update(fs.readFileSync(root+'/app/public/build/'+e.file)).digest('hex'));checks.push('latest served MIME SHA '+e.file);}const file=root+'/hp-final-assets-20261111.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,business_commands:0},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
+1
View File
@@ -0,0 +1 @@
import fs from'node:fs';import assert from'node:assert/strict';import crypto from'node:crypto';import{session}from'./sgf-single-http.mjs';const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',p=JSON.parse(fs.readFileSync(root+'/hp-http-progress-private.json')),checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);},s=await session('hr:hr_read','human_resources');let r=await s.get('/api/hr-personnel?'+new URLSearchParams({kind:'entitlement',worker_key:p.worker_key,page:1,per_page:25}));assert.equal(r.status,200);const body=await r.json(),ent=body.records.data.find(r=>r.record_key===p.keys.entitlement);ok(ent.current_balance.available_minutes===480&&ent.current_balance.taken_minutes===480&&ent.current_balance.reserved_minutes===0,'current condition balance reflects actual effective requests');ok(ent.data.result.available_minutes===960&&ent.data.result.reserved_minutes===0,'original reviewed condition snapshot remains960');ok(ent.current_balance.basis==='current_effective_requests_at_read','current balance basis explicit');r=await s.get('/api/hr-personnel/exports/'+p.steps['report-review'].result.id);assert.equal(r.status,200);ok(Buffer.from(await r.arrayBuffer()).equals(fs.readFileSync(root+'/hp-frozen-period-private.csv')),'frozen period export unchanged after current-balance projection');const entry=await fetch('http://127.0.0.1:8085/sgf'),html=await entry.text();for(const m of['commercial','purchasing','stock','workshop','finance','accounting','human_resources'])ok(html.includes('data-presentation-module="'+m+'"'),'entry preserved '+m);ok(html.includes('Cadastro, contratos, assiduidade, férias'),'HR entry describes delivered functions');const manifest=JSON.parse(fs.readFileSync(root+'/app/public/build/manifest.json'));for(const e of Object.values(manifest).filter(v=>v.isEntry||/PersonnelWorkspace|RemunerationsWorkspace|WorkspaceHome|statement-download/.test(v.file))){r=await fetch('http://127.0.0.1:8085/build/'+e.file);const b=Buffer.from(await r.arrayBuffer());ok(r.status===200&&/javascript/.test(r.headers.get('content-type'))&&crypto.createHash('sha256').update(b).digest('hex')===crypto.createHash('sha256').update(fs.readFileSync(root+'/app/public/build/'+e.file)).digest('hex'),'final JS MIME SHA '+e.file);if(e.file.includes('PersonnelWorkspace'))ok(b.toString().includes('Saldo efectivo nesta consulta'),'served interface distinguishes current from frozen balance');}const file=root+'/hp-final-http-20261111.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,business_commands:0,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
+1
View File
@@ -0,0 +1 @@
import fs from'node:fs';import assert from'node:assert/strict';import{pathToFileURL}from'node:url';import{transformWithOxc}from'vite';let slots=[],cursor=0;const R={createElement:(type,props,...children)=>({type,props:props??{},children}),Fragment:Symbol('fragment'),useState:initial=>{const i=cursor++;return[slots[i]===undefined?initial:slots[i],()=>{}];},useEffect(){},useRef:value=>({current:value}),useMemo:fn=>fn()};globalThis.__finalPersonnel=R;const stub='const r=globalThis.__finalPersonnel;export default r;export const useState=r.useState,useEffect=r.useEffect,useRef=r.useRef,useMemo=r.useMemo;',u='data:text/javascript;base64,'+Buffer.from(stub).toString('base64');let s=fs.readFileSync('resources/js/PersonnelWorkspace.jsx','utf8').replace(/from\s*['"]react['"]/, 'from '+JSON.stringify(u));for(const f of['workshop-stock-operation.js','statement-download.js'])s=s.replace(new RegExp('from\\s*[\'"]\\./'+f.replaceAll('.','\\.')+'[\'"]'),'from '+JSON.stringify(pathToFileURL(process.cwd()+'/resources/js/'+f).href));const c=await transformWithOxc(s,'PersonnelWorkspace.jsx',{jsx:{runtime:'classic'}}),{default:Workspace,PersonnelResult}=await import('data:text/javascript;base64,'+Buffer.from(c.code).toString('base64')),text=n=>Array.isArray(n)?n.map(text).join(''):n&&typeof n==='object'?text(n.children):String(n??''),checks=[];const worker={record_key:'QA-W',version:2,data:{input:{code:'QA-W'}}},row={id:1,record_key:'QA-E',version:2,kind:'entitlement',state:'approved',actor_id:19,created_at:'QA',reason:'Fixture',data:{input:{worker_key:'QA-W',valid_from:'2026-11-01',valid_to:'2026-12-31',minutes:960,evidence:'Fixture'},status:'active',dependencies:{worker:{code:'QA-W'}},result:{available_minutes:960,reserved_minutes:0,taken_minutes:0}},current_balance:{available_minutes:480,reserved_minutes:0,taken_minutes:480},impact:{before:{version:1,state:'prepared'},after:{version:2,state:'approved'}}};row.effective={version:2,data:row.data};slots=[worker,{kind:'entitlement',worker_key:'QA-W',page:1,per_page:25},{search:'',period:'',state:''},{records:{data:[row],current_page:1,last_page:1,total:1},capabilities:{prepare:false,review:false}},row,null,null,false,'','','',null];const tree=Workspace({api:()=>{throw Error('No API expected');},context:{token:'fixed'}});assert(text(tree).includes('Saldo efectivo nesta consulta:')&&text(tree).includes('disponível8h 0min'));checks.push('actual selected-condition handler renders current480 balance separately from conserved snapshot');const refusal={...row.data,input:{...row.data.input,date:'2026-11-10'},status:'reserved',result:{minutes:480,balance_after:{available_minutes:480,reserved_minutes:480,taken_minutes:0}},refusal_conference:{original_source_changed:true,current_balance:{available_minutes:0,reserved_minutes:480}}};const result=PersonnelResult({kind:'leave',data:refusal});assert(text(result).includes('A proposta não foi aplicada')&&text(result).includes('Saldo efectivo conferido: disponível0h 0min'));checks.push('actual result helper shows refusal source change and current insufficient balance without application');const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',file=root+'/hp-final-ui-20261111.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,react_hooks_simulated:true,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
+2
View File
@@ -0,0 +1,2 @@
<?php
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$file='docs/integration/sgf-human-resources-personnel-access-proposal.json';$old=json_decode(file_get_contents($s['app'].'/'.$file),true);$new=json_decode(file_get_contents($repo.'/'.$file),true);$hash=fn($p)=>hash('sha256',Tempo\Finance\Payables\Payables::canonical(['scope'=>$p['scope'],'grants'=>$p['grants_per_book']]));if($hash($old)!==$hash($new))throw new LogicException('Do not change reviewed grants at finalization');copy($s['app'].'/'.$file,$work.'/access-policy-before-private.json');copy($repo.'/'.$file,$s['app'].'/'.$file);$installation=json_decode(file_get_contents($s['root'].'/single-integration/20261111-090956-998c70ef/hp-installation.json'),true);foreach($installation['hashes']as$path=>$sha){if($path===$file)$sha=hash_file('sha256',$repo.'/'.$file);if(hash_file('sha256',$s['app'].'/'.$path)!==$sha)throw new LogicException('Installed source changed '.$path);}file_put_contents($work.'/hp-finalization.json',json_encode(['passed'=>true,'installed_files_verified'=>count($installation['hashes']),'access_pin_unchanged'=>true,'metadata_status_updated'=>true,'new_grants'=>0,'business_commands'=>0,'cache_cleared'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'proof'=>$work.'/hp-finalization.json']).PHP_EOL;
+15
View File
@@ -0,0 +1,15 @@
import fs from'node:fs';import assert from'node:assert/strict';import crypto from'node:crypto';import{session}from'./sgf-single-http.mjs';
const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',file=root+'/hp-http-progress-private.json',progress=fs.existsSync(file)?JSON.parse(fs.readFileSync(file)):{keys:Object.fromEntries(['contract','day9','day10','entitlement','leave','report'].map(k=>[k,crypto.randomUUID()])),steps:{},checks:[]},save=()=>fs.writeFileSync(file,JSON.stringify(progress)),checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);if(!progress.checks.includes(n)){progress.checks.push(n);save();}};save();
const prep=await session('hr:hr_prepare','human_resources'),review=await session('hr:hr_review','human_resources'),reader=await session('hr:hr_read','human_resources');for(const[role,s,count]of[['prepare',prep,10],['review',review,10],['read',reader,6]]){ok(s.identity.context.modules.map(m=>m.id).join(',')==='human_resources','only HR for '+role);ok(s.identity.context.grants.length===count,'exact approved total grant count '+role);for(const area of['contracts','attendance','leave'])ok(s.identity.context.grants.some(g=>g.permission==='human_resources.'+area+'.view'&&g.unit_id===2),'own scoped '+area+' view '+role);ok(s.identity.context.hr_personnel_enabled===true,'personnel available '+role);}
const get=async(q,s=reader)=>{const r=await s.get('/api/hr-personnel?'+new URLSearchParams(q));assert.equal(r.status,200);return r.json();};let r=await reader.get('/api/remunerations?kind=worker&per_page=100');assert.equal(r.status,200);const workers=await r.json(),worker=workers.records.data.find(r=>r.data.input.code==='QA-COLABORADOR-01'&&r.effective);assert(worker);const wk=worker.record_key;ok(progress.worker_key===undefined||progress.worker_key===wk,'existing worker chosen explicitly exact');progress.worker_key=wk;save();
const input=(kind,key,version,data,action='prepare')=>({kind,record_key:key,version,action,reason:'Conferência individual QA de pessoal, sem efeito financeiro ou documento legal.',data});
async function run(step,s,body){let p=progress.steps[step];if(p?.result){assert.deepEqual(p.body,body);ok(true,'accepted decision reused '+step);return p.result;}p??={key:crypto.randomUUID(),body};assert.deepEqual(p.body,body);progress.steps[step]=p;save();const receipt=await s.get('/api/hr-personnel/receipts/'+p.key);if(receipt.status===200){p.result=(await receipt.json()).result;save();ok(true,'uncertain result recovered by receipt '+step);return p.result;}assert.equal(receipt.status,404);const preview=await s.command('/api/hr-personnel/preview',body,p.key);assert.equal(preview.status,200,JSON.stringify(preview.value));ok(preview.value.impact.journals===0&&preview.value.impact.payments===0&&!preview.value.impact.debt_created,'preview no financial effects '+step);p.preview=preview.value.preview;save();const out=await s.command('/api/hr-personnel/confirm',{preview:p.preview},p.key);assert.equal(out.status,200,JSON.stringify(out.value));p.result=out.value;save();ok(out.value.version===body.version+1&&out.value.state===(body.action==='prepare'?'prepared':'approved'),'persisted individual version '+step);const got=await s.get('/api/hr-personnel/receipts/'+p.key);assert.equal(got.status,200);ok((await got.json()).result.id===out.value.id,'own receipt '+step);return out.value;}
const ranges=[{from:480,to:720},{from:780,to:1020}],evidence='Exercício fictício de RH, sem contrato legal ou efeito salarial.',ck=progress.keys.contract,dk9=progress.keys.day9,dk10=progress.keys.day10,ek=progress.keys.entitlement,lk=progress.keys.leave,rk=progress.keys.report;
const contract={operation:'establish',worker_key:wk,reference:'QA-CONTRATO-01',mode:'fixed',valid_from:'2026-11-01',valid_to:'2026-12-31',gross:'10000.00',evidence},day=(date,actual,conferred)=>({operation:'establish',worker_key:wk,contract_key:ck,date,reference_on:'2026-11-11',planned:ranges,actual,leave_conferred:conferred,evidence}),ent={operation:'establish',worker_key:wk,contract_key:ck,valid_from:'2026-11-01',valid_to:'2026-12-31',minutes:960,evidence},leave={operation:'establish',worker_key:wk,contract_key:ck,entitlement_key:ek,day_key:dk10,date:'2026-11-10',reference_on:'2026-11-11',intervals:ranges,evidence};
for(const[name,kind,key,data,version]of[['contract','contract',ck,contract,0],['day9','day',dk9,day('2026-11-09',ranges,true),0],['day10','day',dk10,day('2026-11-10',null,false),0],['entitlement','entitlement',ek,ent,0],['leave','leave',lk,leave,0],['take','leave',lk,{...leave,operation:'take'},2],['day10-conference','day',dk10,{...day('2026-11-10',[],true),operation:'amend'},2],['report','report',rk,{operation:'establish',worker_key:wk,period:'2026-11',evidence},0]]){await run(name+'-prepare',prep,input(kind,key,version,data));await run(name+'-review',review,input(kind,key,version+1,{decision:'approved'},'review'));}
const list=await get({kind:'report',worker_key:wk,page:1,per_page:25}),row=list.records.data.find(r=>r.record_key===rk);assert(row);const result=row.data.result;ok(result.known_days===2&&result.known_conferred_days===2&&result.other_calendar_days_unknown===28&&!result.complete_month_proven,'actual two conferred days coverage partial month');const balance=result.leave_balances[0];ok(balance.entitlement_minutes===960&&balance.reserved_minutes===0&&balance.taken_minutes===480&&balance.available_minutes===480,'actual960 budget480taken480available no duplication');ok(result.days.every(d=>d.calculation.planned_minutes===480&&d.calculation.unaccounted_minutes===0),'two actual planned480 days no unaccounted after explicit conference');ok(result.days.find(d=>d.calculation.date==='2026-11-09').calculation.declared_minutes===480&&result.days.find(d=>d.calculation.date==='2026-11-10').calculation.declared_minutes===0,'actual work9 and explicit no work10 distinct');ok(result.payroll.length===1&&!result.payroll[0].contract_link_proven&&result.payroll[0].net_cents==='890000','original payroll not retrospectively contract linked or recalculated');
const large=await get({kind:'report',worker_key:wk,page:1,per_page:100});ok(JSON.stringify(large.records.data)===JSON.stringify(list.records.data),'real report25/100 parity');ok((await get({kind:'day',worker_key:wk,period:'2026-11',state:'approved',page:1,per_page:25})).records.total===2,'day person month state filters combined SQL');ok((await get({kind:'day',worker_key:wk,period:'2026-10',page:1,per_page:25})).records.total===0,'other month no fallback');ok((await get({kind:'contract',worker_key:wk,search:'QA-CONTRATO-01',page:1,per_page:25})).records.total===1,'exact contract reference search');ok((await get({kind:'contract',worker_key:wk,page:100000,per_page:25})).records.data.length===0,'far page no fallback');const hist=await get({kind:'leave',worker_key:wk,record_key:lk,page:1,per_page:25});ok(hist.records.data.map(r=>r.version).join(',')==='4,3,2,1','leave reserve and take history four immutable versions');const dh=await get({kind:'day',worker_key:wk,record_key:dk10,page:1,per_page:25});ok(dh.records.data.length===4&&dh.records.data.at(-1).data.input.actual===null&&Array.isArray(dh.records.data[0].data.input.actual),'unknown original day preserved after actual empty conference');
r=await reader.get('/api/hr-personnel/exports/'+row.id);assert.equal(r.status,200);const bytes=Buffer.from(await r.arrayBuffer());ok(/text\/csv/.test(r.headers.get('content-type'))&&r.headers.get('x-content-sha256')===row.data.artifact_sha256&&crypto.createHash('sha256').update(bytes).digest('hex')===row.data.artifact_sha256,'frozen period artifact MIME header and exact SHA');ok(bytes.toString().includes('NAO E DOCUMENTO LEGAL'),'export boundary explicit');fs.writeFileSync(root+'/hp-frozen-period-private.csv',bytes);
r=await reader.get('/api/remunerations?kind=payroll&per_page=25');assert.equal(r.status,200);const original=(await r.json()).records.data[0];r=await reader.get('/api/remunerations/exports/'+original.id);assert.equal(r.status,200);ok(Buffer.from(await r.arrayBuffer()).equals(fs.readFileSync(root+'/hr-frozen-export-private.csv')),'old salary frozen CSV byte-for-byte unchanged');
const denied=await reader.command('/api/hr-personnel/preview',input('contract',ck,2,{...contract,operation:'amend'}));ok(denied.status===403,'reader cannot prepare');const own=await prep.command('/api/hr-personnel/preview',input('contract',ck,2,{decision:'approved'},'review'));ok(own.status===403,'preparer cannot review');for(const q of[{kind:'contract',tenant_id:2},{kind:'day',per_page:26},{kind:'report',record_key:crypto.randomUUID()}]){r=await reader.get('/api/hr-personnel?'+new URLSearchParams(q));ok(r.status===(q.record_key?403:422),'invalid or foreign query refused '+JSON.stringify(q));}r=await reader.get('/api/hr-personnel/exports/2147483647');ok(r.status===403,'foreign export refused');r=await review.get('/api/hr-personnel/receipts/'+progress.steps['contract-prepare'].key);ok(r.status===404,'other actor receipt refused');const finance=await session('sgf:finance_prepare','finance');r=await finance.get('/api/hr-personnel?kind=contract');ok([403,409].includes(r.status),'finance cannot consult personnel');r=await reader.get('/api/cash');ok([403,409].includes(r.status),'RH no financial inheritance');r=await fetch('http://127.0.0.1:8085/api/hr-personnel?kind=contract',{headers:{Accept:'application/json'}});ok(r.status===401,'unauthenticated access refused');r=await reader.get('/api/hr-personnel/confirm',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({preview:'invalid'})});ok(r.status===419,'CSRF enforced');
const manifest=JSON.parse(fs.readFileSync(root+'/app/public/build/manifest.json'));for(const e of Object.values(manifest).filter(v=>v.isEntry||/PersonnelWorkspace|RemunerationsWorkspace|statement-download/.test(v.file))){r=await fetch('http://127.0.0.1:8085/build/'+e.file);const b=Buffer.from(await r.arrayBuffer());ok(r.status===200&&/javascript/.test(r.headers.get('content-type'))&&crypto.createHash('sha256').update(b).digest('hex')===crypto.createHash('sha256').update(fs.readFileSync(root+'/app/public/build/'+e.file)).digest('hex'),'servedJS MIME SHA '+e.file);}const output=root+'/hp-http-20261111.json';fs.writeFileSync(output,JSON.stringify({passed:true,checks,individual_commands:16,new_salary_versions:0,new_payments:0,new_journals:0,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:output}));
@@ -0,0 +1,6 @@
<?php
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$pointer=json_decode(file_get_contents($s['root'].'/hr-personnel-backup-pointer.json'),true);$before=json_decode(file_get_contents($pointer['root'].'/preservation-private.json'),true);$checks=[];$same=[];$append=[];$ok=function($v,$n)use(&$checks){if(!$v)throw new LogicException($n);$checks[]=$n;};$p=json_decode(file_get_contents($repo.'/docs/integration/sgf-human-resources-personnel-access-proposal.json'),true);$profiles=$host->table('hr_qa_profiles')->get()->keyBy('profile_key');$order=fn($t)=>in_array($t,['sr_outbox','sr_deliveries'],true)?'event_id':(in_array($t,['sc_catalogue_meta','pd_due_meta','fo_meta','fs_meta','rm_meta'],true)?'policy':(in_array($t,['hr_qa_profiles','sgf_presentation_profiles'],true)?'profile_key':(in_array($t,['fs_results','fs_result_lines'],true)?'snapshot_key':($t==='company_memberships'?'user_id':'id'))));
foreach(['host','native']as$name){$db=$name==='host'?$host:$native;foreach($before[$name]as$table=>$json){$rows=$db->table($table)->orderBy($order($table))->when($table==='fs_result_lines',fn($q)=>$q->orderBy('account_id'))->get();if($name==='host'&&$table==='permission_grants'){$old=collect(json_decode($json,true))->keyBy('id');$ok($rows->filter(fn($r)=>$old->has($r->id))->values()->toJson()===$json,'all old permissions identical');$new=$rows->reject(fn($r)=>$old->has($r->id))->values();$expected=[];foreach($p['grants_per_book']as$g)$expected[]=['user_id'=>$profiles[$g['profile_key']]->user_id,'tenant_id'=>1,'company_id'=>2,'unit_id'=>2,'permission'=>$g['permission']];$actual=$new->map(fn($r)=>['user_id'=>$r->user_id,'tenant_id'=>$r->tenant_id,'company_id'=>$r->company_id,'unit_id'=>$r->unit_id,'permission'=>$r->permission])->all();$sort=fn($a,$b)=>strcmp($a['user_id'].'/'.$a['permission'],$b['user_id'].'/'.$b['permission']);usort($expected,$sort);usort($actual,$sort);$ok($actual===$expected&&count($actual)===15,'only15 exact approved grants to existing HR profiles');$append[]='host.permission_grants';}else{$ok($rows->toJson()===$json,'original identical '.$name.'.'.$table);$same[]=$name.'.'.$table;}}}
$older=json_decode(file_get_contents(json_decode(file_get_contents($s['root'].'/remunerations-backup-pointer.json'),true)['root'].'/preservation-private.json'),true);foreach(['binding'=>'binding.json','single_binding'=>'single-bindings.json','stock_binding'=>'stock-binding.json']as$key=>$f)$ok(hash_file('sha256',$s['root'].'/'.$f)===$older[$key],$f.' unchanged from accepted pre-HR baseline');$originalSalary=json_decode($host->table('rm_versions')->where(['kind'=>'payroll','state'=>'approved'])->first()->data,true);$ok($originalSalary['artifact_csv']===file_get_contents($s['root'].'/hr-frozen-export-private.csv'),'old salary artifact remains byte-identical');
$versions=$host->table('hp_versions')->orderBy('id')->get();$ok($versions->count()===16&&$host->table('hp_commands')->count()===16,'exactly16 individual versions and receipts');$ok($versions->groupBy('kind')->map->count()->all()===['contract'=>2,'day'=>6,'entitlement'=>2,'leave'=>4,'report'=>2],'six sources with expected histories');$prepare=$profiles['hr_prepare']->user_id;$review=$profiles['hr_review']->user_id;$ok($versions->pluck('actor_id')->all()===array_merge(...array_fill(0,8,[$prepare,$review])),'separate preparer reviewer for all8 decisions');$report=json_decode($versions->where('kind','report')->last()->data,true);$ok($report['result']['known_days']===2&&!$report['result']['complete_month_proven'],'only two explicit days no complete month claim');$bal=$report['result']['leave_balances'][0];$ok($bal['available_minutes']===480&&$bal['taken_minutes']===480&&$bal['reserved_minutes']===0,'actual480 available480 taken0 reserved');$ok(file_get_contents($s['root'].'/hp-frozen-period-private.csv')===$report['artifact_csv']&&hash('sha256',$report['artifact_csv'])===$report['artifact_sha256'],'frozen period download equals conserved bytes');$ok($host->table('rm_versions')->count()===6&&$host->table('rm_commands')->count()===6,'original six salary versions and receipts unchanged');$ok($native->table('gl_journals')->count()===27&&$native->table('gl_accounts')->where(['tenant_id'=>1,'company_id'=>2,'unit_id'=>2])->count()===19,'27journals19accounts unchanged');$public=$host->select("SELECT c.relname FROM pg_class c CROSS JOIN LATERAL aclexplode(coalesce(c.relacl,CASE WHEN c.relkind='S' THEN acldefault('S',c.relowner) ELSE acldefault('r',c.relowner) END)) a WHERE c.relnamespace='public'::regnamespace AND c.relname IN('hp_meta','hp_versions','hp_commands','hp_versions_id_seq','hp_commands_id_seq') AND a.grantee=0");$ok($public===[],'no PUBLIC table or sequence grants');$pubfun=$host->select("SELECT a.* FROM pg_proc p CROSS JOIN LATERAL aclexplode(coalesce(p.proacl,acldefault('f',p.proowner))) a WHERE p.pronamespace='public'::regnamespace AND p.proname='hp_append_only' AND a.grantee=0");$ok($pubfun===[],'no PUBLIC append-only function access');
file_put_contents($work.'/hp-preservation.json',json_encode(['passed'=>true,'checks'=>$checks,'identical_original_tables'=>$same,'original_rows_preserved_with_authorized_append'=>$append,'new_grants'=>15,'new_profiles'=>0,'new_personnel_versions'=>16,'new_salary_versions'=>0,'new_journals'=>0,'cache_cleared'=>false,'protected_six_targeted'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'identical_original_tables'=>count($same),'proof'=>$work.'/hp-preservation.json']).PHP_EOL;
+7
View File
@@ -0,0 +1,7 @@
<?php
require __DIR__.'/sgf-hr-personnel-proof-bootstrap.php';$cuts=json_decode(file_get_contents($s['root'].'/hp-concurrency-1794380380400/cuts.json'),true);$before=$host->table('hp_versions')->count();$host->beginTransaction();try{
$foreign=$host->table('units')->where(['tenant_id'=>1,'company_id'=>2])->where('id','<>',2)->value('id');if(!$foreign)throw new LogicException('Existing other unit required');$r=(array)$host->table('hp_versions')->where('kind','contract')->first();unset($r['id']);$r['previous_id']=null;$r['version']=1;$r['record_key']=$uuid();$r['unit_id']=$foreign;$host->table('hp_versions')->insert($r);$visible=$hpList('contract',['worker_key'=>$workerKey]);$ok($visible['records']['total']===1,'foreign unit filtered before count');$reject(fn()=>$hpList('contract',['record_key'=>$r['record_key']]),403,'other unit history refused');$select(17);$id=$host->table('hp_versions')->where('record_key',$r['record_key'])->value('id');$reject(fn()=>$hp->export($hrActor(17),$id),403,'other unit export refused');$reject(fn()=>$hpList('contract',['per_page'=>26]),422,'page size whitelist enforced');$reject(fn()=>$hpList('contract',['worker_key'=>$uuid()]),409,'unavailable employee cannot be selected');
$host->beginTransaction();try{$host->table('permission_grants')->where(['user_id'=>$hrActor(13),'permission'=>'human_resources.leave.prepare'])->delete();$reject(fn()=>$hpPreview(14,$hpInput('leave',$cuts['b_key'],1,['decision'=>'approved'],'review')),403,'current preparer right revalidated on review');}finally{$host->rollBack();}
$host->beginTransaction();try{$host->table('permission_grants')->where(['user_id'=>$hrActor(17),'permission'=>'human_resources.attendance.view'])->delete();$reject(fn()=>$hpList('day'),403,'revoked view blocks read immediately');}finally{$host->rollBack();}
$select(17);$reject(fn()=>$hp->receipt($hrActor(17),$uuid()),404,'other or unknown technical receipt unavailable');
}finally{$host->rollBack();}$ok($host->table('hp_versions')->count()===$before,'foreign fixtures and rights changes rolled back');file_put_contents($work.'/hp-privacy.json',json_encode(['passed'=>true,'checks'=>$checks,'fixtures_rolled_back'=>true,'regression_only'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/hp-privacy.json']).PHP_EOL;
+2
View File
@@ -0,0 +1,2 @@
<?php
require __DIR__.'/sgf-hr-personnel-proof-bootstrap.php';$c=json_decode(file_get_contents($s['root'].'/hp-concurrency-1794380380400/cuts.json'),true);$n=$host->table('hp_versions')->count();$cmd=$host->table('hp_commands')->count();$host->beginTransaction();try{$in=$hpInput('leave',$c['b_key'],1,['decision'=>'refused'],'review');$p=$hpPreview(14,$in);$ok($p['impact']['after']['state']==='refused'&&$p['impact']['after']['effective']===null,'new explicit refusal conference allowed on obsolete insufficient proposal');$ok($p['impact']['after']['proposal']['refusal_conference']['current_balance']['available_minutes']===0,'refusal preview shows actual insufficient balance');$ok($p['impact']['after']['proposal']['refusal_conference']['original_source_changed'],'source changes shown in refusal impact');[$out,$cut]=$hpCommand(14,$in);$ok($out['state']==='refused'&&$out['version']===2&&$out['impact']['after']['effective']===null,'distinct refusal applies no reservation');$again=$hpConfirm(14,$cut['preview']);$ok($again['replayed']&&$again['id']===$out['id'],'refusal replay no duplication');$select(17);$list=$hp->listing($hrActor(17),['kind'=>'entitlement']);$ok($list['records']['data'][0]['current_balance']['available_minutes']===0&&$list['records']['data'][0]['current_balance']['reserved_minutes']===480,'refusal preserves first reservation and available balance');}finally{$host->rollBack();}$ok($host->table('hp_versions')->count()===$n&&$host->table('hp_commands')->count()===$cmd,'refusal recovery test fully reverted');file_put_contents($work.'/hp-refusal.json',json_encode(['passed'=>true,'checks'=>$checks,'all_fixtures_rolled_back'=>true,'regression_only'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/hp-refusal.json']).PHP_EOL;
+12
View File
@@ -0,0 +1,12 @@
import fs from'node:fs';import assert from'node:assert/strict';import{pathToFileURL}from'node:url';import{transformWithOxc}from'vite';const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);};let slots=[],cursor=0,effects=[],tree,calls=[],rootComponent,props;const same=(a,b)=>a&&b&&a.length===b.length&&a.every((v,i)=>v===b[i]);const R={createElement:(type,props,...children)=>({type,props:props??{},children}),Fragment:Symbol('fragment'),Suspense:Symbol('suspense'),lazy:()=>Symbol('lazy'),useRef(initial){const i=cursor++;slots[i]??={value:{current:initial}};return slots[i].value;},useState(initial){const i=cursor++;slots[i]??={value:initial};return[slots[i].value,v=>{slots[i].value=typeof v==='function'?v(slots[i].value):v;}];},useMemo(fn,deps){const i=cursor++;if(!same(slots[i]?.deps,deps))slots[i]={value:fn(),deps};return slots[i].value;},useEffect(fn,deps){const i=cursor++;if(!same(slots[i]?.deps,deps)){slots[i]?.cleanup?.();slots[i]={deps};effects.push(()=>slots[i].cleanup=fn());}}};globalThis.__hrHooks=R;const stub='const r=globalThis.__hrHooks;export default r;export const useState=r.useState,useMemo=r.useMemo,useEffect=r.useEffect,useRef=r.useRef;',reactUrl='data:text/javascript;base64,'+Buffer.from(stub).toString('base64'),compile=async(file,imports)=>{let s=fs.readFileSync('resources/js/'+file,'utf8').replace(/from\s*['"]react['"]/, 'from '+JSON.stringify(reactUrl));for(const f of imports)s=s.replace(new RegExp('from\\s*[\'"]\\./'+f.replaceAll('.','\\.')+'[\'"]'),'from '+JSON.stringify(pathToFileURL(process.cwd()+'/resources/js/'+f).href));const c=await transformWithOxc(s,file,{jsx:{runtime:'classic'}});return import('data:text/javascript;base64,'+Buffer.from(c.code).toString('base64'));};
const{default:Workspace,PersonnelSourcePicker,PersonnelResult,checkedPersonnel}=await compile('PersonnelWorkspace.jsx',['workshop-stock-operation.js','statement-download.js']);const{navigationGroups}=await compile('WorkspaceNavigation.jsx',['billing-menu.js','workshop-menu.js','stock-menu.js','accounting-menu.js']);const{moduleGroups}=await compile('ModuleNavigation.jsx',[]);
const wk='00000000-0000-4000-8000-000000000002',worker={record_key:wk,version:2,data:{status:'active',input:{code:'QA-SECOND-EXPLICIT'}}},row={id:99,kind:'contract',record_key:'00000000-0000-4000-8000-000000000009',version:1,state:'prepared',actor_id:999,prepared_by:999,created_at:'QA',reason:'Fixture UI sem operação de negócio',data:{input:{operation:'establish',worker_key:wk,reference:'QA-CONTRACT-UI',mode:'fixed',valid_from:'2026-11-01',valid_to:'2026-12-31',gross:'10000.00',evidence:'Fixture sem contrato real'},status:'active',result:{dependent_records:0},dependencies:{worker:{code:'QA-SECOND-EXPLICIT'}}},effective:null,impact:{before:{version:0,state:null},after:{version:1,state:'prepared'}}};
const response=q=>({policy:'SGF-HR-PERSONNEL-QA-v1',kind:q.kind,synthetic_only:true,salary_effect:false,worker:{record_key:q.worker_key},records:{data:q.kind==='contract'?[row]:[],current_page:Number(q.page),last_page:2,total:26},capabilities:{prepare:true,review:false}});let fail=true,commands=[];
const api=async(path,method,body,unused,key)=>{calls.push({path,method,body,key});if(method==='POST'&&path.endsWith('/preview'))return{preview:'opaque-fixture',impact:{before:{version:0,state:null},after:{version:1,state:'prepared',proposal:row.data},journals:0,payments:0}};if(method==='POST'){commands.push({path,body:structuredClone(body),key});if(fail){const e=Error('Resultado incerto');e.status=503;throw e;}return{id:100,version:1,state:'prepared',replayed:true};}const q=Object.fromEntries(new URL('http://qa'+path).searchParams);return response(q);};
const render=()=>{cursor=0;tree=rootComponent(props);const jobs=effects;effects=[];jobs.forEach(f=>f());return tree;},tick=async()=>{await new Promise(r=>setTimeout(r,0));render();},descend=n=>Array.isArray(n)?n.flatMap(descend):n&&typeof n==='object'?[n,...descend(n.children)]:[],nodes=(n=tree)=>descend(n),text=n=>Array.isArray(n)?n.map(text).join(''):n&&typeof n==='object'?text(n.children):String(n??''),find=(tag,label)=>nodes().find(n=>n.type===tag&&text(n).includes(label));
rootComponent=Workspace;props={api,context:{token:'hp-context',module_id:'human_resources',unit:{id:2}}};render();await tick();ok(calls.length===0,'no implicit first employee or personnel query');let picker=nodes().find(n=>n.type===PersonnelSourcePicker);picker.props.onChange(wk,worker);render();await tick();ok(calls[0].path.includes('worker_key='+wk),'chosen second worker exact scope in query');ok(text(tree).includes('QA-SECOND-EXPLICIT'),'selected worker identity visible');find('button','Seguinte').props.onClick();render();await tick();ok(calls.at(-1).path.includes('page=2')&&calls.at(-1).path.includes('worker_key='+wk),'pagination preserves exact worker');find('button','Escolher este registo').props.onClick();render();const resultNode=nodes().find(n=>n.type===PersonnelResult);ok(resultNode.props.data.input.reference==='QA-CONTRACT-UI','explicit chosen contract shown');find('button','Novo registo').props.onClick();render();ok(find('label','Referência interna QA').children.find(n=>n?.type==='input').props.value==='QA-','new form does not inherit chosen contract');ok(find('button','Planos e assiduidade').props.disabled,'form locks module function changes');const field=(label,value)=>find('label',label).children.find(n=>['input','textarea'].includes(n?.type)).props.onChange({target:{value}});field('Referência interna QA','QA-NEW-EXPLICIT');field('Início','2026-11-01');field('Fim explícito','2026-12-31');field('Bruto mensal','10000.00');field('Referência da evidência','Evidência fictícia individual');field('Fundamento individual','Fundamento QA sem contrato real');render();const gross=find('label','Bruto mensal').children.find(n=>n?.type==='input');ok(new RegExp('^'+gross.props.pattern+'$').test('10000.00'),'documentary gross browser pattern valid');await nodes().find(n=>n.type==='form'&&text(n).includes('Conferir impacto')).props.onSubmit({preventDefault(){}});render();ok(calls.at(-1).body.data.worker_key===wk&&calls.at(-1).body.data.reference==='QA-NEW-EXPLICIT','preview exact employee and form input');ok(Boolean(find('h2','Impacto antes da confirmação')),'impact visible before confirmation');await find('button','Confirmar esta decisão individual').props.onClick();render();ok(Boolean(find('button','Recuperar a mesma confirmação'))&&find('button','Pedidos e gozo').props.disabled,'uncertain503 keeps decision and blocks navigation');fail=false;await find('button','Recuperar a mesma confirmação').props.onClick();render();await tick();ok(commands.length===2&&JSON.stringify(commands[0])===JSON.stringify(commands[1]),'recovery uses same body path and key');ok(!find('button','Recuperar a mesma confirmação'),'successful receipt clears uncertainty');find('button','Pedidos e gozo').props.onClick();render();await tick();find('button','Novo registo').props.onClick();render();const sources=nodes().filter(n=>n.type===PersonnelSourcePicker&&n.props.kind!=='worker');ok(sources.length===3&&sources.every(n=>n.props.value===''),'contract entitlement and day explicitly unselected');
const q={kind:'contract',worker_key:wk,page:1,per_page:25},sample=response(q);assert.equal(checkedPersonnel(sample,q),sample);assert.throws(()=>checkedPersonnel({...sample,kind:'leave'},q));assert.throws(()=>checkedPersonnel({...sample,records:{...sample.records,data:[{...row,data:{...row.data,input:{...row.data.input,worker_key:'other'}}}]}},q));ok(true,'late wrong kind or worker response refused');const groups=navigationGroups(r=>r.startsWith('human_resources.'),2,{remunerations:true,personnel:true}),owned=moduleGroups(groups,'human_resources');ok(owned.length===1&&owned[0].items.some(i=>i.id==='hr_personnel'),'own personnel dropdown visible with rights');ok(moduleGroups(groups,'finance').length===0,'HR rights confer no finance menu');const denied=navigationGroups(r=>r==='human_resources.remunerations.view',2,{remunerations:true,personnel:true});ok(!denied.flatMap(g=>g.items).some(i=>i.id==='hr_personnel'),'salary read alone cannot open personnel area');
const report={input:{period:'2026-11',evidence:'Fixture'},dependencies:{worker:{code:'QA'}},status:'active',result:{known_days:2,known_conferred_days:2,other_calendar_days_unknown:28,days:[],leave_balances:[],payroll:[{record_key:'x',version:2,contract_link_proven:false}]}};const rt=PersonnelResult({kind:'report',data:report});ok(text(rt).includes('Mês completo: não demonstrado')&&text(rt).includes('Sem ligação contratual histórica comprovada'),'report explicit partial coverage and historical salary boundary');
const file=root+'/hp-ui-20261111.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,api_mocked:true,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
+2
View File
@@ -0,0 +1,2 @@
import fs from'node:fs';import assert from'node:assert/strict';import crypto from'node:crypto';import{personnelReadableDownload}from'../resources/js/statement-download.js';
const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);},body=Buffer.from('\uFEFFResumo QA;8h 00min\r\n'),sha=crypto.createHash('sha256').update(body).digest('hex'),source='a'.repeat(64),headers={'Content-Type':'text/csv; charset=utf-8','X-Content-SHA256':sha,'X-Source-SHA256':source,'X-SGF-Read-Format':'SGF-HR-READABLE-CSV-v1'};let sent=null;const args={id:99,token:'qa-only-token',sourceSha256:source,isCurrent:()=>true,fetcher:async(path,options)=>{sent={path,options};return new Response(body,{headers});}};const out=await personnelReadableDownload(args);ok(Buffer.from(out.bytes).equals(body)&&out.sourceSha256===source,'exact derived bytes with original source reference preserved');ok(sent.path==='/api/hr-personnel/readable/99'&&!sent.path.includes(args.token)&&sent.options.headers['X-SGF-Context']===args.token&&sent.options.credentials==='same-origin','fixed same-origin route and context only in header');for(const[k,v]of[['X-Source-SHA256','b'.repeat(64)],['X-SGF-Read-Format','other'],['Content-Type','text/html'],['X-Content-SHA256','0'.repeat(64)]]){await assert.rejects(personnelReadableDownload({...args,fetcher:async()=>new Response(body,{headers:{...headers,[k]:v}})}));ok(true,'wrong derived response refused '+k);}let called=false;await assert.rejects(personnelReadableDownload({...args,isCurrent:()=>false,fetcher:async()=>{called=true;return new Response(body,{headers});}}));ok(!called,'obsolete context blocks fetch');let current=true;await assert.rejects(personnelReadableDownload({...args,isCurrent:()=>current,fetcher:async()=>({ok:true,headers:new Headers(headers),arrayBuffer:async()=>{current=false;return body.buffer.slice(body.byteOffset,body.byteOffset+body.byteLength);}})}));ok(true,'context change after body blocks saving');await assert.rejects(personnelReadableDownload({...args,fetcher:async()=>new Response(JSON.stringify({message:'Sem acesso'}),{status:403,headers:{'Content-Type':'application/json'}})}),e=>e.status===403);ok(true,'denied access preserved as403');for(const id of[0,1.5,'../1'])await assert.rejects(personnelReadableDownload({...args,id}));ok(true,'invalid identifiers rejected before request');const original=fs.readFileSync(root+'/app/resources/js/statement-download.js','utf8'),latest=fs.readFileSync('resources/js/statement-download.js','utf8');ok(latest.startsWith(original.trimEnd()),'all original download mechanisms unchanged byte prefix');const file=root+'/hp-readable-download-20261111.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,business_commands:0,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
+2
View File
@@ -0,0 +1,2 @@
import fs from'node:fs';import assert from'node:assert/strict';import crypto from'node:crypto';import{session}from'./sgf-single-http.mjs';const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',p=JSON.parse(fs.readFileSync(root+'/hp-http-progress-private.json')),s=await session('hr:hr_read','human_resources'),checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);};let reportBytes;for(const[kind,step]of[['contract','contract-review'],['day','day9-review'],['entitlement','entitlement-review'],['leave','take-review'],['report','report-review']]){const row=p.steps[step].result,id=row.id,source=row.impact.after.proposal.artifact_sha256;let r=await s.get('/api/hr-personnel/readable/'+id);assert.equal(r.status,200);const bytes=Buffer.from(await r.arrayBuffer());ok(/text\/csv/.test(r.headers.get('content-type'))&&r.headers.get('x-source-sha256')===source&&r.headers.get('x-sgf-read-format')==='SGF-HR-READABLE-CSV-v1'&&crypto.createHash('sha256').update(bytes).digest('hex')===r.headers.get('x-content-sha256'),'actual readable headers source and SHA '+kind);ok(bytes.toString().includes('não é documento legal')&&!bytes.toString().includes('{""'),'actual readable labels without raw objects '+kind);r=await s.get('/api/hr-personnel/exports/'+id);assert.equal(r.status,200);const original=Buffer.from(await r.arrayBuffer());ok(crypto.createHash('sha256').update(original).digest('hex')===source,'original preserved CSV SHA '+kind);if(kind==='report'){reportBytes=bytes;ok(bytes.toString().includes('Outros dias sem cobertura comprovada')&&bytes.toString().includes('Não demonstrado')&&bytes.toString().includes('480 minutos'),'real partial period and time limits legible');ok(original.equals(fs.readFileSync(root+'/hp-frozen-period-private.csv')),'original period CSV bytes unchanged');}}
const historical=p.steps['day10-review'].result;let r=await s.get('/api/hr-personnel/readable/'+historical.id);assert.equal(r.status,200);ok((await r.text()).includes('Desconhecida'),'old unknown declaration not replaced by current empty declaration');r=await s.get('/api/hr-personnel/readable/'+p.steps['day10-conference-review'].result.id);assert.equal(r.status,200);ok((await r.text()).includes('Conjunto explicitamente vazio'),'current explicitly empty declaration distinguished');r=await s.get('/api/hr-personnel/readable/2147483647');ok(r.status===403,'foreign readable version refused');r=await s.get('/api/hr-personnel/readable/'+p.steps['contract-prepare'].result.id);ok(r.status===403,'prepared readable version refused');r=await s.get('/api/hr-personnel/readable/'+p.steps['contract-review'].result.id+'?tenant_id=2');ok(r.status===422,'readable tenant override refused');const finance=await session('sgf:finance_prepare','finance');r=await finance.get('/api/hr-personnel/readable/'+p.steps['contract-review'].result.id);ok([403,409].includes(r.status),'financial profile cannot read HR summary');r=await fetch('http://127.0.0.1:8085/api/hr-personnel/readable/'+p.steps['contract-review'].result.id,{headers:{Accept:'application/json'}});ok(r.status===401,'unauthenticated summary refused');const manifest=JSON.parse(fs.readFileSync(root+'/app/public/build/manifest.json'));for(const e of Object.values(manifest).filter(e=>e.isEntry||/PersonnelWorkspace|statement-download/.test(e.file))){r=await fetch('http://127.0.0.1:8085/build/'+e.file);const b=Buffer.from(await r.arrayBuffer());ok(r.status===200&&/javascript/.test(r.headers.get('content-type'))&&crypto.createHash('sha256').update(b).digest('hex')===crypto.createHash('sha256').update(fs.readFileSync(root+'/app/public/build/'+e.file)).digest('hex'),'served JS MIME SHA '+e.file);}fs.writeFileSync(root+'/hp-readable-period-private.csv',reportBytes);const file=root+'/hp-readable-http-20261111.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,business_commands:0,new_grants:0,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
+3
View File
@@ -0,0 +1,3 @@
<?php
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$baseline=$s['root'].'/single-integration/20261111-095619-e3fff98c';$before=json_decode(file_get_contents($baseline.'/preservation-private.json'),true);$installation=json_decode(file_get_contents($baseline.'/hp-readable-installation.json'),true);$same=[];$order=fn($t)=>in_array($t,['sr_outbox','sr_deliveries'],true)?'event_id':(in_array($t,['sc_catalogue_meta','pd_due_meta','fo_meta','fs_meta','rm_meta','hp_meta'],true)?'policy':(in_array($t,['hr_qa_profiles','sgf_presentation_profiles'],true)?'profile_key':(in_array($t,['fs_results','fs_result_lines'],true)?'snapshot_key':($t==='company_memberships'?'user_id':'id'))));foreach($before as$name=>$tables){$db=$name==='host'?$host:$native;foreach($tables as$table=>$json){if($db->table($table)->orderBy($order($table))->when($table==='fs_result_lines',fn($q)=>$q->orderBy('account_id'))->get()->toJson()!==$json)throw new LogicException('Accepted facts changed '.$table);$same[]=$name.'.'.$table;}}
foreach($installation['hashes']as$file=>$sha)if(hash_file('sha256',$s['app'].'/'.$file)!==$sha)throw new LogicException('Installed file changed');$older=json_decode(file_get_contents(json_decode(file_get_contents($s['root'].'/remunerations-backup-pointer.json'),true)['root'].'/preservation-private.json'),true);foreach(['binding'=>'binding.json','single_binding'=>'single-bindings.json','stock_binding'=>'stock-binding.json']as$key=>$file)if(hash_file('sha256',$s['root'].'/'.$file)!==$older[$key])throw new LogicException('Binding changed');if($host->table('hp_versions')->count()!==16||$host->table('hp_commands')->count()!==16||$host->table('rm_versions')->count()!==6||$native->table('gl_journals')->count()!==27)throw new LogicException('Original counts changed');file_put_contents($work.'/hp-readable-preservation.json',json_encode(['passed'=>true,'identical_original_tables'=>$same,'all_installed_hashes_verified'=>true,'three_bindings_unchanged'=>true,'business_commands_8085'=>0,'new_grants'=>0,'new_profiles'=>0,'new_schema'=>false,'cache_cleared'=>false,'protected_six_targeted'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'identical_tables'=>count($same),'proof'=>$work.'/hp-readable-preservation.json']).PHP_EOL;
+4
View File
@@ -0,0 +1,4 @@
<?php
require __DIR__.'/sgf-hr-personnel-proof-bootstrap.php';$before=[$host->table('hp_versions')->count(),$host->table('hp_commands')->count(),$host->table('identity_audit')->count()];$select(17);$rows=$host->table('hp_versions')->where('state','approved')->orderBy('id')->get();$seen=[];foreach($rows as$row){if(isset($seen[$row->kind]))continue;$seen[$row->kind]=true;$out=$hp->readable($hrActor(17),$row->id);$d=json_decode($row->data,true);$ok($out['source_sha256']===$d['artifact_sha256']&&hash('sha256',$out['bytes'])===$out['sha256'],'readable SHA and frozen source '.$row->kind);$ok(str_starts_with($out['bytes'],"\xEF\xBB\xBF")&&!str_contains($out['bytes'],'{""')&&str_contains($out['bytes'],'não é documento legal'),'UTF8 view not raw JSON '.$row->kind);$sample=(array)$row;$sample['data']=$d;$sample['reason']=" =HYPERLINK(\"qa\")\r\nTeste";$bytes=App\Modules\HumanResources\PersonnelReadableCsv::render($sample);$ok(str_contains($bytes,"' =HYPERLINK(\"\"qa\"\")"),'free text CSV formula prefix escaped '.$row->kind);}
$ok($seen===['contract'=>true,'day'=>true,'entitlement'=>true,'leave'=>true],'all available regression source kinds rendered');$select(17);$reject(fn()=>$hp->readable($hrActor(17),2147483647),403,'unknown or foreign readable version refused');$prepared=$host->table('hp_versions')->where('state','prepared')->first();$reject(fn()=>$hp->readable($hrActor(17),$prepared->id),403,'prepared readable view refused');
$host->beginTransaction();try{$r=$host->table('hp_versions')->where(['kind'=>'contract','state'=>'approved'])->first();$data=json_decode($r->data,true)['input'];$data['operation']='amend';[$a]=$hpCommand(13,$hpInput('contract',$r->record_key,$r->version,$data));$event=$host->table('identity_audit')->orderByDesc('id')->first();$ok($event->action==='human_resources.contracts.prepare'&&json_decode($event->details,true)['area']==='contracts','new contract audit classified in correct area');}finally{$host->rollBack();}$ok([$host->table('hp_versions')->count(),$host->table('hp_commands')->count(),$host->table('identity_audit')->count()]===$before,'reads and audit test preserve all accepted facts');file_put_contents($work.'/hp-readable-result.json',json_encode(['passed'=>true,'checks'=>$checks,'business_writes_8085'=>0,'new_grants'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/hp-readable-result.json']).PHP_EOL;
+2
View File
@@ -0,0 +1,2 @@
import fs from'node:fs';import assert from'node:assert/strict';import{pathToFileURL}from'node:url';import{transformWithOxc}from'vite';let slots=[],cursor=0;const R={createElement:(type,props,...children)=>({type,props:props??{},children}),Fragment:Symbol('fragment'),useState:initial=>{const i=cursor++;return[slots[i]===undefined?initial:slots[i],()=>{}];},useEffect(){},useRef:value=>({current:value}),useMemo:fn=>fn()};globalThis.__finalPersonnel=R;const stub='const r=globalThis.__finalPersonnel;export default r;export const useState=r.useState,useEffect=r.useEffect,useRef=r.useRef,useMemo=r.useMemo;',u='data:text/javascript;base64,'+Buffer.from(stub).toString('base64');let s=fs.readFileSync('resources/js/PersonnelWorkspace.jsx','utf8').replace(/from\s*['"]react['"]/, 'from '+JSON.stringify(u));for(const f of['workshop-stock-operation.js','statement-download.js'])s=s.replace(new RegExp('from\\s*[\'"]\\./'+f.replaceAll('.','\\.')+'[\'"]'),'from '+JSON.stringify(pathToFileURL(process.cwd()+'/resources/js/'+f).href));const c=await transformWithOxc(s,'PersonnelWorkspace.jsx',{jsx:{runtime:'classic'}}),{default:Workspace,PersonnelResult}=await import('data:text/javascript;base64,'+Buffer.from(c.code).toString('base64')),text=n=>Array.isArray(n)?n.map(text).join(''):n&&typeof n==='object'?text(n.children):String(n??''),checks=[];const worker={record_key:'QA-W',version:2,data:{input:{code:'QA-W'}}},row={id:1,record_key:'QA-E',version:2,kind:'entitlement',state:'approved',actor_id:19,created_at:'QA',reason:'Fixture',data:{input:{worker_key:'QA-W',valid_from:'2026-11-01',valid_to:'2026-12-31',minutes:960,evidence:'Fixture'},status:'active',dependencies:{worker:{code:'QA-W'}},result:{available_minutes:960,reserved_minutes:0,taken_minutes:0}},current_balance:{available_minutes:480,reserved_minutes:0,taken_minutes:480},impact:{before:{version:1,state:'prepared'},after:{version:2,state:'approved'}}};row.effective={version:2,data:row.data};slots=[worker,{kind:'entitlement',worker_key:'QA-W',page:1,per_page:25},{search:'',period:'',state:''},{records:{data:[row],current_page:1,last_page:1,total:1},capabilities:{prepare:false,review:false}},row,null,null,false,'','','',null];
const bytes=Buffer.from('\uFEFFResumo QA;8h 00min\r\n'.replace('\\uFEFF','\uFEFF')),sha=(await import('node:crypto')).createHash('sha256').update(bytes).digest('hex'),source='a'.repeat(64);row.data.artifact_sha256=source;let request,clicked=false,anchor,blob;globalThis.fetch=async(path,options)=>{request={path,options};return new Response(bytes,{headers:{'Content-Type':'text/csv; charset=utf-8','X-Content-SHA256':sha,'X-Source-SHA256':source,'X-SGF-Read-Format':'SGF-HR-READABLE-CSV-v1'}});};globalThis.document={createElement:()=>anchor={click(){clicked=true;}}};const old=URL.createObjectURL;URL.createObjectURL=value=>{blob=value;return old(value);};const tree=Workspace({api:()=>{throw Error('No business API expected');},context:{token:'fixed'}}),walk=n=>Array.isArray(n)?n.flatMap(walk):n&&typeof n==='object'?[n,...walk(n.children)]:[],button=walk(tree).find(n=>n.type==='button'&&text(n).includes('Descarregar resumo legível desta versão'));assert(button&&!button.props.disabled);checks.push('approved selected version offers readable download');await button.props.onClick();assert(clicked&&request.path==='/api/hr-personnel/readable/1'&&request.options.headers['X-SGF-Context']==='fixed'&&!request.path.includes('fixed'));checks.push('actual UI handler fetches exact version with context only in header');assert(Buffer.from(await blob.arrayBuffer()).equals(bytes)&&anchor.download==='sgf-rh-resumo-legivel-entitlement-v2.csv');checks.push('actual UI saves verified derived bytes under distinct readable filename');URL.createObjectURL=old;const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',file=root+'/hp-readable-ui-20261111.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,react_hooks_simulated:true,fetch_mocked:true,business_commands:0,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
@@ -2,4 +2,5 @@
require __DIR__.'/sgf-single-bootstrap.php';$checks=[];$ok=function($v,$n)use(&$checks){if(!$v)throw new LogicException($n);$checks[]=$n;};$reject=function($fn,$status,$n)use($ok){try{$fn();throw new LogicException('Expected refusal');}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface$e){$ok($e->getStatusCode()===$status,$n);}};
use App\Modules\HumanResources\PersonnelTime as T;
$full=[['from'=>480,'to'=>720],['from'=>780,'to'=>1020]];$d=T::day('2026-11-16',$full,$full,[]);$ok($d['planned_minutes']===480&&$d['declared_minutes']===480&&$d['unaccounted_minutes']===0,'explicit split day480 complete declared');$ok($d['planned_utc'][0]['from']==='2026-11-16T06:00:00+00:00'&&$d['zone']===T::ZONE,'local plan UTC bounds explicit');$ok(!$d['salary_effect']&&!$d['overtime_authorized']&&$d['legal_classification']===null,'no financial or legal assumption');$d=T::day('2026-11-17',$full,[],$full);$ok($d['leave_minutes']===480&&$d['unaccounted_minutes']===0,'explicit no work and approved leave cover planned day');$ok(T::day('2026-11-17',$full,null,[])['unaccounted_minutes']===null,'missing declaration never absence zero');$ok(T::day('2026-11-17',null,[],[])['planned_minutes']===null,'missing plan never zero');$ok(T::day('2026-11-17',$full,[],null)['unaccounted_minutes']===null,'unknown leave conference remains unknown');$ok(T::day('2026-11-17',$full,[],[])['unaccounted_minutes']===480,'explicit absence of work and leave remains unaccounted no legal status');$ok(T::day('2026-11-17',$full,[['from'=>480,'to'=>1080]],[])['outside_plan_minutes']===120,'outside plan counted without approved overtime');$reject(fn()=>T::day('2026-11-17',$full,$full,$full),409,'work and leave conflict refused');$reject(fn()=>T::day('2026-11-17',$full,[],[['from'=>420,'to'=>480]]),409,'leave outside explicit plan refused');$reject(fn()=>T::day('2026-02-30',$full,[],[]),422,'invalid day refused');$reject(fn()=>T::intervals([['from'=>480,'to'=>800],['from'=>700,'to'=>900]]),409,'overlapping intervals refused');$reject(fn()=>T::intervals([['from'=>480,'to'=>480]]),422,'zero duration refused');$reject(fn()=>T::intervals([['from'=>480,'to'=>1600]]),422,'overnight requires explicit next day record');$ok(T::intervals([['from'=>600,'to'=>720],['from'=>480,'to'=>600]])===[['from'=>480,'to'=>720]],'adjacent ranges normalized deterministically');$b=T::leaveBalance(960,[['key'=>'QA-LEAVE-1','minutes'=>480,'state'=>'reserved']]);$ok($b['available_minutes']===480&&$b['reserved_minutes']===480&&$b['taken_minutes']===0,'approved request reserves480 of explicit960');$b=T::leaveBalance(960,[['key'=>'QA-LEAVE-1','minutes'=>480,'state'=>'taken']]);$ok($b['available_minutes']===480&&$b['reserved_minutes']===0&&$b['taken_minutes']===480,'enjoyment moves reservation to used without double count');$ok(T::leaveBalance(960,[['key'=>'QA-LEAVE-1','minutes'=>480,'state'=>'cancelled']])['available_minutes']===960,'cancellation releases balance');$ok(T::leaveBalance(null,[])['available_minutes']===null,'unknown entitlement never zero');$reject(fn()=>T::leaveBalance(100,[['key'=>'A','minutes'=>480,'state'=>'reserved']]),409,'insufficient entitlement refused');$reject(fn()=>T::leaveBalance(960,[['key'=>'A','minutes'=>480,'state'=>'reserved'],['key'=>'A','minutes'=>480,'state'=>'taken']]),409,'same request cannot count twice');
$ok(T::day('2026-11-09',[['to'=>720,'from'=>480]],[],[])['planned_minutes']===240,'JSON field order has no semantic effect');
file_put_contents($work.'/hr-personnel-time-core.json',json_encode(['passed'=>true,'checks'=>$checks,'pure_arithmetic_only'=>true,'business_writes'=>0,'new_grants'=>0,'installed_8085'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/hr-personnel-time-core.json']).PHP_EOL;
+2 -2
View File
@@ -6,7 +6,7 @@ const row={id:99,kind:'worker',record_key:'00000000-0000-4000-8000-000000000001'
const page=q=>({policy:'SGF-REMUNERATIONS-QA-v1',synthetic_only:true,legal_payroll_validated:false,kind:q.kind,records:{data:q.kind==='worker'?[row]:[],current_page:q.page,last_page:2,total:26},capabilities:{prepare:allowPrepare,review:false},page_totals:{approved_active_on_page:0,gross_cents:'0',net_cents:'0',employer_cost_cents:'0'}});
const api=async(path,method,body,unused,key)=>{calls.push({path,method,body,key});if(method==='POST'&&path.endsWith('/preview'))return{preview:'opaque-fixture',impact:{before:{version:0,state:null},after:{version:1,state:'prepared',proposal:{input:body.data,status:'active',calculation:null}}}};if(method==='POST'){commandCalls.push({path,body:structuredClone(body),key});if(failConfirm){const e=Error('Resposta perdida');e.status=503;throw e;}return{id:100,version:1,state:'prepared',replayed:true};}const q=Object.fromEntries(new URL('http://qa'+path).searchParams);q.page=Number(q.page);q.per_page=Number(q.per_page);return page(q);};
const render=()=>{cursor=0;tree=rootComponent(props);const jobs=effects;effects=[];jobs.forEach(f=>f());return tree;},tick=async()=>{await new Promise(r=>setTimeout(r,0));render();},descend=n=>Array.isArray(n)?n.flatMap(descend):n&&typeof n==='object'?[n,...descend(n.children)]:[],nodes=(n=tree)=>descend(n),text=n=>Array.isArray(n)?n.map(text).join(''):n&&typeof n==='object'?text(n.children):String(n??''),find=(tag,label)=>nodes().find(n=>n.type===tag&&text(n).includes(label));
rootComponent=Workspace;props={api,context:{token:'hr-context',module_id:'human_resources',unit:{id:2}}};render();await tick();ok(text(tree).includes('RECURSOS HUMANOS')&&text(tree).includes('Contratos, assiduidade, férias'),'RH autonomous identity and honest limits');ok(!find('h2','Registo escolhido'),'no implicit first record selection');find('button','Escolher este registo').props.onClick();render();ok(Boolean(find('h2','Registo escolhido')),'individual chosen record opens');find('button','Novo:').props.onClick();render();ok(nodes().find(n=>n.type==='input'&&n.props.pattern==='QA-[A-Z0-9-]{2,60}').props.value==='QA-','new worker does not inherit selected code');ok(find('button','Processamento interno').props.disabled===true,'form blocks tab change');find('label','Código pseudónimo').children.find(n=>n?.type==='input').props.onChange({target:{value:'QA-NEW-ONLY'}});find('label','Fundamento individual').children.find(n=>n?.type==='textarea').props.onChange({target:{value:'Fundamento individual de ensaio sem operação real'}});render();const form=nodes().find(n=>n.type==='form'&&text(n).includes('Conferir impacto'));await form.props.onSubmit({preventDefault(){}});render();ok(calls.at(-1).body.data.code==='QA-NEW-ONLY'&&calls.at(-1).body.kind==='worker','preview exact selected form body');ok(Boolean(find('h2','Impacto antes da confirmação')),'impact shown before confirm');const confirm=find('button','Confirmar esta decisão individual');await confirm.props.onClick();render();ok(Boolean(find('button','Recuperar a mesma confirmação'))&&find('button','Processamento interno').props.disabled,'503 preserves uncertain decision and blocks navigation');failConfirm=false;await find('button','Recuperar a mesma confirmação').props.onClick();render();await tick();ok(commandCalls.length===2&&JSON.stringify(commandCalls[0])===JSON.stringify(commandCalls[1]),'recovery sends identical body path and key');ok(!find('button','Recuperar a mesma confirmação'),'successful receipt ends uncertainty');
rootComponent=Workspace;props={api,context:{token:'hr-context',module_id:'human_resources',unit:{id:2}}};render();await tick();ok(text(tree).includes('RECURSOS HUMANOS')&&text(tree).includes('A conformidade legal continua pendente'),'RH autonomous identity and honest limits');ok(!find('h2','Registo escolhido'),'no implicit first record selection');find('button','Escolher este registo').props.onClick();render();ok(Boolean(find('h2','Registo escolhido')),'individual chosen record opens');find('button','Novo:').props.onClick();render();ok(nodes().find(n=>n.type==='input'&&n.props.pattern==='QA-[A-Z0-9-]{2,60}').props.value==='QA-','new worker does not inherit selected code');ok(find('button','Processamento interno').props.disabled===true,'form blocks tab change');find('label','Código pseudónimo').children.find(n=>n?.type==='input').props.onChange({target:{value:'QA-NEW-ONLY'}});find('label','Fundamento individual').children.find(n=>n?.type==='textarea').props.onChange({target:{value:'Fundamento individual de ensaio sem operação real'}});render();const form=nodes().find(n=>n.type==='form'&&text(n).includes('Conferir impacto'));await form.props.onSubmit({preventDefault(){}});render();ok(calls.at(-1).body.data.code==='QA-NEW-ONLY'&&calls.at(-1).body.kind==='worker','preview exact selected form body');ok(Boolean(find('h2','Impacto antes da confirmação')),'impact shown before confirm');const confirm=find('button','Confirmar esta decisão individual');await confirm.props.onClick();render();ok(Boolean(find('button','Recuperar a mesma confirmação'))&&find('button','Processamento interno').props.disabled,'503 preserves uncertain decision and blocks navigation');failConfirm=false;await find('button','Recuperar a mesma confirmação').props.onClick();render();await tick();ok(commandCalls.length===2&&JSON.stringify(commandCalls[0])===JSON.stringify(commandCalls[1]),'recovery sends identical body path and key');ok(!find('button','Recuperar a mesma confirmação'),'successful receipt ends uncertainty');
find('button','Processamento interno').props.onClick();render();await tick();find('button','Novo:').props.onClick();render();const gross=find('label','Bruto MZN').children.find(n=>n?.type==='input');ok(new RegExp('^'+gross.props.pattern+'$').test('10000.00'),'gross browser pattern accepts documentary amount');const pickers=nodes().filter(n=>typeof n.type==='function'&&n.props.kind);ok(pickers.length===2&&pickers.every(n=>n.props.value===''),'worker and rules sources explicitly unselected');
const hrCan=(right)=>right==='human_resources.remunerations.view',groups=navigationGroups(hrCan,2,{remunerations:true}),owned=moduleGroups(groups,'human_resources');ok(owned.length===1&&owned[0].id==='human_resources'&&owned[0].items[0].id==='remunerations','own dropdown contains only HR functions');ok(moduleGroups(groups,'finance').length===0,'HR grant grants no financial menu');ok(navigationGroups(()=>false,2,{remunerations:true}).every(g=>!g.items.some(i=>i.id==='remunerations')),'no rights no HR menu');const valid=page({kind:'worker',page:1,per_page:25});assert.equal(checkedRemunerations(valid,{kind:'worker',page:1,per_page:25}),valid);assert.throws(()=>checkedRemunerations({...valid,kind:'payroll'},{kind:'worker',page:1,per_page:25}));ok(true,'late wrong kind response refused');
const file=root+'/hr-prepared-ui-20261111.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,api_mocked:true,profiles_not_granted:true,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
const file=root+'/hp-remunerations-ui-20261111.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,api_mocked:true,profiles_not_granted:true,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
+8
View File
@@ -0,0 +1,8 @@
<?php
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$proofs=[$s['root'].'/assets-ui-20261111.json',$run['root'].'/private/receipt-composition-b4093c6f6707/single-integration/20261111-110254-0efa3049/assets-functional.json',$run['root'].'/private/receipt-composition-b4093c6f6707/single-integration/20261111-110545-62635f67/assets-correction.json',$run['root'].'/private/receipt-composition-b4093c6f6707/single-integration/20261111-110916-457253bb/assets-boundaries.json',$run['root'].'/private/receipt-composition-b4093c6f6707/single-integration/20261111-111046-e1ba8857/assets-journal-boundaries.json',$run['root'].'/private/receipt-composition-b4093c6f6707/single-integration/20261111-111227-7b78452d/assets-period.json',$run['root'].'/private/receipt-composition-b4093c6f6707/assets-concurrency-1794388203631/result.json'];foreach($proofs as$file)if((json_decode(file_get_contents($file),true)['passed']??false)!==true)throw new LogicException('Required proof missing');if(!$native->getSchemaBuilder()->hasTable('gl_asset_extension_versions'))throw new LogicException('Explicit setup required');
$originals=json_decode(file_get_contents(json_decode(file_get_contents($s['root'].'/assets-backup-pointer.json'),true)['root'].'/preservation-private.json'),true);$oldJournals=json_decode($originals['native']['gl_journals'],true);foreach($oldJournals as$j)$native->select('SELECT gl_assert_balanced(?)',[$j['id']]);if(count($oldJournals)!==27)throw new LogicException('Known27 original journals required');
$files=['routes/web.php','app/Modules/Accounting/AssetDepreciation.php','app/Modules/Accounting/FixedAssets.php','app/Modules/Accounting/FixedAssetsController.php','app/Modules/Accounting/FixedAssetLedgerSource.php','app/Modules/Accounting/FixedAssetJournalController.php','app/Modules/Accounting/ComposedLedger.php','app/Modules/Identity/Services/IdentityWorkflow.php','app/Shared/Context/ModuleNavigation.php','packages/general-ledger/bootstrap.php','packages/general-ledger/src/LedgerAssets.php','packages/general-ledger/src/PayableJournal.php','packages/general-ledger/src/LedgerCycle.php','database/sql/fixed_assets.sql','database/sql/fixed_assets_native.sql','docs/integration/sgf-fixed-assets-access-proposal.json','resources/js/FixedAssetsWorkspace.jsx','resources/js/WorkspaceNavigation.jsx','resources/js/app.jsx'];
foreach($files as$file)if(str_ends_with($file,'.php'))(new Symfony\Component\Process\Process([PHP_BINARY,'-l',$repo.'/'.$file]))->mustRun();$hashes=[];$copy=function($src,$file)use($s,$work,&$hashes){$dest=$s['app'].'/'.$file;if(is_file($dest)){$backup=$work.'/before/'.$file;if(!is_dir(dirname($backup)))mkdir(dirname($backup),0700,true);copy($dest,$backup);}if(!is_dir(dirname($dest)))mkdir(dirname($dest),0700,true);copy($src,$dest);$hashes[$file]=hash_file('sha256',$dest);};foreach($files as$file)$copy($repo.'/'.$file,$file);
$p=json_decode(file_get_contents($repo.'/docs/integration/sgf-fixed-assets-access-proposal.json'),true);$cfg=['enabled'=>true,'schema_sha256'=>hash_file('sha256',$repo.'/database/sql/fixed_assets.sql'),'native_schema_sha256'=>hash_file('sha256',$repo.'/database/sql/fixed_assets_native.sql'),'access_sha256'=>hash('sha256',Tempo\Finance\Payables\Payables::canonical(['scope'=>$p['scope'],'grants'=>$p['grants_per_book']]))];file_put_contents($work.'/fixed_assets.php',"<?php\nreturn ".var_export($cfg,true).";\n");$copy($work.'/fixed_assets.php','config/fixed_assets.php');
$build=$run['root'].'/private/single-assets-build-20261111';$manifest=json_decode(file_get_contents($build.'/manifest.json'),true);$assets=[];foreach($manifest as$entry){$assets[]=$entry['file'];foreach($entry['css']??[]as$file)$assets[]=$file;}foreach(array_unique($assets)as$file){if(!preg_match('~^assets/[a-zA-Z0-9_.-]+$~D',$file))throw new LogicException('Asset path invalid');$copy($build.'/'.$file,'public/build/'.$file);}$copy($build.'/manifest.json','public/build/manifest.json');
foreach($originals['files']as$file=>$hash)if(hash_file('sha256',$s['root'].'/'.$file)!==$hash)throw new LogicException('Binding changed');file_put_contents($work.'/assets-installation.json',json_encode(['passed'=>true,'hashes'=>$hashes,'legacy_balanced_journals'=>27,'bindings_unchanged'=>true,'new_business_commands'=>0,'cache_cleared'=>false,'new_demo'=>false],JSON_PRETTY_PRINT));file_put_contents($s['root'].'/assets-installation-pointer.json',json_encode(['root'=>$work]));echo json_encode(['passed'=>true,'legacy_balanced_journals'=>27,'proof'=>$work.'/assets-installation.json']).PHP_EOL;
@@ -0,0 +1,7 @@
<?php
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';
if((json_decode(file_get_contents($run['root'].'/private/receipt-composition-b4093c6f6707/single-integration/20261111-103843-f515a584/coverage-reconciliation.json'),true)['passed']??false)!==true)throw new LogicException('Metadata proof required');
$previous=json_decode(file_get_contents($s['root'].'/single-integration/20261111-090956-998c70ef/hp-installation.json'),true);$qualified=$previous['identical_original_tables'];$before=[];$order=fn($table)=>in_array($table,['sr_outbox','sr_deliveries'],true)?'event_id':(in_array($table,['sc_catalogue_meta','pd_due_meta','fo_meta','fs_meta','rm_meta','hp_meta'],true)?'policy':(in_array($table,['hr_qa_profiles','sgf_presentation_profiles'],true)?'profile_key':(in_array($table,['fs_results','fs_result_lines'],true)?'snapshot_key':($table==='company_memberships'?'user_id':'id'))));foreach($qualified as$key){[$name,$table]=explode('.',$key);$db=$name==='host'?$host:$native;$before[$name][$table]=$db->table($table)->orderBy($order($table))->when($table==='fs_result_lines',fn($q)=>$q->orderBy('account_id'))->get()->toJson();}$binding=hash_file('sha256',$s['root'].'/binding.json');file_put_contents($work.'/preservation-private.json',json_encode($before));
$files=['app/Modules/Services/FiscalCoverage.php'];foreach($files as$file)if(str_ends_with($file,'.php'))(new Symfony\Component\Process\Process([PHP_BINARY,'-l',$repo.'/'.$file]))->mustRun();$hashes=[];$copy=function($src,$file)use($s,$work,&$hashes){$dest=$s['app'].'/'.$file;$backup=$work.'/before/'.$file;if(is_file($dest)){if(!is_dir(dirname($backup)))mkdir(dirname($backup),0700,true);copy($dest,$backup);}if(!is_dir(dirname($dest)))mkdir(dirname($dest),0700,true);copy($src,$dest);$hashes[$file]=hash_file('sha256',$dest);};foreach($files as$file)$copy($repo.'/'.$file,$file);
$checks=[];foreach($before as$name=>$tables){$db=$name==='host'?$host:$native;foreach($tables as$table=>$json){if($db->table($table)->orderBy($order($table))->when($table==='fs_result_lines',fn($q)=>$q->orderBy('account_id'))->get()->toJson()!==$json)throw new LogicException('Original facts changed '.$table);$checks[]=$name.'.'.$table;}}if($binding!==hash_file('sha256',$s['root'].'/binding.json'))throw new LogicException('Binding changed');file_put_contents($work.'/coverage-reconciliation-installation.json',json_encode(['passed'=>true,'hashes'=>$hashes,'identical_original_tables'=>$checks,'business_commands'=>0,'new_grants'=>0,'new_demo'=>false,'cache_cleared'=>false,'native_writes'=>0,'binding_unchanged'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'proof'=>$work.'/coverage-reconciliation-installation.json']).PHP_EOL;
+9
View File
@@ -0,0 +1,9 @@
<?php
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';
foreach([$s['root'].'/hp-ui-20261111.json',$run['root'].'/private/receipt-composition-b4093c6f6707/single-integration/20261111-085805-d6985581/hp-result.json',$run['root'].'/private/receipt-composition-b4093c6f6707/single-integration/20261111-085910-105a205c/hp-boundaries.json',$run['root'].'/private/receipt-composition-b4093c6f6707/single-integration/20261111-090216-f627a6b2/hp-privacy.json',$run['root'].'/private/receipt-composition-b4093c6f6707/hp-concurrency-1794380380400/result.json']as$file)if((json_decode(file_get_contents($file),true)['passed']??false)!==true)throw new LogicException('Affected proofs required');
$previous=json_decode(file_get_contents($s['root'].'/single-integration/20261111-073116-5f71ef3d/statements-installation.json'),true);$qualified=array_merge($previous['identical_original_tables'],['host.rm_meta','host.rm_versions','host.rm_commands','host.hr_qa_profiles','host.users','host.company_memberships','host.sgf_presentation_profiles','host.hp_meta','host.hp_versions','host.hp_commands']);$before=[];$order=fn($table)=>in_array($table,['sr_outbox','sr_deliveries'],true)?'event_id':(in_array($table,['sc_catalogue_meta','pd_due_meta','fo_meta','fs_meta','rm_meta','hp_meta'],true)?'policy':(in_array($table,['hr_qa_profiles','sgf_presentation_profiles'],true)?'profile_key':(in_array($table,['fs_results','fs_result_lines'],true)?'snapshot_key':($table==='company_memberships'?'user_id':'id'))));foreach($qualified as$key){[$name,$table]=explode('.',$key);$db=$name==='host'?$host:$native;$before[$name][$table]=$db->table($table)->orderBy($order($table))->when($table==='fs_result_lines',fn($q)=>$q->orderBy('account_id'))->get()->toJson();}$binding=hash_file('sha256',$s['root'].'/binding.json');file_put_contents($work.'/preservation-private.json',json_encode($before));
$files=['routes/web.php', 'app/Modules/HumanResources/Personnel.php', 'app/Modules/HumanResources/PersonnelTime.php', 'app/Modules/HumanResources/Remunerations.php', 'app/Modules/HumanResources/Http/PersonnelController.php', 'app/Shared/Installation/PresentationEntry.php','app/Shared/Context/ModuleNavigation.php', 'app/Modules/Identity/Services/IdentityWorkflow.php', 'resources/js/workspace-overview.js','resources/js/PersonnelWorkspace.jsx', 'resources/js/RemunerationsWorkspace.jsx', 'resources/js/app.jsx', 'resources/js/WorkspaceNavigation.jsx', 'resources/js/statement-download.js', 'database/sql/hr_personnel.sql', 'docs/integration/sgf-human-resources-personnel-access-proposal.json'];foreach($files as$file)if(str_ends_with($file,'.php'))(new Symfony\Component\Process\Process([PHP_BINARY,'-l',$repo.'/'.$file]))->mustRun();$hashes=[];$copy=function($src,$file)use($s,$work,&$hashes){$dest=$s['app'].'/'.$file;$backup=$work.'/before/'.$file;if(is_file($dest)){if(!is_dir(dirname($backup)))mkdir(dirname($backup),0700,true);copy($dest,$backup);}if(!is_dir(dirname($dest)))mkdir(dirname($dest),0700,true);copy($src,$dest);$hashes[$file]=hash_file('sha256',$dest);};foreach($files as$file)$copy($repo.'/'.$file,$file);
$a=json_decode(file_get_contents($repo.'/docs/integration/sgf-human-resources-personnel-access-proposal.json'),true);$cfg=['enabled'=>true,'schema_sha256'=>hash_file('sha256',$repo.'/database/sql/hr_personnel.sql'),'access_sha256'=>hash('sha256',Tempo\Finance\Payables\Payables::canonical(['scope'=>$a['scope'],'grants'=>$a['grants_per_book']]))];$file=$work.'/hr_personnel.php';file_put_contents($file,"<?php\nreturn ".var_export($cfg,true).";\n");$copy($file,'config/hr_personnel.php');
$build=$run['root'].'/private/single-hr-personnel-final-build-20261111';$manifest=json_decode(file_get_contents($build.'/manifest.json'),true);$assets=[];foreach($manifest as$entry){$assets[]=$entry['file'];foreach($entry['css']??[]as$file)$assets[]=$file;}foreach(array_unique($assets)as$file){if(!preg_match('~^assets/[a-zA-Z0-9_.-]+$~D',$file))throw new LogicException('Asset path invalid');$copy($build.'/'.$file,'public/build/'.$file);}$copy($build.'/manifest.json','public/build/manifest.json');
$checks=[];foreach($before as$name=>$tables){$db=$name==='host'?$host:$native;foreach($tables as$table=>$json){if($db->table($table)->orderBy($order($table))->when($table==='fs_result_lines',fn($q)=>$q->orderBy('account_id'))->get()->toJson()!==$json)throw new LogicException('Original facts changed '.$table);$checks[]=$name.'.'.$table;}}if($binding!==hash_file('sha256',$s['root'].'/binding.json'))throw new LogicException('Binding changed');file_put_contents($work.'/hp-installation.json',json_encode(['passed'=>true,'hashes'=>$hashes,'identical_original_tables'=>$checks,'business_commands'=>0,'new_grants'=>0,'new_demo'=>false,'cache_cleared'=>false,'native_writes'=>0,'binding_unchanged'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'proof'=>$work.'/hp-installation.json']).PHP_EOL;
+8
View File
@@ -0,0 +1,8 @@
<?php
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';
foreach([$s['root'].'/hp-readable-download-20261111.json',$run['root'].'/private/receipt-composition-b4093c6f6707/single-integration/20261111-095512-0ff4f79e/hp-readable-result.json']as$f)if((json_decode(file_get_contents($f),true)['passed']??false)!==true)throw new LogicException('Directed proofs required');
$previous=json_decode(file_get_contents($s['root'].'/single-integration/20261111-090956-998c70ef/hp-installation.json'),true);$qualified=$previous['identical_original_tables'];$before=[];$order=fn($table)=>in_array($table,['sr_outbox','sr_deliveries'],true)?'event_id':(in_array($table,['sc_catalogue_meta','pd_due_meta','fo_meta','fs_meta','rm_meta','hp_meta'],true)?'policy':(in_array($table,['hr_qa_profiles','sgf_presentation_profiles'],true)?'profile_key':(in_array($table,['fs_results','fs_result_lines'],true)?'snapshot_key':($table==='company_memberships'?'user_id':'id'))));foreach($qualified as$key){[$name,$table]=explode('.',$key);$db=$name==='host'?$host:$native;$before[$name][$table]=$db->table($table)->orderBy($order($table))->when($table==='fs_result_lines',fn($q)=>$q->orderBy('account_id'))->get()->toJson();}$binding=hash_file('sha256',$s['root'].'/binding.json');file_put_contents($work.'/preservation-private.json',json_encode($before));
$files=['routes/web.php','app/Modules/HumanResources/Personnel.php','app/Modules/HumanResources/PersonnelReadableCsv.php','app/Modules/HumanResources/Http/PersonnelController.php','resources/js/PersonnelWorkspace.jsx','resources/js/statement-download.js'];foreach($files as$file)if(str_ends_with($file,'.php'))(new Symfony\Component\Process\Process([PHP_BINARY,'-l',$repo.'/'.$file]))->mustRun();$hashes=[];$copy=function($src,$file)use($s,$work,&$hashes){$dest=$s['app'].'/'.$file;$backup=$work.'/before/'.$file;if(is_file($dest)){if(!is_dir(dirname($backup)))mkdir(dirname($backup),0700,true);copy($dest,$backup);}if(!is_dir(dirname($dest)))mkdir(dirname($dest),0700,true);copy($src,$dest);$hashes[$file]=hash_file('sha256',$dest);};foreach($files as$file)$copy($repo.'/'.$file,$file);
$build=$run['root'].'/private/single-hr-readable-build-20261111';$manifest=json_decode(file_get_contents($build.'/manifest.json'),true);$assets=[];foreach($manifest as$entry){$assets[]=$entry['file'];foreach($entry['css']??[]as$file)$assets[]=$file;}foreach(array_unique($assets)as$file){if(!preg_match('~^assets/[a-zA-Z0-9_.-]+$~D',$file))throw new LogicException('Asset path invalid');$copy($build.'/'.$file,'public/build/'.$file);}$copy($build.'/manifest.json','public/build/manifest.json');
$checks=[];foreach($before as$name=>$tables){$db=$name==='host'?$host:$native;foreach($tables as$table=>$json){if($db->table($table)->orderBy($order($table))->when($table==='fs_result_lines',fn($q)=>$q->orderBy('account_id'))->get()->toJson()!==$json)throw new LogicException('Original facts changed '.$table);$checks[]=$name.'.'.$table;}}if($binding!==hash_file('sha256',$s['root'].'/binding.json'))throw new LogicException('Binding changed');file_put_contents($work.'/hp-readable-installation.json',json_encode(['passed'=>true,'hashes'=>$hashes,'identical_original_tables'=>$checks,'business_commands'=>0,'new_grants'=>0,'new_demo'=>false,'cache_cleared'=>false,'native_writes'=>0,'binding_unchanged'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'proof'=>$work.'/hp-readable-installation.json']).PHP_EOL;
@@ -0,0 +1,5 @@
<?php
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$reg=$run['root'].'/private/receipt-composition-b4093c6f6707';$proofs=[$reg.'/single-integration/20261111-121907-b7ee8767/multiline-invoice-financial.json',$reg.'/single-integration/20261111-122111-47dc936c/multiline-invoice-controller.json',$reg.'/single-integration/20261111-121606-5ed552d1/multiline-invoice-boundaries.json',$reg.'/single-integration/20261111-122030-d06270d0/multiline-invoice-recovery.json',$reg.'/multiline-concurrency-1794392455292/result.json',$s['root'].'/multiline-invoice-ui-20261111.json'];foreach($proofs as$f)if((json_decode(file_get_contents($f),true)['passed']??false)!==true)throw new LogicException('Proof required');if(!$host->getSchemaBuilder()->hasTable('mi_drafts'))throw new LogicException('Explicit schema setup required');
$files=['app/Modules/Services/MultiLineInvoiceCalculation.php','app/Modules/Services/MultiLineInvoiceIssuance.php','app/Modules/Services/MultiLineInvoicePdf.php','app/Modules/Services/MultiLineDrafts.php','app/Modules/Services/QualifiedInvoiceSource.php','app/Modules/Services/Http/MultiLineInvoiceController.php','app/Modules/Services/FiscalRules.php','app/Modules/Services/InvoicePdf.php','app/Modules/Services/ServiceInvoicing.php','app/Modules/Services/ServiceReceivables.php','app/Modules/Accounting/SalesLedgerSource.php','app/Modules/Accounting/ComposedLedgerCandidates.php','app/Modules/Accounting/ComposedLedgerController.php','app/Modules/Accounting/LedgerComposition.php','app/Modules/Identity/Services/IdentityWorkflow.php','app/Shared/Context/ModuleNavigation.php','routes/web.php','database/sql/multiline_invoices.sql','database/sql/multiline_invoice_drafts.sql','resources/js/MultiLineInvoiceWorkspace.jsx','resources/css/multiline-invoice.css','resources/js/WorkspaceNavigation.jsx','resources/js/app.jsx'];foreach($files as$f)if(str_ends_with($f,'.php'))(new Symfony\Component\Process\Process([PHP_BINARY,'-l',$repo.'/'.$f]))->mustRun();$hashes=[];$copy=function($src,$f)use($s,$work,&$hashes){$d=$s['app'].'/'.$f;if(is_file($d)){$b=$work.'/before/'.$f;if(!is_dir(dirname($b)))mkdir(dirname($b),0700,true);copy($d,$b);}if(!is_dir(dirname($d)))mkdir(dirname($d),0700,true);if(!copy($src,$d))throw new LogicException('Copy failed');$hashes[$f]=hash_file('sha256',$d);};foreach($files as$f)$copy($repo.'/'.$f,$f);
$build=$run['root'].'/private/single-multiline-final-build-20261111';$manifest=json_decode(file_get_contents($build.'/manifest.json'),true);$assets=[];foreach($manifest as$e){$assets[]=$e['file'];foreach($e['css']??[]as$f)$assets[]=$f;}foreach(array_unique($assets)as$f){if(!preg_match('~^assets/[a-zA-Z0-9_.-]+$~D',$f))throw new LogicException('Asset path invalid');$copy($build.'/'.$f,'public/build/'.$f);}$copy($build.'/manifest.json','public/build/manifest.json');$cfg=['enabled'=>true,'schema_sha256'=>hash_file('sha256',$repo.'/database/sql/multiline_invoices.sql'),'draft_schema_sha256'=>hash_file('sha256',$repo.'/database/sql/multiline_invoice_drafts.sql')];file_put_contents($work.'/multiline_invoices.php',"<?php\nreturn ".var_export($cfg,true).";\n");$copy($work.'/multiline_invoices.php','config/multiline_invoices.php');file_put_contents($work.'/multiline-invoice-installation.json',json_encode(['passed'=>true,'hashes'=>$hashes,'new_business_commands'=>0,'new_grants'=>0,'new_accounts'=>0,'cache_cleared'=>false,'new_demo'=>false],JSON_PRETTY_PRINT));file_put_contents($s['root'].'/multiline-invoice-installation-pointer.json',json_encode(['root'=>$work]));echo json_encode(['passed'=>true,'proof'=>$work.'/multiline-invoice-installation.json']).PHP_EOL;
+2
View File
@@ -0,0 +1,2 @@
<?php
$book='8085';require __DIR__.'/sgf-assets-proof-bootstrap.php';config(['multiline_invoices.enabled'=>true,'multiline_invoices.schema_sha256'=>hash_file('sha256',$repo.'/database/sql/multiline_invoices.sql'),'multiline_invoices.draft_schema_sha256'=>hash_file('sha256',$repo.'/database/sql/multiline_invoice_drafts.sql')]);config(['sales_ledger.synthetic_enabled'=>true,'cost_ledger.synthetic_enabled'=>true,'industrial_ledger.synthetic_enabled'=>true,'cost_ledger.correction_enabled'=>true,'industrial_ledger.correction_enabled'=>true,'cash_qa.enabled'=>true,'cash_qa.binding_sha256'=>hash_file('sha256',$s['root'].'/cash-binding.json')]);try{$out=$call('read_only','view',function($e){$method=new ReflectionMethod($e,'periodSnapshot');return$method->invoke($e,'QA-2026-INTEGRADO');});}catch(Throwable $e){echo json_encode(['error'=>get_class($e),'file'=>$e->getFile(),'line'=>$e->getLine(),'trace'=>array_map(fn($t)=>array_intersect_key($t,array_flip(['file','line','class','function'])),array_slice($e->getTrace(),0,9))]);exit(1);}file_put_contents($work.'/assets-current-period-private.json',json_encode($out));$pending=$out['pending']??$out['pending_facts']??null;$balanced=(string)$out['debit_minor']===(string)$out['credit_minor'];if($pending!==0||!$balanced)throw new LogicException('Current period not reconciled');file_put_contents($work.'/multiline-current-period.json',json_encode(['passed'=>true,'facts'=>$out['facts'],'pending'=>$pending,'balanced'=>$balanced,'read_only'=>true]));echo json_encode(['passed'=>true,'pending'=>$pending,'facts'=>$out['facts'],'balanced'=>$balanced,'proof'=>$work.'/multiline-current-period.json']).PHP_EOL;
@@ -0,0 +1,2 @@
<?php
$book=$argv[1]??'8085';require __DIR__.'/sgf-single-bootstrap.php';$rights=['services.view','services.create','service_drafts.view','service_drafts.create','service_drafts.edit','billing_drafts.view','billing_drafts.create','billing_drafts.edit','issuer_documents.view','client_documents.view','draft_documents.view','draft_documents.edit','invoice_series.propose','invoice_series.approve','service_fiscal.propose','service_fiscal.approve','service_invoices.view','service_invoices.issue','general_ledger.view','general_ledger.prepare','general_ledger.validate','general_ledger.post'];$rows=$host->table('permission_grants')->where(['tenant_id'=>1,'company_id'=>2])->where(fn($q)=>$q->whereNull('unit_id')->orWhere('unit_id',2))->whereIn('permission',$rights)->orderBy('user_id')->orderBy('permission')->get(['user_id','permission','unit_id'])->map(fn($r)=>(array)$r)->all();$missing=array_values(array_diff($rights,array_column($rows,'permission')));if($missing)throw new LogicException('Additional powers need explicit proposal '.implode(',',$missing));file_put_contents($work.'/multiline-invoice-existing-access-private.json',json_encode(['passed'=>true,'book'=>$book,'existing_grants'=>$rows,'required_names'=>$rights,'missing'=>[],'database_writes'=>0,'new_grants'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'book'=>$book,'existing_permission_names'=>count($rights),'new_grants'=>0,'proof'=>$work.'/multiline-invoice-existing-access-private.json']).PHP_EOL;
+2
View File
@@ -0,0 +1,2 @@
<?php
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$path=$s['root'].'/multiline-invoice-backup-pointer.json';if(is_file($path)){echo json_encode(['passed'=>true,'reused_existing_backup'=>true]);exit;}$old=json_decode(file_get_contents(json_decode(file_get_contents($s['root'].'/assets-backup-pointer.json'),true)['root'].'/preservation-private.json'),true);$tables=[];foreach(['host','native']as$n)$tables[$n]=array_keys($old[$n]);$tables['host']=array_unique(array_merge($tables['host'],['fa_meta','fa_versions','fa_commands','si_proposals','si_decisions','si_counters','si_invoices','si_commands','si_events','si_outbox','service_calculation_profiles','service_calculation_results','fr_events','fr_calculations','fr_commands','a_service_drafts','a_billing_drafts','a_history','a_events','a_outbox','a_commands']));$tables['native']=array_unique(array_merge($tables['native'],['gl_asset_books','gl_asset_extension_versions']));$before=[];foreach($tables as$n=>$names){$db=$n==='host'?$host:$native;foreach($names as$t)$before[$n][$t]=$db->table($t)->get()->toJson();}foreach(['binding.json','single-bindings.json','stock-binding.json']as$f)$before['files'][$f]=hash_file('sha256',$s['root'].'/'.$f);file_put_contents($work.'/preservation-private.json',json_encode($before));$files=['app/Modules/Services/ServiceInvoicing.php','app/Modules/Services/ServiceCalculation.php','app/Modules/Services/ServiceFiscal.php','app/Modules/Services/FiscalRules.php','app/Modules/Services/FiscalDocumentMath.php','app/Modules/Services/InvoicePdf.php','app/Modules/Services/DocumentPreview.php','app/Modules/Services/ServiceWorkflow.php','app/Modules/Services/ServiceReceivables.php','app/Modules/Accounting/SalesLedgerSource.php','resources/js/BillingMenuWorkspace.jsx','resources/js/ServiceInvoiceWorkspace.jsx','resources/js/ServicesWorkspace.jsx','routes/web.php'];foreach($files as$f){$to=$work.'/before/'.$f;if(!is_dir(dirname($to)))mkdir(dirname($to),0700,true);copy($s['app'].'/'.$f,$to);}file_put_contents($path,json_encode(['root'=>$work]));echo json_encode(['passed'=>true,'backup'=>$work,'database_writes'=>0,'original_host_sets'=>count($tables['host']),'original_native_sets'=>count($tables['native'])]).PHP_EOL;
@@ -0,0 +1,16 @@
<?php
use App\Modules\Services\{MultiLineInvoiceCalculation,MultiLineDrafts,MultiLineInvoicePdf,ServiceWorkflow,ServiceInvoicing,FiscalRules,InvoicePdf};use Illuminate\Support\Str;use Illuminate\Support\Facades\DB;
require __DIR__.'/sgf-multiline-invoice-proof-bootstrap.php';$host->beginTransaction();try{
$actor=$select('commercial_prepare');$draftWriter=app(MultiLineDrafts::class);$prototype=['draft_id'=>null,'version'=>0,'source_version'=>0,'preparation_version'=>0,'name'=>'QA-PENDING-MULTI','client_id'=>$client,'currency'=>'MZN','price_mode'=>'undefined','service_date'=>null,'period_start'=>null,'period_end'=>null,'proposed_due_date'=>null,'payment_terms'=>null,'lines'=>[['service_id'=>$service,'description'=>'Linha ainda indeterminada','quantity'=>null,'unit_price'=>null,'unit'=>null]],'reason'=>'Rascunho incompleto explícito, sem tomar desconhecidos por zero.'];$count=$host->table('mi_drafts')->count();$p=$draftWriter->preview($actor,$prototype,(string)Str::uuid());$ok($host->table('mi_drafts')->count()===$count,'draft preview creates no persisted source or registry');$pending=$draftWriter->confirm($actor,$p['preview']);$ok($pending['version']===1&&$pending['new_documents']===0,'unknown dates mode quantity price and unit can remain a saved draft');$actor=$select('finance_prepare');$reject(fn()=>app(MultiLineInvoiceCalculation::class)->preview($actor,['draft_id'=>$pending['id'],'draft_version'=>1,'preparation_version'=>1,'rule_event_id'=>$ruleEvent,'reason'=>'Não assumir datas, quantidade ou preço indeterminados.'],(string)Str::uuid()),422,'incomplete draft cannot become a zero calculation');
$actor=$select('commercial_prepare');$tooMany=$prototype;$tooMany['lines']=array_fill(0,26,$prototype['lines'][0]);$reject(fn()=>$draftWriter->preview($actor,$tooMany,(string)Str::uuid()),422,'26 lines refused');$secret=$prototype;$secret['payment_terms']='token=not-a-real-secret';$reject(fn()=>$draftWriter->preview($actor,$secret,(string)Str::uuid()),422,'secret assignments refused by composed preparation');$bad=$prototype;$bad['lines'][0]['quantity']='1e3';$reject(fn()=>$draftWriter->preview($actor,$bad,(string)Str::uuid()),422,'exponential quantity refused');$bad=$prototype;$bad['scope']=['company_id'=>999];$reject(fn()=>$draftWriter->preview($actor,$bad,(string)Str::uuid()),422,'scope injection refused');$reject(fn()=>$draftWriter->preview($actor,$prototype,str_repeat('-',36)),422,'non UUID key refused before SQL');
$x=$fixture('added',['400','600'],'BOUNDARY');$actor=$select('finance_prepare');$calcWriter=app(MultiLineInvoiceCalculation::class);$input=['draft_id'=>$x['draft']['id'],'draft_version'=>1,'preparation_version'=>1,'rule_event_id'=>$ruleEvent,'reason'=>'Cálculo vinculado a origem e preparador actuais.'];$cut=$calcWriter->preview($actor,$input,(string)Str::uuid());$calc=$calcWriter->confirm($actor,$cut['preview']);
$host->beginTransaction();try{$host->table('permission_grants')->where($f['scope']+['user_id'=>$profiles['finance_prepare']['user_id'],'permission'=>'fiscal_rules.prepare'])->delete();$actor=$select('commercial_review');$reject(fn()=>DB::transaction(fn()=>$calcWriter->current($actor,$calc['id'],$x['draft']['id'])),403,'calc preparer authority revalidated by independent reviewer');}finally{$host->rollBack();}
$host->beginTransaction();try{$host->table('users')->where('id',$profiles['finance_prepare']['user_id'])->update(['name'=>'Identidade QA alterada apenas em rollback']);$actor=$select('commercial_review');$reject(fn()=>DB::transaction(fn()=>$calcWriter->current($actor,$calc['id'],$x['draft']['id'])),409,'changed preparer identity requires fresh calculation');}finally{$host->rollBack();}
$host->beginTransaction();try{$actor=$select('finance_review','accounting');$rules=app(FiscalRules::class);$rule=$host->table('fr_events')->where('id',$ruleEvent)->first();$revision=(int)$host->table('fr_events')->where($f['scope'])->where('rule_key',$rule->rule_key)->max('revision');$data=['action'=>'withdraw','rule_key'=>$rule->rule_key,'revision'=>$revision,'target_id'=>$ruleEvent,'data'=>[],'reason'=>'Retirada de regra apenas para testar a fronteira documental em rollback.'];$withdraw=$rules->preview($actor,$data,(string)Str::uuid());$rules->confirm($actor,$withdraw['preview']);$actor=$select('commercial_review');$reject(fn()=>DB::transaction(fn()=>$calcWriter->current($actor,$calc['id'],$x['draft']['id'])),409,'withdrawn common rule blocks existing calculation');}finally{$host->rollBack();}
$actor=$select('finance_prepare');$pendingCut=$calcWriter->preview($actor,$input,(string)Str::uuid());$actor=$select('commercial_prepare');$edit=$prototype;$edit['draft_id']=$x['draft']['id'];$edit['version']=1;$edit['source_version']=1;$edit['preparation_version']=1;$edit['name']='QA-MULTI-EDIT';$edit['price_mode']='added';$edit['period_start']='2026-11-01';$edit['period_end']='2026-11-30';$edit['lines']=[['service_id'=>$service,'description'=>'Linha400','quantity'=>'1','unit_price'=>'400','unit'=>'UN'],['service_id'=>$service,'description'=>'Linha601','quantity'=>'1','unit_price'=>'601','unit'=>'UN']];$e=$draftWriter->preview($actor,$edit,(string)Str::uuid());$updated=$draftWriter->confirm($actor,$e['preview']);$ok($updated['version']===2&&$updated['source_version']===2&&$updated['preparation_version']===2,'composed edit updates all own versions atomically');$actor=$select('finance_prepare');$reject(fn()=>$calcWriter->confirm($actor,$pendingCut['preview']),409,'unconfirmed old preview refuses changed source');$prior=$calcWriter->confirm($actor,$cut['preview']);$ok($prior['replayed']&&$prior['id']===$calc['id'],'previous successful confirmation remains historical recovery, not a new calculation');$actor=$select('commercial_review');$reject(fn()=>DB::transaction(fn()=>$calcWriter->current($actor,$calc['id'],$x['draft']['id'])),409,'old calculation cannot validate changed line');
$select('read_only');$actor=$profiles['read_only']['user_id'];$reject(fn()=>$draftWriter->preview($actor,$prototype,(string)Str::uuid()),403,'read only cannot prepare new draft');$select('commercial_prepare','finance');$reject(fn()=>$draftWriter->preview($profiles['commercial_prepare']['user_id'],$prototype,(string)Str::uuid()),403,'wrong module cannot prepare commercial draft');
// New source calculation and PDF limit sample use the same writers, then roll back all data.
$x=$fixture('added',array_fill(0,25,'1'),'PDF25');$actor=$select('finance_prepare');$input=['draft_id'=>$x['draft']['id'],'draft_version'=>1,'preparation_version'=>1,'rule_event_id'=>$ruleEvent,'reason'=>'Conferência do limite25 por linhas, sem outra instalação.'];$cut=$calcWriter->preview($actor,$input,(string)Str::uuid());$max=$calcWriter->confirm($actor,$cut['preview']);$ok(count($max['result']['lines'])===25&&$max['result']['total']==='29.00','25 lines use common truncation and sum29');
$snp=DB::table('mi_calculations')->where('id',$max['id'])->value('snapshot');$snp=json_decode($snp,true);$doc=['renderer'=>MultiLineInvoicePdf::VERSION,'calculation_policy'=>MultiLineInvoiceCalculation::POLICY,'series_code'=>'TEST-MULTI-PDF25','number'=>0,'issued_local'=>'2026-11-11 12:00:00','timezone'=>ServiceInvoicing::ZONE,'document'=>$snp['document'],'calculation'=>['common_document_result'=>$max['result']],'profile_identifier'=>$snp['common']['rule']['rule_key'],'profile_version'=>$snp['common']['rule']['rule_version'],'rate_percent'=>'16','draft_id'=>$x['draft']['id'],'rule_id'=>0,'rule_version'=>0];$lines=MultiLineInvoicePdf::lines($doc);$ok(InvoicePdf::supports($lines)&&count(array_filter($lines,fn($l)=>$l[0]==='section'&&str_starts_with($l[1],'Linha ')))===25,'PDF projection includes all25 lines');$pdf=app(InvoicePdf::class)->render($doc);file_put_contents($work.'/multiline-25-lines-private.pdf',$pdf);$ok(substr_count($pdf,'/Type /Page ')>1&&str_starts_with($pdf,'%PDF-1.4'),'25 line PDF paginated with preserved test marks');
}finally{$host->rollBack();}
file_put_contents($work.'/multiline-invoice-boundaries.json',json_encode(['passed'=>true,'checks'=>$checks,'fixtures_rolled_back'=>true,'new_grants'=>0,'new_demo'=>false,'pdf_sample_not_issued'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/multiline-invoice-boundaries.json','pdf_sample'=>$work.'/multiline-25-lines-private.pdf']).PHP_EOL;
@@ -0,0 +1,2 @@
import fs from 'node:fs';import assert from 'node:assert/strict';import {spawn} from 'node:child_process';
const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-b4093c6f6707',directory=root+'/multiline-concurrency-'+Date.now();fs.mkdirSync(directory);const start=mode=>{const child=spawn('C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',['scripts/sgf-multiline-invoice-concurrency.php',mode,directory],{windowsHide:true});let output='';child.stdout.on('data',v=>output+=v);child.stderr.on('data',v=>output+=v);return{child,done:new Promise(resolve=>child.on('close',code=>resolve({code,output})))};};let result=await start('prepare').done;assert.equal(result.code,0,result.output);const first=start('first');try{const end=Date.now()+8000;while(!fs.existsSync(directory+'/locked.json')&&Date.now()<end)await new Promise(r=>setTimeout(r,30));assert(fs.existsSync(directory+'/locked.json'));const waiting=start('second');await new Promise(r=>setTimeout(r,900));const inspected=await start('inspect').done;assert.equal(inspected.code,0,inspected.output);const second=await waiting.done;assert.equal(second.code,0,second.output);const busy=JSON.parse(fs.readFileSync(directory+'/second.json'));assert.equal(busy.status,503);fs.writeFileSync(directory+'/release','1');result=await first.done;assert.equal(result.code,0,result.output);result=await start('recover').done;assert.equal(result.code,0,result.output);const recovery=JSON.parse(fs.readFileSync(directory+'/recovery.json'));assert(recovery.passed,result.output);const blocking=JSON.parse(fs.readFileSync(directory+'/blocking.json'));assert(blocking.waiters.some(w=>w.wait_event_type==='Lock'));const proof=directory+'/result.json';fs.writeFileSync(proof,JSON.stringify({...recovery,during_lock:503,seconds_waiting:busy.seconds,blocking},null,2));console.log(JSON.stringify({passed:true,proof}));}finally{fs.writeFileSync(directory+'/release','1');await first.done;}
@@ -0,0 +1,11 @@
<?php
use App\Modules\Services\{MultiLineInvoiceCalculation,ServiceInvoicing};use Illuminate\Support\Str;
require __DIR__.'/sgf-multiline-invoice-proof-bootstrap.php';$mode=$argv[1]??'';$directory=$argv[2]??'';if($book!=='regression'||!str_starts_with(str_replace('\\','/',$directory),str_replace('\\','/',$s['root']).'/multiline-concurrency-'))throw new LogicException('Regression only');
if($mode==='prepare'){$x=$fixture('added',['400','600'],'CONCURRENT');$result=['id'=>$x['draft']['id']];
$c=app(MultiLineInvoiceCalculation::class);$actor=$select('finance_prepare');$p=$c->preview($actor,['draft_id'=>$result['id'],'draft_version'=>1,'preparation_version'=>1,'rule_event_id'=>$ruleEvent,'reason'=>'Cálculo do rascunho recuperado explicitamente.'],(string)Str::uuid());$calc=$c->confirm($actor,$p['preview']);$actor=$select('commercial_prepare');$e=app(ServiceInvoicing::class);$code='TEST-MULTI-FAIL-'.strtoupper(substr((string)Str::uuid(),0,8));$series=$e->propose($actor,['kind'=>'series','identifier'=>$code,'data'=>['code'=>$code,'first_number'=>1,'state'=>'open','valid_from'=>'2026-11-01','valid_until'=>'2026-11-30','evidence'=>'Série artificicial de recuperação.']]);$temporal=$e->propose($actor,['kind'=>'temporal','identifier'=>'MULTI-FAIL-'.$result['id'],'data'=>['calculation_policy'=>MultiLineInvoiceCalculation::POLICY,'common_calculation_id'=>$calc['id'],'common_rule_event_id'=>$ruleEvent,'draft_id'=>$result['id'],'draft_version'=>1,'preparation_version'=>1,'valid_from'=>'2026-11-01','valid_until'=>'2026-11-30','period_start'=>'2026-11-01','period_end'=>'2026-11-30','issue_window_start'=>'2026-11-11','issue_window_end'=>'2026-11-11','event'=>'on_issue_within_deadline','readiness_basis'=>'Documento recuperado com cálculo actual.','evidence'=>'Origem e valores fictícios conferidos.']]);$actor=$select('commercial_review');foreach([$series,$temporal]as$p)$e->decide($p['id'],$actor,['version'=>1,'decision'=>'approved','reason'=>'Revisão independente do documento de recuperação.']);$actor=$select('commercial_prepare');$ready=$e->review($result['id'],$series['id'],$temporal['id'],$actor);$input=['series_id'=>$series['id'],'rule_id'=>$temporal['id'],'review_hash'=>$ready['review_hash']];$key=(string)Str::uuid();
file_put_contents($directory.'/cuts-private.json',json_encode(['draft'=>$result['id'],'input'=>$input,'first'=>$key,'second'=>(string)Str::uuid(),'counter'=>$code,'before'=>$host->table('si_invoices')->count()]));exit;}
if($mode==='inspect'){$pid=json_decode(file_get_contents($directory.'/locked.json'),true)['host_pid'];$rows=$host->select('SELECT pid,wait_event_type,wait_event,pg_blocking_pids(pid) blockers FROM pg_stat_activity WHERE ? = ANY(pg_blocking_pids(pid))',[$pid]);file_put_contents($directory.'/blocking.json',json_encode(['blocker_host_pid'=>$pid,'waiters'=>$rows,'company_for_update'=>true]));exit;}
$p=json_decode(file_get_contents($directory.'/cuts-private.json'),true);$actor=$select('commercial_prepare');$http=function($key)use($p){request()->request->replace($p['input']);request()->headers->set('Idempotency-Key',$key);try{$r=app(App\Modules\Services\Http\MultiLineInvoiceController::class)->issue(request(),$p['draft']);return['status'=>$r->getStatusCode(),'value'=>$r->getData(true)];}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface$e){return['status'=>$e->getStatusCode()];}};
if($mode==='first'){$host->beginTransaction();try{$out=$http($p['first']);if($out['status']!==200)throw new LogicException('First issuance failed');file_put_contents($directory.'/locked.json',json_encode(['host_pid'=>$host->selectOne('SELECT pg_backend_pid() pid')->pid]));$end=microtime(true)+25;while(!is_file($directory.'/release')&&microtime(true)<$end)usleep(50000);if(!is_file($directory.'/release'))throw new LogicException('Release missing');$host->commit();}finally{while($host->transactionLevel()>0)$host->rollBack();}exit;}
if($mode==='second'){$start=microtime(true);$out=$http($p['second']);file_put_contents($directory.'/second.json',json_encode(['status'=>$out['status'],'seconds'=>microtime(true)-$start]));exit;}
if($mode==='recover'){$second=$http($p['second']);$replay=$http($p['first']);$invoices=$host->table('si_invoices')->where('draft_id',$p['draft'])->count();$commands=$host->table('si_commands')->whereIn('command_key',[$p['first'],$p['second']])->count();$counter=(int)$host->table('si_counters')->where('code',$p['counter'])->value('last_number');$out=['passed'=>$second['status']===409&&$replay['status']===200&&$replay['value']['replayed']&&$invoices===1&&$commands===1&&$counter===1&&$host->table('si_invoices')->count()===$p['before']+1,'after_release'=>$second['status'],'original_replay'=>$replay['status'],'invoices'=>$invoices,'receipts'=>$commands,'last_number'=>$counter,'regression_only'=>true,'pending_receivable_and_journal'=>true];file_put_contents($directory.'/recovery.json',json_encode($out));echo json_encode($out).PHP_EOL;}
@@ -0,0 +1,12 @@
<?php
use App\Modules\Services\{MultiLineInvoiceCalculation,MultiLineDrafts,ServiceInvoicing};use App\Modules\Services\Http\MultiLineInvoiceController;use Illuminate\Support\Str;
require __DIR__.'/sgf-multiline-invoice-proof-bootstrap.php';$controller=app(MultiLineInvoiceController::class);$http=function($role,$method,$input=[],?string $key=null,$id=null)use($select,$controller){$select($role);$get=in_array($method,['catalogue','editor','receipt','issueReceipt']);request()->query->replace($get?$input:[]);request()->request->replace($get?[]:$input);request()->headers->set('Idempotency-Key',$key??(string)Str::uuid());$response=match($method){'catalogue'=>$controller->catalogue(request(),app(MultiLineInvoiceCalculation::class)),'editor'=>$controller->editor(request(),$id,app(MultiLineInvoiceCalculation::class)),'draftPreview'=>$controller->draftPreview(request(),app(MultiLineDrafts::class)),'draftConfirm'=>$controller->draftConfirm(request(),app(MultiLineDrafts::class)),'calculationPreview'=>$controller->calculationPreview(request(),app(MultiLineInvoiceCalculation::class)),'calculationConfirm'=>$controller->calculationConfirm(request(),app(MultiLineInvoiceCalculation::class)),'receipt'=>$controller->receipt(request(),$key,app(MultiLineInvoiceCalculation::class)),'issueReceipt'=>$controller->issueReceipt(request(),$key,app(MultiLineInvoiceCalculation::class)),'temporalReview'=>$controller->temporalReview(request(),$id),'readiness'=>$controller->readiness(request(),$id),'issue'=>$controller->issue(request(),$id),default=>$controller->$method(request())};return ['status'=>$response->getStatusCode(),'value'=>$response->getData(true)];};
$host->beginTransaction();try{
$x=$fixture('added',['400','600'],'CONTROL');$id=$x['draft']['id'];$v=$http('commercial_prepare','editor',[],null,$id)['value'];$ok($v['billing']['id']===$id&&count($v['billing']['data']['lines'])===2&&$v['preparation']['version']===1,'single editor retrieves header lines and original versions');$reject(fn()=>$http('commercial_prepare','editor',[],null,(int)$oldDraft->id),403,'new editor does not adopt old draft');$reject(fn()=>$http('read_only','draftPreview',[]),422,'empty body refused before mutation');$reject(fn()=>$http('commercial_prepare','catalogue',['kind'=>'drafts','tenant_id'=>2]),422,'tenant override refused');$reject(fn()=>$http('commercial_prepare','catalogue',['kind'=>'drafts','per_page'=>1000]),422,'unbounded catalogue page refused');$reject(fn()=>$http('commercial_prepare','catalogue',['kind'=>'rules']),403,'commercial reader cannot enumerate accounting rule catalogue');
$input=['draft_id'=>$id,'rule_event_id'=>$ruleEvent,'draft_version'=>1,'preparation_version'=>1,'reason'=>'Cálculo explícito do documento actual pelo preparador fiscal.'];$reject(fn()=>$http('commercial_prepare','calculationPreview',$input),403,'draft preparer cannot acquire fiscal calculation preparation authority');$cut=$http('finance_prepare','calculationPreview',$input)['value'];$calc=$http('finance_prepare','calculationConfirm',['preview'=>$cut['preview']])['value'];$ok($calc['result']['total']==='1160.00'&&$calc['draft_version']===1,'fiscal preparer existing powers calculate current draft');
$key=(string)Str::uuid();$body=['kind'=>'temporal','identifier'=>'MULTI-CONTROL-'.$id,'data'=>['calculation_policy'=>MultiLineInvoiceCalculation::POLICY,'common_calculation_id'=>$calc['id'],'common_rule_event_id'=>$ruleEvent,'draft_id'=>$id,'draft_version'=>1,'preparation_version'=>1,'valid_from'=>'2026-11-01','valid_until'=>'2026-11-30','period_start'=>'2026-11-01','period_end'=>'2026-11-30','issue_window_start'=>'2026-11-11','issue_window_end'=>'2026-11-11','event'=>'on_issue_within_deadline','readiness_basis'=>'Prontidão artificial do documento/linhas escolhidos.','evidence'=>'Origem fiscal revista ligada individualmente ao documento.']];$first=$http('commercial_prepare','temporal',$body,$key)['value'];$second=$http('commercial_prepare','temporal',$body,$key)['value'];$ok($second['replayed']&&$second['id']===$first['id'],'temporal command receipt prevents duplicate proposal');$receipt=$http('commercial_prepare','receipt',[],$key)['value'];$ok($receipt['historical_receipt_only']&&$receipt['result']['id']===$first['id'],'own temporal receipt recovers without execution');$reject(fn()=>$http('commercial_review','receipt',[],$key),404,'another actor receipt hidden');$reject(fn()=>$http('commercial_prepare','temporalReview',['version'=>1,'decision'=>'approved','reason'=>'Tentativa de revisão própria proibida.'],null,$first['id']),403,'own temporal approval denied');$reviewKey=(string)Str::uuid();$review=['version'=>1,'decision'=>'approved','reason'=>'Revisão distinta do cálculo, linhas e janela de emissão.'];$one=$http('commercial_review','temporalReview',$review,$reviewKey,$first['id'])['value'];$two=$http('commercial_review','temporalReview',$review,$reviewKey,$first['id'])['value'];$ok($two['replayed']&&$two['id']===$one['id'],'temporal review receipt prevents duplicate decision');
$all=$http('commercial_prepare','catalogue',['kind'=>'temporals','draft_id'=>$id,'per_page'=>25])['value'];$ok(count($all['records']['data'])===1&&$all['records']['data'][0]['decision']['decision']==='approved','current decision batched into page');$far=$http('commercial_prepare','catalogue',['kind'=>'temporals','draft_id'=>$id,'page'=>100000,'per_page'=>100])['value'];$ok($far['records']['data']===[],'far page remains empty without fallback');
for($n=0;$n<27;$n++){$d=$host->table('mi_calculations')->where('id',$calc['id'])->first();$a=(array)$d;unset($a['id']);$a['fingerprint']=hash('sha256','mi-pagination-'.$n);$host->table('mi_calculations')->insert($a);}$a=$http('read_only','catalogue',['kind'=>'calculations','draft_id'=>$id,'page'=>1,'per_page'=>25])['value']['records'];$b=$http('read_only','catalogue',['kind'=>'calculations','draft_id'=>$id,'page'=>2,'per_page'=>25])['value']['records'];$c=$http('read_only','catalogue',['kind'=>'calculations','draft_id'=>$id,'page'=>1,'per_page'=>100])['value']['records'];$ok(count($a['data'])===25&&count($b['data'])===3&&$c['total']===28,'calculation choices SQL25/100 beyond first page');$ok(!array_intersect(array_column($a['data'],'id'),array_column($b['data'],'id')),'source page has no overlap');
$actor=$select('finance_prepare');$last=$host->table('mi_calculations')->where('id',$calc['id'])->first();$host->unprepared('ALTER TABLE mi_calculations DISABLE TRIGGER mi_calculations_immutable');$host->table('mi_calculations')->where('id',$calc['id'])->update(['unit_id'=>1]);$host->unprepared('ALTER TABLE mi_calculations ENABLE TRIGGER mi_calculations_immutable');$reject(fn()=>DB::transaction(fn()=>app(MultiLineInvoiceCalculation::class)->current($actor,$calc['id'],$id)),403,'known calculation id in another unit refused');
}finally{$host->rollBack();}
file_put_contents($work.'/multiline-invoice-controller.json',json_encode(['passed'=>true,'checks'=>$checks,'fixtures_rolled_back'=>true,'new_grants'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/multiline-invoice-controller.json']).PHP_EOL;
@@ -0,0 +1,2 @@
import fs from'node:fs';import assert from'node:assert/strict';import crypto from'node:crypto';import{session}from'./sgf-single-http.mjs';const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);},c=await session('sgf:commercial_prepare','commercial'),f=await session('sgf:finance_prepare','finance'),a=await session('sgf:accounting_prepare','accounting');const get=async(s,p)=>{const r=await s.get(p);assert.equal(r.status,200,p);return r.json();};const x=await get(c,'/api/service-documents/register?kind=invoice&series=TEST-MULTI-2026&per_page=25&page=1');ok(x.documents.total===2&&x.documents.data.every(r=>r.amount==='1160.00'),'Commercial register lists exactly two new1160 documents');const larger=await get(c,'/api/service-documents/register?kind=invoice&series=TEST-MULTI-2026&per_page=100&page=1');ok(JSON.stringify(x.documents.data)===JSON.stringify(larger.documents.data),'register25/100 same page records');const p=await get(f,'/api/service-receivables/positions?reference_on=2026-11-11&search=TEST-MULTI-2026&per_page=25');ok(p.positions.total===2&&p.totals.recognized_balance==='2320.00'&&p.positions.data.every(r=>r.balance==='1160.00'&&r.allocated==='0.00'),'Finance positions two uncollected1160 total2320');ok(p.positions.data.every(r=>r.due_state==='unknown'),'draft proposed date does not silently become agreed receivable due date');const progress=JSON.parse(fs.readFileSync(root+'/multiline-invoice-http-progress-private.json')).steps;for(const mode of['added','included']){const id=progress[mode+'Issue'].result.id,d=await get(a,'/api/general-ledger/sales?invoice_id='+id);ok(JSON.stringify(d).includes('TEST-MULTI-2026'),'accounting source workspace resolves exact '+mode);const receipt=await c.get('/api/multiline-invoices/invoice-receipts/'+progress[mode+'Issue'].key);ok(receipt.status===200&&(await receipt.json()).result.id===id,'read-only original issuance receipt '+mode);const wrong=await f.get('/api/service-invoices/'+id);ok(wrong.status===409,'direct Commercial API requires explicit module switch '+mode);const draft=await get(c,'/api/multiline-invoices/drafts/'+progress[mode+'Draft'].result.id);ok(draft.invoice.id===id&&draft.invoice.totals.total==='1160.00','issued editor photograph readable '+mode);}
const file=root+'/multiline-invoice-final-read-http-20261111.json';fs.writeFileSync(file,JSON.stringify({passed:true,checks,business_commands:0,new_demo:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file}));
@@ -0,0 +1,15 @@
<?php
use App\Modules\Services\{ServiceInvoicing,MultiLineInvoiceCalculation};use Illuminate\Support\Str;
require __DIR__.'/sgf-multiline-invoice-proof-bootstrap.php';$host->beginTransaction();$native->beginTransaction();try{
$actor=$select('commercial_prepare');$engine=app(ServiceInvoicing::class);$tag=strtoupper(substr((string)Str::uuid(),0,8));$series=$engine->propose($actor,['kind'=>'series','identifier'=>'TEST-MULTI-REG-'.$tag,'data'=>['code'=>'TEST-MULTI-REG-'.$tag,'first_number'=>1,'state'=>'open','valid_from'=>'2026-11-01','valid_until'=>'2026-11-30','evidence'=>'Série artificial de regressão do ciclo multilinha.']]);$actor=$select('commercial_review');$engine->decide($series['id'],$actor,['version'=>1,'decision'=>'approved','reason'=>'Revisão independente da série fictícia multilinha.']);
foreach([['added',['400','600']],['included',['464','696']],['included',['100']]]as[$mode,$prices]){
$x=$fixture($mode,$prices,$tag);$actor=$select('finance_prepare');$c=app(MultiLineInvoiceCalculation::class);$in=['draft_id'=>$x['draft']['id'],'rule_event_id'=>$ruleEvent,'draft_version'=>1,'preparation_version'=>$x['preparation']['version'],'reason'=>'Cálculo individual de linhas e modalidade pela regra QA revista.'];$cut=$c->preview($actor,$in,(string)Str::uuid());$calc=$c->confirm($actor,$cut['preview']);$ok(($prices===['100']?$calc['result']['base_document']==='86.21'&&$calc['result']['tax_document']==='13.79'&&$calc['result']['total']==='100.00':$calc['result']['base_document']==='1000.00'&&$calc['result']['tax_document']==='160.00'&&$calc['result']['total']==='1160.00'),'shared math exact '.$mode);$replay=$c->confirm($actor,$cut['preview']);$ok($replay['replayed']&&$replay['id']===$calc['id'],'calculation replay no duplicate '.$mode);
$actor=$select('commercial_prepare');$data=['calculation_policy'=>MultiLineInvoiceCalculation::POLICY,'common_calculation_id'=>$calc['id'],'common_rule_event_id'=>$ruleEvent,'draft_id'=>$x['draft']['id'],'draft_version'=>1,'preparation_version'=>$x['preparation']['version'],'valid_from'=>'2026-11-01','valid_until'=>'2026-11-30','period_start'=>'2026-11-01','period_end'=>'2026-11-30','issue_window_start'=>'2026-11-11','issue_window_end'=>'2026-11-11','event'=>'on_issue_within_deadline','readiness_basis'=>'Origem, linhas e modalidade explícitos; prontidão apenas artificial.','evidence'=>'Conferência temporal individual de emissão QA multilinha.'];$temporal=$engine->propose($actor,['kind'=>'temporal','identifier'=>'MULTI-REG-'.$tag.'-'.$mode.'-'.count($prices),'data'=>$data]);$actor=$select('commercial_review');$engine->decide($temporal['id'],$actor,['version'=>1,'decision'=>'approved','reason'=>'Revisão distinta das linhas, valores e prontidão artificial.']);$actor=$select('commercial_prepare');$review=$engine->review($x['draft']['id'],$series['id'],$temporal['id'],$actor);$ok($review['ready'],'review current and ready '.$mode);$key=(string)Str::uuid();$input=['series_id'=>$series['id'],'rule_id'=>$temporal['id'],'review_hash'=>$review['review_hash']];$invoice=$engine->issue($x['draft']['id'],$actor,$input,$key);$again=$engine->issue($x['draft']['id'],$actor,$input,$key);$ok($again['replayed']&&$again['id']===$invoice['id'],'invoice key recovers number without duplicate '.$mode);$shown=$engine->show($invoice['id'],$actor);$pdf=$engine->pdf($invoice['id'],$actor);$ok(hash('sha256',$pdf)===$shown['pdf_sha256']&&count(array_filter($shown['document_lines'],fn($l)=>$l[0]==='section'&&str_starts_with($l[1],'Linha ')))===count($prices),'frozen PDF includes both lines '.$mode);
$sr=app(App\Modules\Services\ServiceReceivables::class);$actor=$select('finance_prepare','finance');$ob=$sr->propose($actor,['kind'=>'obligation','invoice_id'=>$invoice['id'],'currency'=>'MZN','source'=>'SIM-MULTI-REG','external_id'=>'MULTI-OB-'.$invoice['id'],'revision'=>0],(string)Str::uuid());$actor=$select('finance_review','finance');$sr->decide($ob['id'],$actor,['version'=>1,'revision'=>0,'decision'=>'approved','reason'=>'Abertura distinta do recebível multilinha sem recebimento.'],(string)Str::uuid());$position=$sr->position($ob['id'],$actor);$expected=$calc['result']['total'];$ok($position['balance']===$expected,'qualified commercial invoice opens correct receivable in finance '.$mode.' '.count($prices));
$p=$call('accounting_prepare','prepare',fn($e)=>$e->command('effect-prepare',['effect_type'=>'sales.invoice','source_id'=>$invoice['id'],'journal_on'=>'2026-11-11','period_key'=>'QA-2026-INTEGRADO','rule_key'=>'sales.invoice','reason'=>'Origem multilinha actual do mesmo SGF, sem copiar dívida.'],(string)Str::uuid()));$call('accounting_review','validate',fn($e)=>$e->command('validate',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'decision'=>'approved','reason'=>'Conferência distinta dos totais documentais, base e imposto.'],(string)Str::uuid()));$j=$call('accounting_prepare','post',fn($e)=>$e->command('post',['proposal_id'=>$p['proposal_id'],'revision'=>$p['revision'],'reason'=>'Aplicar uma única vez a origem documental multilinha revista.'],(string)Str::uuid()));$lines=$native->table('gl_lines')->where('journal_id',$j['journal_id'])->orderBy('line_no')->get();$ok($lines->count()===3&&$lines[1]->credit_minor===(int)str_replace('.','',$calc['result']['base_document'])&&$lines[2]->credit_minor===(int)str_replace('.','',$calc['result']['tax_document']),'same three-line writer uses documentary base and tax '.$mode.' '.count($prices));
$associations=$host->table('identity_audit')->where('action','module.record.associated')->where('details->area','billing_drafts')->where('details->record_id',$x['draft']['id'])->pluck('details')->map(fn($v)=>json_decode($v,true)['module_id'])->all();$ok($associations===['commercial'],'financial dependency read does not assign draft to other modules');
}
$native->statement('SET CONSTRAINTS ALL IMMEDIATE');$ok(true,'native constraints validate all three qualified invoice journals');
$host->statement('SET CONSTRAINTS ALL IMMEDIATE');$ok(true,'host invoice constraints accept new typed photographs');
}finally{$native->rollBack();$host->rollBack();}file_put_contents($work.'/multiline-invoice-financial.json',json_encode(['passed'=>true,'checks'=>$checks,'fixtures_rolled_back'=>true,'new_demo'=>false,'new_grants'=>0,'new_accounts'=>0,'regression_only'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/multiline-invoice-financial.json']).PHP_EOL;
+24
View File
@@ -0,0 +1,24 @@
import fs from'node:fs';import assert from'node:assert/strict';import crypto from'node:crypto';import{session}from'./sgf-single-http.mjs';
const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',file=root+'/multiline-invoice-http-progress-private.json',saved=fs.existsSync(file)?JSON.parse(fs.readFileSync(file)):{steps:{}},save=()=>fs.writeFileSync(file,JSON.stringify(saved)),refs=JSON.parse(fs.readFileSync(root+'/multiline-invoice-inputs-private.json')),checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);},base='/api/multiline-invoices',policy='SGF-MULTI-LINE-INVOICE-INTEGRATED-QA-v1';save();
const commercial=await session('sgf:commercial_prepare','commercial'),review=await session('sgf:commercial_review','commercial'),fiscal=await session('sgf:finance_prepare','commercial'),fp=await session('sgf:finance_prepare','finance'),fr=await session('sgf:finance_review','finance'),ap=await session('sgf:accounting_prepare','accounting'),ar=await session('sgf:accounting_review','accounting');
const get=async(s,p)=>{const r=await s.get(p);assert.equal(r.status,200,p);return r.json();};ok(commercial.identity.context.multiline_invoices_enabled===true,'same8085 typed editor enabled');
async function direct(name,s,path,body,{receipt=null}={}){const step=saved.steps[name]??={key:crypto.randomUUID(),body,path};save();assert.equal(JSON.stringify(step.body),JSON.stringify(body),'stable request '+name);if(step.result)return step.result;if(receipt){const r=await s.get(receipt+step.key);if(r.status===200){step.result=(await r.json()).result;save();return step.result;}assert.equal(r.status,404);}const r=await s.command(path,body,step.key);assert.equal(r.status,200,JSON.stringify(r.value));step.result=r.value;save();ok(true,name+' one explicit command');return r.value;}
async function composed(name,s,domain,body){const step=saved.steps[name]??={key:crypto.randomUUID(),body};save();assert.equal(JSON.stringify(step.body),JSON.stringify(body));if(step.result)return step.result;const receipt=domain==='ledger'?'/api/general-ledger/receipts/':base+'/receipts/';const old=await s.get(receipt+step.key);if(old.status===200){step.result=(await old.json()).result;save();return step.result;}assert.equal(old.status,404);if(domain==='ledger'){const out=await s.command('/api/general-ledger/'+body.action,body.data,step.key);assert.equal(out.status,200,JSON.stringify(out.value));step.result=out.value;save();ok(true,name+' existing individual ledger command');return out.value;}const b=base+'/'+domain;const cut=await s.command(b+'/preview',body,step.key);assert.equal(cut.status,200,JSON.stringify(cut.value));step.cut=cut.value;save();const out=await s.command(b+'/confirm',{preview:cut.value.preview},step.key);assert.equal(out.status,200,JSON.stringify(out.value));step.result=out.value;save();ok(true,name+' explicit preview and confirmation');return out.value;}
// Existing series administration is recoverable by its exact new identity, without a second proposal.
if(!saved.steps.seriesPrepare?.result){const state=await get(commercial,'/api/service-invoices'),matches=state.proposals.filter(p=>p.kind==='series'&&p.identifier==='TEST-MULTI-2026');assert(matches.length<=1);if(matches.length){saved.steps.seriesPrepare??={};saved.steps.seriesPrepare.result={id:matches[0].id,version:matches[0].version};save();}}
const series=saved.steps.seriesPrepare?.result??await direct('seriesPrepare',commercial,'/api/service-invoices/proposals',{kind:'series',identifier:'TEST-MULTI-2026',data:{code:'TEST-MULTI-2026',first_number:1,state:'open',valid_from:'2026-11-01',valid_until:'2026-11-30',evidence:'Série QA expressamente autorizada para dois documentos multilinha.'}});
if(!saved.steps.seriesReview?.result){const state=await get(review,'/api/service-invoices'),row=state.proposals.find(p=>p.id===series.id);if(row?.decision){assert.equal(row.decision.decision,'approved');saved.steps.seriesReview={result:{id:series.id,decision:'approved'}};save();}}
if(!saved.steps.seriesReview?.result)await direct('seriesReview',review,'/api/service-invoices/proposals/'+series.id+'/decision',{version:1,decision:'approved',reason:'Revisão independente da série QA e seu primeiro número, sem reiniciar séries anteriores.'});
for(const [mode,prices]of[['added',['400','600']],['included',['464','696']]]){
const draft=await composed(mode+'Draft',commercial,'drafts',{draft_id:null,version:0,source_version:0,preparation_version:0,name:mode==='added'?'QA-MULTI-ACRESCIDO-01':'QA-MULTI-INCLUIDO-01',client_id:refs.client_id,currency:'MZN',price_mode:mode,service_date:'2026-11-11',period_start:'2026-11-01',period_end:'2026-11-30',proposed_due_date:'2026-11-30',payment_terms:'Vencimento artificial30Nov, sem recebimento ou condição legal presumida.',lines:prices.map((p,i)=>({service_id:refs.service_id,description:'Serviço fictício da linha '+(i+1),quantity:'1',unit_price:p,unit:'UN'})),reason:'Preparação individual das duas linhas QA autorizadas e respectiva modalidade.'});
const calc=await composed(mode+'Calculation',fiscal,'calculations',{draft_id:draft.id,rule_event_id:refs.rule_event_id,draft_version:draft.version,preparation_version:draft.preparation_version,reason:'Conferência fiscal individual das linhas e da regra artificial16% já revista.'});ok(calc.result.base_document==='1000.00'&&calc.result.tax_document==='160.00'&&calc.result.total==='1160.00','documentary totals1000+160=1160 '+mode);
const temporal=await direct(mode+'Temporal',commercial,base+'/temporals',{kind:'temporal',identifier:'MULTI-DRAFT-'+draft.id,data:{calculation_policy:policy,common_calculation_id:calc.id,common_rule_event_id:refs.rule_event_id,draft_id:draft.id,draft_version:draft.version,preparation_version:draft.preparation_version,valid_from:'2026-11-11',valid_until:'2026-11-11',period_start:'2026-11-01',period_end:'2026-11-30',issue_window_start:'2026-11-11',issue_window_end:'2026-11-11',event:'on_issue_within_deadline',readiness_basis:'Origem, duas linhas e modalidade QA explicitamente conferidas antes de emitir.',evidence:'Autorização11Nov; emissão exclusivamente de ensaio sem validade fiscal.'}},{receipt:base+'/receipts/'});
await direct(mode+'TemporalReview',review,base+'/temporals/'+temporal.id+'/decision',{version:1,decision:'approved',reason:'Conferência independente da origem, cálculo conservado e janela de emissão QA.'},{receipt:base+'/receipts/'});
let invoice=saved.steps[mode+'Issue']?.result;if(!invoice){const cut=await commercial.command(base+'/drafts/'+draft.id+'/readiness',{series_id:series.id,rule_id:temporal.id});assert.equal(cut.status,200,JSON.stringify(cut.value));assert(cut.value.ready);invoice=await direct(mode+'Issue',commercial,base+'/drafts/'+draft.id+'/issue',{series_id:series.id,rule_id:temporal.id,review_hash:cut.value.review_hash},{receipt:base+'/invoice-receipts/'});}ok(invoice.series_code==='TEST-MULTI-2026'&&invoice.number===(mode==='added'?1:2),'new series exact numbering '+mode);
const shown=await get(commercial,'/api/service-invoices/'+invoice.id);ok(shown.snapshot.calculation_policy===policy&&shown.snapshot.calculation.common_document_result.total==='1160.00','immutable typed photograph '+mode);const pdf=await commercial.get('/api/service-invoices/'+invoice.id+'/pdf'),bytes=Buffer.from(await pdf.arrayBuffer());ok(pdf.status===200&&/application\/pdf/.test(pdf.headers.get('content-type'))&&crypto.createHash('sha256').update(bytes).digest('hex')===shown.pdf_sha256,'actual PDF MIME and stored SHA '+mode);fs.writeFileSync(root+'/multiline-'+mode+'-invoice-private.pdf',bytes);
const ob=await direct(mode+'Obligation',fp,'/api/service-receivables/proposals',{kind:'obligation',invoice_id:invoice.id,currency:'MZN',source:'SIM-MULTI-8085',external_id:'MULTI-8085-'+invoice.id,revision:0});await direct(mode+'ObligationReview',fr,'/api/service-receivables/'+ob.id+'/decision',{version:1,revision:0,decision:'approved',reason:'Reconhecimento distinto do recebível fictício1160, sem recebimento nem afectação.'});
const proposal=await composed(mode+'JournalPrepare',ap,'ledger',{action:'effect-prepare',data:{effect_type:'sales.invoice',source_id:invoice.id,journal_on:'2026-11-11',period_key:'QA-2026-INTEGRADO',rule_key:'sales.invoice',reason:'Preparação individual da origem multilinha QA no mesmo diário.'}});await composed(mode+'JournalReview',ar,'ledger',{action:'validate',data:{proposal_id:proposal.proposal_id,revision:proposal.revision,decision:'approved',reason:'Revisão distinta dos três movimentos1160/1000/160 e origem documental.'}});await composed(mode+'JournalPost',ap,'ledger',{action:'post',data:{proposal_id:proposal.proposal_id,revision:proposal.revision,reason:'Aplicação única do documento QA conferido no diário existente.'}});
const editor=await get(commercial,base+'/drafts/'+draft.id);ok(editor.invoice?.id===invoice.id&&editor.invoice.totals.total==='1160.00','unified editor retains issued totals '+mode);const financial=await get(fp,'/api/service-receivables');ok(financial.documents.some(d=>d.id===invoice.id),'finance reads qualified Commercial dependency in its own workspace '+mode);
}
const noRules=await commercial.get(base+'/options?kind=rules');ok(noRules.status===403,'commercial preparation cannot enumerate fiscal catalogue');const bad=await commercial.get(base+'/options?kind=drafts&company_id=99');ok(bad.status===422,'company injection denied');const own=await review.get(base+'/receipts/'+saved.steps.addedDraft.key);ok(own.status===404,'another actor receipt denied');const manifest=JSON.parse(fs.readFileSync(root+'/app/public/build/manifest.json'));for(const e of Object.values(manifest).filter(e=>e.isEntry||/MultiLineInvoiceWorkspace/.test(e.file))){const r=await fetch('http://127.0.0.1:8085/build/'+e.file),b=Buffer.from(await r.arrayBuffer());ok(r.status===200&&/javascript/.test(r.headers.get('content-type'))&&crypto.createHash('sha256').update(b).digest('hex')===crypto.createHash('sha256').update(fs.readFileSync(root+'/app/public/build/'+e.file)).digest('hex'),'actual main/lazy MIME SHA '+e.file);}
const proof=root+'/multiline-invoice-http-20261111.json';fs.writeFileSync(proof,JSON.stringify({passed:true,checks,new_documents:2,new_receivables:2,new_journals:2,new_grants:0,new_accounts:0,new_demo:false,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof}));
+2
View File
@@ -0,0 +1,2 @@
<?php
$book='8085';require __DIR__.'/sgf-multiline-invoice-proof-bootstrap.php';$out=['client_id'=>$client,'service_id'=>$service,'rule_event_id'=>$ruleEvent];file_put_contents($s['root'].'/multiline-invoice-inputs-private.json',json_encode($out));echo json_encode(['passed'=>true,'explicit_existing_references'=>3]);
@@ -0,0 +1,2 @@
<?php
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$b=json_decode(file_get_contents(json_decode(file_get_contents($s['root'].'/multiline-invoice-backup-pointer.json'),true)['root'].'/preservation-private.json'),true);echo json_encode(['host'=>array_keys($b['host']),'native'=>array_keys($b['native'])]);
@@ -0,0 +1,2 @@
<?php
$book='8085';require __DIR__.'/sgf-single-bootstrap.php';$before=json_decode(file_get_contents(json_decode(file_get_contents($s['root'].'/multiline-invoice-backup-pointer.json'),true)['root'].'/preservation-private.json'),true);$changes=[];$changed=[];foreach(['host','native']as$n){$db=$n==='host'?$host:$native;foreach($before[$n]as$t=>$json){$old=json_decode($json,true);$now=$db->table($t)->get()->map(fn($r)=>(array)$r)->all();$canon=fn($r)=>Tempo\Finance\Payables\Payables::canonical($r);$seen=array_fill_keys(array_map($canon,$now),true);foreach($old as$r)if(!isset($seen[$canon($r)]))$changed[]=$n.'.'.$t;if(count($now)!==count($old))$changes[$n.'.'.$t]=count($now)-count($old);}}echo json_encode(['original_changed'=>array_unique($changed),'appends'=>$changes,'steps'=>count(json_decode(file_get_contents($s['root'].'/multiline-invoice-http-progress-private.json'),true)['steps'])],JSON_PRETTY_PRINT);
@@ -0,0 +1,7 @@
<?php
$book='8085';require __DIR__.'/sgf-multiline-invoice-proof-bootstrap.php';$before=json_decode(file_get_contents(json_decode(file_get_contents($s['root'].'/multiline-invoice-backup-pointer.json'),true)['root'].'/preservation-private.json'),true);$expected=['host.si_invoices'=>2,'host.sr_records'=>2,'host.sr_effects'=>2,'host.sr_commands'=>4,'host.sr_events'=>4,'host.sr_outbox'=>4,'host.si_proposals'=>3,'host.si_decisions'=>3,'host.si_counters'=>1,'host.si_commands'=>2,'host.si_events'=>2,'host.si_outbox'=>2,'host.a_service_drafts'=>2,'host.a_billing_drafts'=>2,'host.a_history'=>4,'host.a_events'=>4,'host.a_outbox'=>4,'host.a_commands'=>4,'native.gl_journals'=>2,'native.audit_logs'=>8,'native.fs_versions'=>2,'native.fs_results'=>1,'native.fs_result_lines'=>23,'native.fs_commands'=>2,'native.gl_proposals'=>2,'native.gl_reviews'=>2,'native.gl_lines'=>6,'native.gl_commands'=>6,'native.gl_events'=>6];$changes=[];$canon=fn($r)=>Tempo\Finance\Payables\Payables::canonical((array)$r);foreach(['host','native']as$n){$db=$n==='host'?$host:$native;foreach($before[$n]as$t=>$json){$old=json_decode($json,true);$now=$db->table($t)->get()->map(fn($r)=>(array)$r)->all();$seen=array_fill_keys(array_map($canon,$now),true);foreach($old as$r)if(!isset($seen[$canon($r)]))throw new LogicException('Original changed '.$n.'.'.$t);$delta=count($now)-count($old);$ok($delta===($expected[$n.'.'.$t]??0),'original rows preserved with exact append '.$n.'.'.$t);if($delta)$changes[$n.'.'.$t]=$delta;}}foreach($before['files']as$f=>$sha)$ok(hash_file('sha256',$s['root'].'/'.$f)===$sha,'binding preserved '.$f);
$ok($native->table('gl_journals')->count()===31&&$native->table('gl_accounts')->count()===24,'31 journals24 accounts no new accounts');foreach($native->table('gl_journals')->pluck('id')as$id)$native->select('SELECT gl_assert_balanced(?)',[$id]);$ok(true,'all31 journals satisfy native balance guard read only');$ok($native->table('gl_periods')->orderByDesc('version')->value('status')==='open','period remains open');$ok($host->table('mi_drafts')->count()===2&&$host->table('mi_calculations')->count()===2&&$host->table('mi_commands')->count()===8,'exact2drafts2calculations8newadministrative receipts');$ok((int)$host->table('si_counters')->where('code','TEST-MULTI-2026')->value('last_number')===2,'only new series advanced to2');
$progress=json_decode(file_get_contents($s['root'].'/multiline-invoice-http-progress-private.json'),true)['steps'];$actor=$select('finance_review','finance');$sr=app(App\Modules\Services\ServiceReceivables::class);foreach(['added','included']as$mode){$i=$progress[$mode.'Issue']['result']['id'];$ob=$progress[$mode.'Obligation']['result']['id'];$position=$sr->position($ob,$actor);$ok($position['balance']==='1160.00','new receivable uncollected1160 '.$mode);$rows=$native->table('gl_lines')->where('journal_id',$progress[$mode.'JournalPost']['result']['journal_id'])->orderBy('line_no')->get();$ok($rows->count()===3&&(int)$rows[0]->debit_minor===116000&&(int)$rows[1]->credit_minor===100000&&(int)$rows[2]->credit_minor===16000,'exact original3line writer1160/1000/160 '.$mode);}
$backup=json_decode(file_get_contents($s['root'].'/multiline-invoice-backup-pointer.json'),true)['root'];$oldPhp=file_get_contents($backup.'/before/app/Modules/Services/InvoicePdf.php');$oldPhp=str_replace('final class InvoicePdf','final class InvoicePdfLegacyMultilineProof',$oldPhp);eval(substr($oldPhp,5));$legacy=0;foreach(json_decode($before['host']['si_invoices'],true)as$row){$snapshot=json_decode($row['snapshot'],true);if(($snapshot['renderer']??null)===App\Modules\Services\InvoicePdf::VERSION){$ok(App\Modules\Services\InvoicePdfLegacyMultilineProof::lines($snapshot)===App\Modules\Services\InvoicePdf::lines($snapshot),'old one-line PDF projection unchanged invoice'.$row['id']);$legacy++;}$bytes=base64_decode($row['pdf_base64'],true);$ok(hash('sha256',$bytes)===$row['pdf_sha256'],'old frozen PDF hash intact invoice'.$row['id']);}$ok($legacy>0,'legacy one-line branch actually exercised without repeating emission');
foreach(['ServiceWorkflow.php','ServiceCalculation.php','ServiceFiscal.php','FiscalDocumentMath.php','DocumentPreview.php']as$file)$ok(hash_file('sha256',$backup.'/before/app/Modules/Services/'.$file)===hash_file('sha256',$s['app'].'/app/Modules/Services/'.$file),'original source unchanged '.$file);$install=json_decode(file_get_contents(json_decode(file_get_contents($s['root'].'/multiline-invoice-installation-pointer.json'),true)['root'].'/multiline-invoice-installation.json'),true);foreach($install['hashes']as$file=>$sha)if(hash_file('sha256',$s['app'].'/'.$file)!==$sha)throw new LogicException('Installation hash drift '.$file);$ok(true,'all installed files match recorded SHA');$pub=$host->selectOne("SELECT EXISTS(SELECT 1 FROM information_schema.table_privileges WHERE grantee='PUBLIC' AND table_name IN('mi_meta','mi_calculations','mi_commands','mi_draft_meta','mi_drafts')) p")->p;$ok(!$pub,'no PUBLIC access to new administrative tables');
file_put_contents($work.'/multiline-invoice-preservation.json',json_encode(['passed'=>true,'checks'=>$checks,'original_sets'=>count($before['host'])+count($before['native']),'appends'=>$changes,'new_grants'=>0,'new_accounts'=>0,'legacy_pdf_projections'=>$legacy,'new_demo'=>false,'cache_cleared'=>false,'old_rows_identical'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/multiline-invoice-preservation.json']).PHP_EOL;
@@ -0,0 +1,2 @@
<?php
require __DIR__.'/sgf-single-bootstrap.php';$profiles=json_decode(file_get_contents($s['root'].'/single-profiles.json'),true)['profiles'];$names=['commercial_prepare','commercial_review','finance_prepare','finance_review','accounting_prepare','accounting_review','read_only'];$out=[];foreach($names as$n){$id=$profiles[$n]['user_id'];$out[$n]=['id'=>$id,'rights'=>$host->table('permission_grants')->where(['user_id'=>$id,'tenant_id'=>1,'company_id'=>2])->whereIn('permission',['service_fiscal.calculate','services.create','service_drafts.create','billing_drafts.create','billing_drafts.view','draft_documents.edit','service_invoices.issue','fiscal_rules.view'])->get(['permission','unit_id'])->all()];}echo json_encode($out,JSON_PRETTY_PRINT);
@@ -0,0 +1,8 @@
<?php
use Illuminate\Support\Facades\Auth;use Illuminate\Support\Str;use App\Shared\Context\CompanyContext;use App\Modules\Services\{ServiceWorkflow,DocumentPreparation,ServiceInvoicing,MultiLineInvoiceCalculation};
require __DIR__.'/sgf-ledger-proof-bootstrap.php';config(['multiline_invoices.enabled'=>true,'multiline_invoices.schema_sha256'=>hash_file('sha256',$repo.'/database/sql/multiline_invoices.sql'),'multiline_invoices.draft_schema_sha256'=>hash_file('sha256',$repo.'/database/sql/multiline_invoice_drafts.sql'),'sales_ledger.synthetic_enabled'=>true]);config(['fiscal_rules_qa.enabled'=>true,'fiscal_rules_qa.document_enabled'=>true,'fiscal_rules_qa.source_sha256'=>hash_file('sha256',$repo.'/resources/data/fiscal-rule-qa.json')]);
set_exception_handler(function($e){echo json_encode(['passed'=>false,'exception'=>get_class($e),'message'=>$e->getMessage(),'errors'=>$e instanceof Illuminate\Validation\ValidationException?$e->errors():null,'frames'=>array_map(fn($x)=>[basename($x['file']??''),$x['line']??0,$x['function']??''],array_slice($e->getTrace(),0,9))]).PHP_EOL;exit(1);});
$select=function($role,string $module='commercial')use($profiles,$app){$id=is_int($role)?$role:$profiles[$role]['user_id'];Auth::forgetGuards();Auth::shouldUse('web');$u=App\Models\User::findOrFail($id);Auth::guard('web')->setUser($u);$r=Illuminate\Http\Request::create('/api/multiline-invoices','POST');$r->attributes->set('sgf_module',$module);$r->setUserResolver(fn()=>$u);$session=app('session')->driver();$session->start();$session->put('company_context',['tenant_id'=>1,'company_id'=>2,'unit_id'=>2,'module_id'=>$module,'token'=>'multiline-'.$id]);$r->setLaravelSession($session);$app->instance('request',$r);app(CompanyContext::class)->selectFor($id,1,2,2);return$id;};
$inputs=json_decode(file_get_contents($s['root'].'/single-document-inputs.json'),true);$oldDraft=$host->table('a_billing_drafts')->where($f['scope'])->where('id',$inputs['credit_invoice']['draft_id'])->first();if(!$oldDraft)throw new LogicException('Known QA source missing');$client=(int)$oldDraft->client_id;$service=(int)json_decode($oldDraft->data,true)['lines'][0]['service_id'];$rules=$host->table('fr_events')->where($f['scope'])->where('state','reviewed')->where('definition->parameters->rate','0.16')->get();if($rules->count()!==1)throw new LogicException('Expected exact approved artificial16% fixture only');$ruleEvent=(int)$rules[0]->id;
$reject=function($fn,$status,$label)use($ok){$actual=null;try{$r=$fn();$actual=is_array($r)?($r['status']??null):null;}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface$e){$actual=$e->getStatusCode();}catch(Illuminate\Validation\ValidationException){$actual=422;}catch(DomainException){$actual=409;}$ok($actual===$status,$label);};
$fixture=function(string $mode,array $prices,string $suffix='')use($select,$service,$client){$actor=$select('commercial_prepare');$lines=array_map(fn($price)=>['service_id'=>$service,'description'=>'Linha fictícia '.$price,'quantity'=>'1','unit_price'=>$price,'unit'=>'UN'],$prices);$input=['draft_id'=>null,'version'=>0,'source_version'=>0,'preparation_version'=>0,'name'=>'QA-MULTI-'.$mode.'-'.$suffix,'client_id'=>$client,'currency'=>'MZN','price_mode'=>$mode,'service_date'=>'2026-11-11','period_start'=>'2026-11-01','period_end'=>'2026-11-30','proposed_due_date'=>'2026-11-30','payment_terms'=>'Prazo artificial de ensaio, sem afirmação legal.','lines'=>$lines,'reason'=>'Preparação explícita de rascunho multilinha, sem emissão.'];$writer=app(App\Modules\Services\MultiLineDrafts::class);$cut=$writer->preview($actor,$input,(string)Str::uuid());$r=$writer->confirm($actor,$cut['preview']);return ['source'=>['id'=>$r['source_id'],'version'=>$r['source_version']],'draft'=>['id'=>$r['id'],'version'=>$r['version']],'preparation'=>['version'=>$r['preparation_version']]];};
+10
View File
@@ -0,0 +1,10 @@
<?php
use App\Modules\Services\{ServiceInvoicing,MultiLineInvoiceCalculation};use Illuminate\Support\Str;
require __DIR__.'/sgf-multiline-invoice-proof-bootstrap.php';$host->beginTransaction();$native->beginTransaction();try{
$actor=$select('commercial_prepare');$engine=app(ServiceInvoicing::class);$tag=strtoupper(substr((string)Str::uuid(),0,8));$series=$engine->propose($actor,['kind'=>'series','identifier'=>'TEST-MULTI-REG-'.$tag,'data'=>['code'=>'TEST-MULTI-REG-'.$tag,'first_number'=>1,'state'=>'open','valid_from'=>'2026-11-01','valid_until'=>'2026-11-30','evidence'=>'Série artificial de regressão do ciclo multilinha.']]);$actor=$select('commercial_review');$engine->decide($series['id'],$actor,['version'=>1,'decision'=>'approved','reason'=>'Revisão independente da série fictícia multilinha.']);
foreach([['added',['400','600']],['included',['464','696']]]as[$mode,$prices]){
$x=$fixture($mode,$prices,$tag);$actor=$select('finance_prepare');$c=app(MultiLineInvoiceCalculation::class);$in=['draft_id'=>$x['draft']['id'],'rule_event_id'=>$ruleEvent,'draft_version'=>1,'preparation_version'=>$x['preparation']['version'],'reason'=>'Cálculo individual de linhas e modalidade pela regra QA revista.'];$cut=$c->preview($actor,$in,(string)Str::uuid());$calc=$c->confirm($actor,$cut['preview']);$ok($calc['result']['base_document']==='1000.00'&&$calc['result']['tax_document']==='160.00'&&$calc['result']['total']==='1160.00','shared math exact '.$mode);$replay=$c->confirm($actor,$cut['preview']);$ok($replay['replayed']&&$replay['id']===$calc['id'],'calculation replay no duplicate '.$mode);
$actor=$select('commercial_prepare');$data=['calculation_policy'=>MultiLineInvoiceCalculation::POLICY,'common_calculation_id'=>$calc['id'],'common_rule_event_id'=>$ruleEvent,'draft_id'=>$x['draft']['id'],'draft_version'=>1,'preparation_version'=>$x['preparation']['version'],'valid_from'=>'2026-11-01','valid_until'=>'2026-11-30','period_start'=>'2026-11-01','period_end'=>'2026-11-30','issue_window_start'=>'2026-11-11','issue_window_end'=>'2026-11-11','event'=>'on_issue_within_deadline','readiness_basis'=>'Origem, linhas e modalidade explícitos; prontidão apenas artificial.','evidence'=>'Conferência temporal individual de emissão QA multilinha.'];$temporal=$engine->propose($actor,['kind'=>'temporal','identifier'=>'MULTI-REG-'.$tag.'-'.$mode,'data'=>$data]);$actor=$select('commercial_review');$engine->decide($temporal['id'],$actor,['version'=>1,'decision'=>'approved','reason'=>'Revisão distinta das linhas, valores e prontidão artificial.']);$actor=$select('commercial_prepare');$review=$engine->review($x['draft']['id'],$series['id'],$temporal['id'],$actor);$ok($review['ready'],'review current and ready '.$mode);$key=(string)Str::uuid();$input=['series_id'=>$series['id'],'rule_id'=>$temporal['id'],'review_hash'=>$review['review_hash']];$invoice=$engine->issue($x['draft']['id'],$actor,$input,$key);$again=$engine->issue($x['draft']['id'],$actor,$input,$key);$ok($again['replayed']&&$again['id']===$invoice['id'],'invoice key recovers number without duplicate '.$mode);$shown=$engine->show($invoice['id'],$actor);$pdf=$engine->pdf($invoice['id'],$actor);$ok(hash('sha256',$pdf)===$shown['pdf_sha256']&&count(array_filter($shown['document_lines'],fn($l)=>$l[0]==='section'&&str_starts_with($l[1],'Linha ')))===2,'frozen PDF includes both lines '.$mode);
}
$host->statement('SET CONSTRAINTS ALL IMMEDIATE');$ok(true,'host invoice constraints accept new typed photographs');
}finally{$native->rollBack();$host->rollBack();}file_put_contents($work.'/multiline-invoice-result.json',json_encode(['passed'=>true,'checks'=>$checks,'fixtures_rolled_back'=>true,'new_demo'=>false,'new_grants'=>0,'new_accounts'=>0,'regression_only'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/multiline-invoice-result.json']).PHP_EOL;
@@ -0,0 +1,7 @@
<?php
use App\Modules\Services\{MultiLineDrafts,MultiLineInvoiceCalculation,ServiceInvoicing};use Illuminate\Support\Str;
require __DIR__.'/sgf-multiline-invoice-proof-bootstrap.php';$host->beginTransaction();try{
$actor=$select('commercial_prepare');$w=app(MultiLineDrafts::class);$body=['draft_id'=>null,'version'=>0,'source_version'=>0,'preparation_version'=>0,'name'=>'QA-MULTI-FAILURE','client_id'=>$client,'currency'=>'MZN','price_mode'=>'added','service_date'=>'2026-11-11','period_start'=>'2026-11-01','period_end'=>'2026-11-30','proposed_due_date'=>'2026-11-30','payment_terms'=>'Condições artificiais.','lines'=>[['service_id'=>$service,'description'=>'Linha de recuperação','quantity'=>'1','unit_price'=>'100','unit'=>'UN']],'reason'=>'Recuperação sintética após falha parcial controlada.'];$cut=$w->preview($actor,$body,(string)Str::uuid());$counts=fn()=>array_map(fn($t)=>$host->table($t)->count(),['a_service_drafts','a_billing_drafts','a_history','a_events','a_commands','mi_drafts','mi_commands','identity_audit']);$before=$counts();
$host->statement("CREATE FUNCTION mi_fail_test() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN RAISE EXCEPTION 'controlled partial failure'; END \$\$");$host->statement('CREATE TRIGGER mi_fail_test BEFORE INSERT ON mi_commands FOR EACH ROW EXECUTE FUNCTION mi_fail_test()');$failed=false;try{$w->confirm($actor,$cut['preview']);}catch(Illuminate\Database\QueryException $e){$failed=str_contains($e->getMessage(),'controlled partial failure');}$ok($failed,'failure injected after original draft writers before receipt');$ok($counts()===$before,'draft source preparation events registry and receipts roll back together');$host->statement('DROP TRIGGER mi_fail_test ON mi_commands');$result=$w->confirm($actor,$cut['preview']);$again=$w->confirm($actor,$cut['preview']);$ok($again['replayed']&&$again['id']===$result['id'],'same draft cut recovers exactly once after failure');
$c=app(MultiLineInvoiceCalculation::class);$actor=$select('finance_prepare');$p=$c->preview($actor,['draft_id'=>$result['id'],'draft_version'=>1,'preparation_version'=>1,'rule_event_id'=>$ruleEvent,'reason'=>'Cálculo do rascunho recuperado explicitamente.'],(string)Str::uuid());$calc=$c->confirm($actor,$p['preview']);$actor=$select('commercial_prepare');$e=app(ServiceInvoicing::class);$code='TEST-MULTI-FAIL-'.strtoupper(substr((string)Str::uuid(),0,8));$series=$e->propose($actor,['kind'=>'series','identifier'=>$code,'data'=>['code'=>$code,'first_number'=>1,'state'=>'open','valid_from'=>'2026-11-01','valid_until'=>'2026-11-30','evidence'=>'Série artificicial de recuperação.']]);$temporal=$e->propose($actor,['kind'=>'temporal','identifier'=>'MULTI-FAIL-'.$result['id'],'data'=>['calculation_policy'=>MultiLineInvoiceCalculation::POLICY,'common_calculation_id'=>$calc['id'],'common_rule_event_id'=>$ruleEvent,'draft_id'=>$result['id'],'draft_version'=>1,'preparation_version'=>1,'valid_from'=>'2026-11-01','valid_until'=>'2026-11-30','period_start'=>'2026-11-01','period_end'=>'2026-11-30','issue_window_start'=>'2026-11-11','issue_window_end'=>'2026-11-11','event'=>'on_issue_within_deadline','readiness_basis'=>'Documento recuperado com cálculo actual.','evidence'=>'Origem e valores fictícios conferidos.']]);$actor=$select('commercial_review');foreach([$series,$temporal]as$p)$e->decide($p['id'],$actor,['version'=>1,'decision'=>'approved','reason'=>'Revisão independente do documento de recuperação.']);$actor=$select('commercial_prepare');$ready=$e->review($result['id'],$series['id'],$temporal['id'],$actor);$input=['series_id'=>$series['id'],'rule_id'=>$temporal['id'],'review_hash'=>$ready['review_hash']];$key=(string)Str::uuid();$invoiceCounts=fn()=>array_map(fn($t)=>$host->table($t)->count(),['si_invoices','si_events','si_outbox','si_counters','si_commands']);$before=$invoiceCounts();$host->statement('CREATE TRIGGER mi_invoice_fail_test BEFORE INSERT ON si_commands FOR EACH ROW EXECUTE FUNCTION mi_fail_test()');$failed=false;try{$e->issue($result['id'],$actor,$input,$key);}catch(Illuminate\Database\QueryException $ex){$failed=str_contains($ex->getMessage(),'controlled partial failure');}$ok($failed&&$invoiceCounts()===$before,'PDF number invoice outbox and receipt roll back on late failure');$host->statement('DROP TRIGGER mi_invoice_fail_test ON si_commands');$i=$e->issue($result['id'],$actor,$input,$key);$again=$e->issue($result['id'],$actor,$input,$key);$ok($i['number']===1&&$again['replayed']&&$again['id']===$i['id'],'same invoice request recovers number1 once');$ok($host->table('si_invoices')->where('draft_id',$result['id'])->count()===1,'one invoice after recovery');
}finally{$host->rollBack();}file_put_contents($work.'/multiline-invoice-recovery.json',json_encode(['passed'=>true,'checks'=>$checks,'fixtures_rolled_back'=>true,'regression_only'=>true],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/multiline-invoice-recovery.json']).PHP_EOL;
+3
View File
@@ -0,0 +1,3 @@
<?php
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';$p=json_decode(file_get_contents($repo.'/docs/integration/sgf-multi-line-invoice-proposal.json'),true);if(($p['approved']??false)!==true||$p['authorization']['decision']!=='Autorizar a facturação multilinha QA proposta'||$p['new_grants']!==0||$p['new_accounts']!==0)throw new LogicException('Exact approval required');if($book==='8085'&&!is_file($s['root'].'/multiline-invoice-backup-pointer.json'))throw new LogicException('Backup required');$sql=file_get_contents($repo.'/database/sql/multiline_invoices.sql');$sha=hash('sha256',$sql);$host->transaction(function()use($host,$sql,$sha,$tag){if(!$host->getSchemaBuilder()->hasTable('mi_meta')){$host->unprepared($sql);$host->table('mi_meta')->insert(['policy'=>App\Modules\Services\MultiLineInvoiceCalculation::POLICY,'composition'=>$tag,'source_hash'=>$sha]);}else{$m=$host->table('mi_meta')->first();if(!$m||$m->composition!==$tag||$m->source_hash!==$sha)throw new LogicException('Preserve different schema');}});$draftSql=file_get_contents($repo.'/database/sql/multiline_invoice_drafts.sql');$draftSha=hash('sha256',$draftSql);$host->transaction(function()use($host,$draftSql,$draftSha,$tag){if(!$host->getSchemaBuilder()->hasTable('mi_draft_meta')){$host->unprepared($draftSql);$host->table('mi_draft_meta')->insert(['policy'=>App\Modules\Services\MultiLineInvoiceCalculation::POLICY,'composition'=>$tag,'source_hash'=>$draftSha]);}else{$m=$host->table('mi_draft_meta')->first();if(!$m||$m->composition!==$tag||$m->source_hash!==$draftSha)throw new LogicException('Preserve different draft composition');}});
file_put_contents($work.'/multiline-invoice-setup.json',json_encode(['passed'=>true,'book'=>$book,'schema_sha256'=>$sha,'new_grants'=>0,'new_accounts'=>0,'new_demo'=>false],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'proof'=>$work.'/multiline-invoice-setup.json']).PHP_EOL;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
import fs from 'node:fs';import assert from 'node:assert/strict';import crypto from 'node:crypto';import {session} from './sgf-single-http.mjs';import {checkedStatements} from '../resources/js/internal-statements-view.js';
const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',progress=root+'/multiline-statements-http-progress-private.json',saved=fs.existsSync(progress)?JSON.parse(fs.readFileSync(progress)):{},save=()=>fs.writeFileSync(progress,JSON.stringify(saved)),checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);},base='/api/general-ledger/statements';
const prep=await session('sgf:accounting_prepare','accounting'),review=await session('sgf:accounting_review','accounting'),read=await session('sgf:read_only','accounting');const get=async(q={view:'history',page:1,per_page:25},s=read)=>{const r=await s.get(base+'?'+new URLSearchParams(q));assert.equal(r.status,200);return checkedStatements(await r.json(),q);};let state=await get();ok(prep.identity.context.internal_statements_enabled===true,'selected installation feature enabled');const names=s=>s.identity.context.grants.filter(g=>g.permission.startsWith('general_ledger.statements.')).map(g=>g.permission);ok(JSON.stringify(names(prep))===JSON.stringify(['general_ledger.statements.prepare'])&&JSON.stringify(names(review))===JSON.stringify(['general_ledger.statements.review'])&&names(read).length===0,'exact approved preparation review rights only');
const input=(kind,action,version,data)=>({kind,action,version,data,reason:'Decisão individual QA de demonstrações internas parciais, sem classificação legal ou novos lançamentos.'});
async function perform(name,s,body){saved[name]??={key:crypto.randomUUID()};save();const step=saved[name];if(step.result)return step.result;const old=await s.get(base+'/receipts/'+step.key);if(old.status===200){step.result=(await old.json()).result;save();return step.result;}assert.equal(old.status,404);const cut=await s.command(base+'/preview',body,step.key);assert.equal(cut.status,200,JSON.stringify(cut.value));step.cut=cut.value;save();const out=await s.command(base+'/confirm',{preview:cut.value.preview},step.key);assert.equal(out.status,201,JSON.stringify(out.value));step.result=out.value;save();return out.value;}
const assetProgress=JSON.parse(fs.readFileSync(root+'/assets-statements-http-progress-private.json'));const mapping=assetProgress.mappingReview.result;const r1=await perform('resultPrepare',prep,input('result','prepare',4,{mapping_id:mapping.id,from_on:'2026-01-01',to_on:'2026-11-11',per_page:25,evidence:'Resultado interno parcial actualizado com dois documentos QA multilinha; mesmas correspondências revistas e31 lançamentos.'}));const r2=await perform('resultReview',review,input('result','review',5,{decision:'approved'}));ok(r1.version===5&&r2.version===6,'new result independently prepared reviewed versions5/6');const query={view:'result',version_id:r2.id,page:1,per_page:25},report=await get(query),summary=report.snapshot.summary,c=summary.currencies.MZN;ok(summary.plan.accounts===24&&summary.accounts===23&&summary.basis.journals===31&&report.current_source_matches,'24 chart accounts23 accounts with movements31 journals current source');ok(c.groups.asset.presentation_minor==='557500'&&c.groups.liability.presentation_minor==='10000','classified assets5575 supplier100');ok(c.groups.expense.presentation_minor==='230000'&&c.known_result_subtotal_minor==='228621','year-to-date expense2300 known subtotal2286.21');ok(c.unclassified_accounts===10&&c.complete_result_minor===null&&c.equity_minor===null&&!c.complete_balance_sheet,'ten unclassified complete result and equity unknown');const larger=await get({...query,per_page:100});ok(JSON.stringify(larger.records.data)===JSON.stringify(report.records.data),'same frozen23 movement rows pages25/100');
const old=JSON.parse(fs.readFileSync(root+'/statements-http-progress.json')).resultReview.result;let response=await read.get(base+'/exports/'+old.id);const oldBytes=Buffer.from(await response.arrayBuffer());ok(response.status===200&&oldBytes.equals(fs.readFileSync(root+'/statements-frozen-export-private.csv')),'old reviewed export byte identical');const historical=await get({...query,version_id:old.id});ok(historical.snapshot.summary.accounts===19&&historical.snapshot.summary.basis.journals===27&&historical.current_source_matches===false,'old19/27 snapshot explicit historical');response=await read.get(base+'/exports/'+r2.id);const bytes=Buffer.from(await response.arrayBuffer());ok(response.status===200&&crypto.createHash('sha256').update(bytes).digest('hex')===report.snapshot.csv_sha256,'new partial result export and SHA');fs.writeFileSync(root+'/multiline-statements-frozen-private.csv',bytes);fs.writeFileSync(root+'/multiline-statements-sample-private.json',JSON.stringify(report));const assetOld=assetProgress.resultReview.result;response=await read.get(base+'/exports/'+assetOld.id);ok(response.status===200&&Buffer.from(await response.arrayBuffer()).equals(fs.readFileSync(root+'/assets-statements-frozen-private.csv')),'asset29 journal snapshot byte identical');const proof=root+'/multiline-statements-http-20261111.json';fs.writeFileSync(proof,JSON.stringify({passed:true,checks,administrative_commands:2,old_snapshot_preserved:true,new_demo:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof}));
+1 -1
View File
@@ -6,7 +6,7 @@ $host->beginTransaction();try{
[$r1]=$command(13,$input('rules',$rk,0,$rulesData));[$r2]=$command(14,$input('rules',$rk,1,['decision'=>'approved'],'review'));$pay=['operation'=>'establish','worker_key'=>$wk,'rules_key'=>$rk,'period'=>'2026-11','gross'=>'10000.00'];[$v1,$cut,$key]=$command(13,$input('payroll',$pk,0,$pay));$c=$v1['impact']['after']['proposal']['calculation'];$ok($c['net_cents']==='890000'&&$c['employer_cost_cents']==='1040000'&&$c['deductions_cents']==='110000','exact artificial gross10000 net8900 cost10400');$ok($c['rubrics']['worker_rate']['cents']==='30000'&&$c['rubrics']['retention_rate']['cents']==='80000'&&$c['rubrics']['employer_rate']['cents']==='40000','rubrics distinguish employee and employer');$ok(!$v1['impact']['debt_created']&&$v1['impact']['journals']===0&&$v1['impact']['payments']===0,'no financial effect');$reject(fn()=>$preview(13,$input('payroll',$uuid(),0,$pay)),409,'duplicate person period refused');
$stale=$preview(14,$input('payroll',$pk,1,['decision'=>'approved'],'review'));[$v2]=$command(13,$input('payroll',$pk,1,$pay));$reject(fn()=>$confirm(14,$stale['preview']),409,'stale review version refused');[$v3]=$command(14,$input('payroll',$pk,2,['decision'=>'approved'],'review'));$ok($v3['version']===3&&$v3['state']==='approved','current preparation reviewed distinctly');$select(17);$export=$rm->export($hrActor(17),$v3['id']);$ok(hash('sha256',$export['bytes'])===$export['sha256']&&str_contains($export['bytes'],'890000'),'frozen reviewed artifact SHA');
$changed=array_replace($pay,['operation'=>'amend','gross'=>'10001.00']);[$v4]=$command(13,$input('payroll',$pk,3,$changed));[$v5]=$command(14,$input('payroll',$pk,4,['decision'=>'refused'],'review'));$list=$listing('payroll',['record_key'=>$pk]);$ok($list['records']['data'][0]['effective']['id']===$v3['id'],'refusal preserves effective reviewed amount');[$v6]=$command(13,$input('payroll',$pk,5,$changed));[$v7]=$command(14,$input('payroll',$pk,6,['decision'=>'approved'],'review'));$ok($v7['impact']['after']['effective']['calculation']['net_cents']==='890089','reviewed correction updates net exactly');$select(17);$ok($rm->export($hrActor(17),$v3['id'])===$export,'old reviewed export bytes immutable after correction');
$cancel=array_replace($changed,['operation'=>'cancel']);[$v8]=$command(13,$input('payroll',$pk,7,$cancel));[$v9]=$command(14,$input('payroll',$pk,8,['decision'=>'approved'],'review'));$ok($v9['impact']['after']['effective']['status']==='cancelled','distinct cancellation preserved');$ok($listing()['page_totals']['approved_active_on_page']===0,'cancelled excluded from active page totals');$reject(fn()=>$preview(13,$input('payroll',$pk,9,$changed)),409,'cancelled record cannot be silently revived');
$cancel=array_replace($changed,['operation'=>'cancel']);[$v8]=$command(13,$input('payroll',$pk,7,$cancel));[$v9]=$command(14,$input('payroll',$pk,8,['decision'=>'approved'],'review'));$ok($v9['impact']['after']['effective']['status']==='cancelled','distinct cancellation preserved');$ok($listing('payroll',['search'=>'QA-COLABORADOR-01'])['page_totals']['approved_active_on_page']===0,'cancelled excluded from active page totals');$reject(fn()=>$preview(13,$input('payroll',$pk,9,$changed)),409,'cancelled record cannot be silently revived');
$tiny=App\Modules\HumanResources\Remunerations::calculate('0.01',$rulesData);$ok($tiny['net_cents']==='1'&&$tiny['rubrics']['worker_rate']['difference']==='3/100','cent difference exact rational preserved');$missing=App\Modules\HumanResources\Remunerations::calculate('10000.00',array_replace($rulesData,['retention_rate'=>null]));$ok(!$missing['complete']&&$missing['net_cents']===null&&$missing['rubrics']['retention_rate']['cents']===null,'missing parameter not zero net unknown');
$newRules=$uuid();$missingRules=array_replace($rulesData,['retention_rate'=>null]);[$a]=$command(13,$input('rules',$newRules,0,$missingRules));[$a]=$command(14,$input('rules',$newRules,1,['decision'=>'approved'],'review'));$p2=$uuid();$pay2=array_replace($pay,['period'=>'2026-10','rules_key'=>$newRules]);[$a]=$command(13,$input('payroll',$p2,0,$pay2));$reject(fn()=>$preview(14,$input('payroll',$p2,1,['decision'=>'approved'],'review')),409,'incomplete payroll cannot be approved complete');
$suspended=array_replace($workerData,['operation'=>'amend','status'=>'suspended']);[$a]=$command(13,$input('worker',$wk,2,$suspended));[$a]=$command(14,$input('worker',$wk,3,['decision'=>'approved'],'review'));$reject(fn()=>$preview(13,$input('payroll',$uuid(),0,array_replace($pay,['period'=>'2026-09']))),409,'suspended worker processing refused');