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

22 lines
8.0 KiB
PHP

<?php
require __DIR__.'/sgf-remunerations-proof-bootstrap.php';
$cuts=json_decode(file_get_contents($s['root'].'/hr-concurrency-1794377543991/cuts.json'),true);$row=$host->table('rm_versions')->where('record_key',$cuts['payroll_key'])->orderByDesc('version')->first();$base=json_decode($row->data,true);$wk=$base['input']['worker_key'];$rk=$base['input']['rules_key'];$n=$host->table('rm_versions')->count();$cmd=$host->table('rm_commands')->count();$audit=$host->table('identity_audit')->count();$host->beginTransaction();try{
$pay=array_replace($base['input'],['operation'=>'amend','gross'=>'10010.00']);$old=$preview(13,$input('payroll',$row->record_key,2,$pay));$rd=array_replace($rulesData,['operation'=>'amend','retention_rate'=>'9']);[$a]=$command(13,$input('rules',$rk,2,$rd));[$a]=$command(14,$input('rules',$rk,3,['decision'=>'approved'],'review'));$reject(fn()=>$confirm(13,$old['preview']),409,'new reviewed rule invalidates earlier preview');
[$a]=$command(13,$input('payroll',$row->record_key,2,$pay));$rv=$preview(14,$input('payroll',$row->record_key,3,['decision'=>'approved'],'review'));$wd=$base['dependencies']['worker']['data']['input'];$wd['operation']='amend';$wd['status']='suspended';[$a]=$command(13,$input('worker',$wk,2,$wd));[$a]=$command(14,$input('worker',$wk,3,['decision'=>'approved'],'review'));$reject(fn()=>$confirm(14,$rv['preview']),409,'worker source change invalidates pending review');
$other=$uuid();$otherData=['operation'=>'establish','code'=>'QA-BOUNDARY-OTHER','status'=>'active'];[$a]=$command(13,$input('worker',$other,0,$otherData));[$a]=$command(14,$input('worker',$other,1,['decision'=>'approved'],'review'));$pk=$uuid();$pd=['operation'=>'establish','worker_key'=>$other,'rules_key'=>$rk,'period'=>'2026-10','gross'=>'2000.00'];[$a]=$command(13,$input('payroll',$pk,0,$pd));[$b]=$command(14,$input('payroll',$pk,1,['decision'=>'approved'],'review'));$ok($b['impact']['after']['effective']['calculation']['net_cents']==='176000','second person and period use exact reviewed current rule');$reject(fn()=>$preview(13,$input('payroll',$uuid(),0,array_replace($pd,['period'=>'2027-01']))),409,'rule must cover whole processing month');
$forged=$input('rules',$uuid(),0,array_replace($rulesData,['real_inss'=>true]));$reject(fn()=>$preview(13,$forged),422,'unknown fiscal attribute refused');$reject(fn()=>$preview(13,$input('rules',$uuid(),0,array_replace($rulesData,['worker_rate'=>'101']))),422,'rate bound enforced');$reject(fn()=>$preview(13,$input('rules',$uuid(),0,array_replace($rulesData,['valid_to'=>'2025-12-31']))),422,'inverted validity refused');
$save=function($fn)use($host){$host->beginTransaction();try{$fn();}finally{$host->rollBack();}};
[$pending]=$command(13,$input('payroll',$pk,2,array_replace($pd,['operation'=>'amend','gross'=>'2001.00'])));
$save(function()use($host,$hrActor,$preview,$input,$pk,$reject){$host->table('permission_grants')->where(['user_id'=>$hrActor(13),'permission'=>'human_resources.remunerations.prepare'])->delete();$reject(fn()=>$preview(14,$input('payroll',$pk,3,['decision'=>'approved'],'review')),403,'cannot review when preparer right revoked');});
$new=$uuid();$newData=['operation'=>'establish','code'=>'QA-FAIL-RECOVERY','status'=>'active'];$cut=$preview(13,$input('worker',$new,0,$newData));$before=[$host->table('rm_versions')->count(),$host->table('rm_commands')->count(),$host->table('identity_audit')->count()];
$save(function()use($host,$confirm,$cut,$ok){$host->unprepared("CREATE FUNCTION pg_temp.rm_qa_fail() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN RAISE EXCEPTION 'QA_FORCED_AFTER_VERSION'; END; \$\$;REVOKE ALL ON FUNCTION pg_temp.rm_qa_fail() FROM PUBLIC;CREATE TRIGGER rm_qa_fail BEFORE INSERT ON rm_commands FOR EACH ROW EXECUTE FUNCTION pg_temp.rm_qa_fail();");try{$confirm(13,$cut['preview']);throw new LogicException('Expected injected failure');}catch(Illuminate\Database\QueryException$e){$ok(str_contains($e->getMessage(),'QA_FORCED_AFTER_VERSION'),'injected failure after version before receipt observed');}});
$ok([$host->table('rm_versions')->count(),$host->table('rm_commands')->count(),$host->table('identity_audit')->count()]===$before,'version receipt and audit rolled back atomically after failure');$out=$confirm(13,$cut['preview']);$again=$confirm(13,$cut['preview']);$ok(!$out['replayed']&&$again['replayed']&&$out['id']===$again['id'],'same preview recovers after failure exactly once');
$select(17);$own=app(App\Shared\Context\ModuleNavigation::class);config(['modules.contracted'=>array_keys(App\Shared\Context\ModuleNavigation::LABELS)]);$modules=$own->available(app(App\Shared\Context\CompanyContext::class));$ok(array_column($modules,'id')===['human_resources'],'HR reader has only HR no finance accounting commercial');
// Real identity13 is financial, not the alias used by this harness.
$select(13);$reject(fn()=>app(App\Modules\Identity\Services\Permissions::class)->require(13,1,2,'human_resources.remunerations.view',2),403,'financial identity13 cannot read HR');
$pageKeys=[];for($i=0;$i<27;$i++){$key=$uuid();$pageKeys[]=$key;$data=['input'=>['code'=>'QA-PAGE-'.sprintf('%03d',$i),'status'=>'active','operation'=>'establish'],'status'=>'active','dependencies'=>[],'calculation'=>null,'synthetic_only'=>true];$host->table('rm_versions')->insert(['tenant_id'=>1,'company_id'=>2,'unit_id'=>2,'kind'=>'worker','record_key'=>$key,'version'=>1,'previous_id'=>null,'action'=>'prepare','state'=>'prepared','prepared_by'=>$hrActor(13),'actor_id'=>$hrActor(13),'preparer_identity'=>str_repeat('0',64),'data'=>json_encode($data),'source_basis'=>str_repeat('0',64),'impact'=>'{}','reason'=>'Fixture SQL de paginação sem workflow de negócio.','created_at'=>now()]);}
$q=['search'=>'QA-PAGE-','per_page'=>25,'page'=>1];$a=$listing('worker',$q);$b=$listing('worker',array_replace($q,['page'=>2]));$large=$listing('worker',array_replace($q,['per_page'=>100]));$ids=array_column($large['records']['data'],'record_key');$ok(count($a['records']['data'])===25&&count($b['records']['data'])===2&&$a['records']['total']===27,'SQL filter before25 plus2 pagination');$ok(array_merge(array_column($a['records']['data'],'record_key'),array_column($b['records']['data'],'record_key'))===$ids&&count(array_unique($ids))===27,'100 page same ordered records no duplicate');$ok($listing('worker',array_replace($q,['page'=>100000]))['records']['data']===[],'empty far page no fallback');$ok($listing('worker',['search'=>'QA-PAGE-%','per_page'=>25])['records']['total']===0,'search wildcard literal escaped');$reject(fn()=>$listing('worker',['tenant_id'=>2]),422,'listing tenant override refused');$reject(fn()=>$listing('worker',['record_key'=>$uuid()]),403,'foreign history key refused');$reject(fn()=>$listing('worker',['per_page'=>26]),422,'page size whitelist enforced');
$foreignUnit=$host->table('units')->where(['tenant_id'=>1,'company_id'=>2])->where('id','<>',2)->value('id');if(!$foreignUnit)throw new LogicException('Existing foreign unit required');$foreign=$uuid();$host->table('rm_versions')->insert(['tenant_id'=>1,'company_id'=>2,'unit_id'=>$foreignUnit,'kind'=>'worker','record_key'=>$foreign,'version'=>1,'action'=>'prepare','state'=>'prepared','prepared_by'=>$hrActor(13),'actor_id'=>$hrActor(13),'preparer_identity'=>str_repeat('0',64),'data'=>json_encode(['input'=>['code'=>'QA-PAGE-FOREIGN'],'status'=>'active']),'source_basis'=>str_repeat('0',64),'impact'=>'{}','reason'=>'Fixture SQL estrangeira isolada, sem workflow.','created_at'=>now()]);$ok($listing('worker',$q)['records']['total']===27,'other unit row excluded before counts');$reject(fn()=>$listing('worker',['record_key'=>$foreign]),403,'other unit history refused');
}finally{$host->rollBack();}
$ok($host->table('rm_versions')->count()===$n&&$host->table('rm_commands')->count()===$cmd&&$host->table('identity_audit')->count()===$audit,'all boundary fixtures and temporary writes rolled back');file_put_contents($work.'/hr-boundaries.json',json_encode(['passed'=>true,'checks'=>$checks,'fixtures_rolled_back'=>true,'regression_only'=>true,'financial_effects'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/hr-boundaries.json']).PHP_EOL;