Files
SGF/app/Shared/Installation/HumanResourcesProfiles.php
2026-11-11 09:00:17 +02:00

26 lines
5.6 KiB
PHP

<?php
namespace App\Shared\Installation;
use App\Models\User;
use App\Modules\Identity\Services\Audit;
use Illuminate\Support\Facades\DB;
/** Separate QA registry; never rewrites the accepted ten-profile matrix. */
final class HumanResourcesProfiles
{
public static function policy():array{$p=json_decode(file_get_contents(base_path('docs/integration/sgf-human-resources-access-proposal.json')),true,64,JSON_THROW_ON_ERROR);abort_unless(($p['approved']??false)===true&&($p['access_authorized']??false)===true&&($p['scope']??null)===['tenant_id'=>1,'company_id'=>2,'unit_id'=>2]&&($p['authorization']['decision']??null)==='Autorizar os perfis QA próprios de RH e as concessões apresentadas',403,'Perfis próprios de RH por autorizar.');return$p;}
public static function fingerprint(array $p):string{return hash('sha256',json_encode(array_intersect_key($p,array_flip(['version','scope','module','profiles'])),JSON_THROW_ON_ERROR));}
public static function registered():array{
abort_unless(config('human_resources.enabled')===true,404);CompositionRuntime::assert();$p=self::policy();$pin=self::fingerprint($p);abort_unless(hash_equals($pin,(string)config('human_resources.profiles_sha256')),503,'Matriz de RH não conferida.');$rows=DB::table('hr_qa_profiles')->orderBy('profile_key')->get();$expected=collect($p['profiles'])->keyBy('id');abort_unless($rows->count()===3&&$rows->every(fn($r)=>isset($expected[$r->profile_key])&&hash_equals($pin,$r->source_hash)&&$r->tenant_id===1&&$r->company_id===2&&$r->unit_id===2),503,'Registo de perfis de RH alterado.');return$rows->all();
}
public static function assertActor(int $actor,array $s,string $action):void{$rows=self::registered();$row=collect($rows)->firstWhere('user_id',$actor);abort_unless($row&&['tenant_id'=>$row->tenant_id,'company_id'=>$row->company_id,'unit_id'=>$row->unit_id]===$s,403,'Perfil próprio de RH obrigatório.');if($action!=='view')abort_unless($row->profile_key===($action==='prepare'?'hr_prepare':'hr_review'),403);}
public function install():array{
CompositionRuntime::assert();$p=self::policy();$pin=self::fingerprint($p);$expected=[];foreach(['prepare','review','read']as$role){$rights=[['permission'=>'company.view','scope'=>'company'],['permission'=>'unit.view','scope'=>'unit'],['permission'=>'human_resources.remunerations.view','scope'=>'unit']];if($role!=='read')$rights[]=['permission'=>'human_resources.remunerations.'.$role,'scope'=>'unit'];$expected['hr_'.$role]=$rights;}abort_unless(count($p['profiles'])===3&&collect($p['profiles'])->every(fn($r)=>isset($expected[$r['id']])&&$r['rights']===$expected[$r['id']]),403,'Não ampliar a matriz aprovada.');
return DB::transaction(function()use($p,$pin){DB::selectOne("SELECT pg_advisory_xact_lock(hashtext('sgf-approved-human-resources-profiles'))");DB::table('companies')->where(['tenant_id'=>1,'id'=>2])->lockForUpdate()->firstOrFail();
if(!DB::getSchemaBuilder()->hasTable('hr_qa_profiles')){DB::unprepared("CREATE TABLE hr_qa_profiles(profile_key text PRIMARY KEY CHECK(profile_key IN('hr_prepare','hr_review','hr_read')),user_id bigint UNIQUE NOT NULL REFERENCES users(id),tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,label text NOT NULL,source_hash char(64) NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id));CREATE TRIGGER hr_qa_profiles_immutable BEFORE UPDATE OR DELETE ON hr_qa_profiles FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();REVOKE ALL ON TABLE hr_qa_profiles FROM PUBLIC;");}
$old=DB::table('hr_qa_profiles')->get();if($old->isNotEmpty()){abort_unless($old->count()===3&&$old->every(fn($r)=>hash_equals($pin,$r->source_hash)),409,'Preservar perfis RH diferentes.');return['profiles'=>$old->all(),'replayed'=>true,'source_hash'=>$pin,'new_grants'=>0];}
$check=DB::selectOne("SELECT pg_get_constraintdef(oid) d FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check'")->d;abort_unless(preg_match('/^CHECK \((.*)\)$/s',$check,$m),503);if(!str_contains($check,'human_resources.remunerations.view')){DB::statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check');DB::statement('ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK (('.$m[1].") OR (unit_id IS NOT NULL AND permission IN('human_resources.remunerations.view','human_resources.remunerations.prepare','human_resources.remunerations.review')))");}
foreach($p['profiles']as$profile){$email='sgf-'.$profile['id'].'-'.CompositionRuntime::physical()['composition'].'@example.test';abort_if(User::where('email',$email)->exists(),409,'Identidade existente sem registo: conferir, sem adoptar.');$user=User::create(['name'=>$profile['label'],'email'=>$email,'password'=>bin2hex(random_bytes(32))]);DB::table('company_memberships')->insert(['user_id'=>$user->id,'tenant_id'=>1,'company_id'=>2]);foreach($profile['rights']as$r){$g=['user_id'=>$user->id,'tenant_id'=>1,'company_id'=>2,'unit_id'=>$r['scope']==='unit'?2:null,'permission'=>$r['permission']];DB::table('permission_grants')->insert($g);Audit::record('access.granted_on_creation',null,1,2,$g['unit_id'],$user->id,['permission'=>$r['permission'],'reason'=>'Matriz de RH exclusivamente QA autorizada11/11.']);}DB::table('hr_qa_profiles')->insert(['profile_key'=>$profile['id'],'user_id'=>$user->id]+$p['scope']+['label'=>$profile['label'],'source_hash'=>$pin]);Audit::record('access.synthetic_hr_profile_created',null,1,2,2,$user->id,['reason'=>'Perfil autónomo QA de RH expressamente autorizado.']);}
return['profiles'=>DB::table('hr_qa_profiles')->orderBy('profile_key')->get()->all(),'replayed'=>false,'source_hash'=>$pin,'new_grants'=>11];
});
}
}