commit melhorias sgf

This commit is contained in:
2026-11-11 09:00:17 +02:00
parent 0b05a1192c
commit 1d11a5e57b
736 changed files with 40746 additions and 210 deletions
@@ -0,0 +1,29 @@
<?php
namespace App\Shared\Installation;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Support\Facades\DB;
/** The composed presentation remains physically isolated, regardless of environment labels. */
final class CompositionRuntime
{
public static function physical(?ConnectionInterface $db=null):array
{
$db??=DB::connection();$c=$db->getConfig();$run=(string)getenv('SGF_ISOLATED_REGRESSION_RUN');
abort_unless($db->getDriverName()==='pgsql'&&preg_match('/^budget_core_sgf_([a-f0-9]{12})$/D',$db->getDatabaseName(),$m)
&&($c['username']??null)==='ir_composition_'.$m[1]&&($c['host']??null)==='127.0.0.1'&&(int)($c['port']??0)===55439
&&preg_match('/^[a-f0-9]{12}$/D',$run)&&empty($c['url'])&&empty($c['read'])&&empty($c['write'])&&($c['search_path']??'public')==='public',403);
$actual=$db->selectOne("SELECT current_database() db,current_user role,inet_server_port() port,current_setting('cluster_name') cluster,rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls elevated FROM pg_roles WHERE rolname=current_user");
abort_unless($actual->db===$db->getDatabaseName()&&$actual->role===$c['username']&&(int)$actual->port===55439&&$actual->cluster==='sgf_modular_regression_'.$run&&!$actual->elevated,403);
return ['database'=>$actual->db,'role'=>$actual->role,'cluster'=>$actual->cluster,'composition'=>$m[1]];
}
public static function assert(?string $connection=null):void
{
abort_unless(app()->environment('budget-testing')&&config('sgf_single_installation.enabled')===true,403);
$db=DB::connection($connection);self::physical($db);$pin=config('sgf_single_installation.schema_sha256');
abort_unless(is_string($pin)&&preg_match('/^[a-f0-9]{64}$/D',$pin)&&$db->getSchemaBuilder()->hasTable('sgf_composition_schema'),503,'Composição SGF não instalada ou não conferida.');
abort_unless(hash_equals($pin,(string)$db->table('sgf_composition_schema')->where('id',1)->value('source_hash')),503,'A composição SGF mudou; exige nova conferência.');
}
}
@@ -0,0 +1,102 @@
<?php
namespace App\Shared\Installation;
use Illuminate\Support\Facades\{DB,Schema};
/** Explicit additive composition, separate from global migrate and the SGC connector. */
final class CompositionSchema
{
public const MIGRATIONS=[
'2026_10_25_030000_create_services_delivery_a.php',
'2026_10_25_040000_create_internal_confirmations.php',
'2026_10_26_010000_create_document_preparation.php',
'2026_10_26_020000_create_document_preview_models.php',
'2026_10_26_030000_create_service_calculations.php',
'2026_10_26_040000_create_catalogue_accounting.php',
'2026_10_26_050000_create_service_fiscal_lifecycle.php',
'2026_10_26_060000_create_test_invoice_issuance.php',
'2026_10_27_070000_create_simulated_receivables.php',
'2026_10_27_080000_create_workshop_resource_records.php',
'2026_10_27_090000_create_workshop_direct_costs.php',
'2026_10_27_100000_create_industrial_cost_simulation.php',
'2026_10_28_110000_create_industrial_budgets.php',
'2026_10_28_120000_create_workshop_billing_drafts.php',
'2026_10_28_130000_create_workshop_billing_corrections.php',
];
public static function identity():array
{
abort_unless(app()->runningInConsole()&&app()->environment('budget-testing'),403);
return CompositionRuntime::physical();
}
/** Callback may prove installed structures inside the same transaction.
* Default is a full rollback. No role, grant, account, association or document is created.
*/
public function run(bool $commit=false,?\Closure $verify=null):array
{
$identity=self::identity();$host=DB::connection();
abort_unless($host->transactionLevel()===0,409,'Não compor dentro de uma transacção alheia.');
$constraint=$host->selectOne("SELECT pg_get_constraintdef(oid) value FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check'")->value??null;
if(!is_string($constraint)||!preg_match('/^CHECK \((.*)\)$/s',$constraint,$m))throw new \LogicException('COMPOSITION_PERMISSION_CHECK_MISSING');
$connection=new CompositionSchemaConnection($host->getPdo(),$host->getDatabaseName(),'',$host->getConfig());
$connection->originalPermissionCheck=$m[1];
$name='sgf_composition_schema';$original=DB::getDefaultConnection();
config(['database.connections.'.$name=>$host->getConfig()]);DB::extend($name,fn()=>$connection);DB::setDefaultConnection($name);Schema::clearResolvedInstance('db.schema');app()->forgetInstance('db.schema');
$result=[];$connection->beginTransaction();
try{
$connection->statement("SET LOCAL lock_timeout='5s'");$connection->statement("SET LOCAL statement_timeout='30s'");
$connection->selectOne("SELECT pg_advisory_xact_lock(hashtext('sgf-single-installation-schema'))");
if($connection->getSchemaBuilder()->hasTable('sgf_composition_schema')){
$stored=$connection->table('sgf_composition_schema')->where('id',1)->first();
if(!$stored||$stored->source_hash!==$this->fingerprint())throw new \LogicException('COMPOSITION_SCHEMA_VERSION_CHANGED');
$result=['identity'=>$identity,'replayed'=>true,'source_hash'=>$stored->source_hash,'reused_tables'=>[],'migrations'=>[]];
}else{
$before=array_column($connection->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"),'tablename');
foreach(['b_internal_confirmations','doc_records','wo_orders','sr_records','ca_proposals'] as $table)if(in_array($table,$before,true))throw new \LogicException('COMPOSITION_PARTIAL_SCHEMA_REQUIRES_REVIEW: '.$table);
if(!$connection->selectOne("SELECT to_regprocedure('public.sgf_append_only_audit()') value")->value)$connection->unprepared("CREATE FUNCTION sgf_append_only_audit() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN RAISE EXCEPTION 'SGF_APPEND_ONLY_AUDIT'; END; \$\$;");
$migrations=[];
$auditTables=['a_history','a_events','a_commands','a_outbox','a_pending'];$present=array_intersect($auditTables,$before);
if(count($present)!==0&&count($present)!==count($auditTables))throw new \LogicException('COMPOSITION_PARTIAL_A_HISTORY');
foreach(self::MIGRATIONS as $index=>$file){
if($index===0&&count($present)===count($auditTables)){
foreach(['a_clients','a_services','a_service_drafts','a_billing_drafts'] as $table){
if(!in_array($table,$before,true))throw new \LogicException('COMPOSITION_PARTIAL_A_RECORDS');
$connection->getSchemaBuilder()->create($table,fn()=>null);
}
$guards=$connection->selectOne("SELECT count(*) n FROM pg_trigger WHERE NOT tgisinternal AND tgenabled='O' AND tgname IN ('a_clients_guard','a_services_guard','a_service_drafts_guard','a_billing_drafts_guard','a_history_scope','a_history_append_only','a_events_append_only','a_commands_append_only','a_outbox_append_only','a_pending_append_only')")->n;
if((int)$guards!==10)throw new \LogicException('COMPOSITION_A_GUARDS_INCOMPLETE');
continue;
}
$migration=require database_path('migrations/'.$file);$migration->up();$migrations[]=$file;
}
$connection->unprepared('CREATE TABLE sgf_composition_schema(id smallint PRIMARY KEY CHECK(id=1),source_hash char(64) NOT NULL,installed_at timestamptz NOT NULL DEFAULT now())');
$connection->table('sgf_composition_schema')->insert(['id'=>1,'source_hash'=>$this->fingerprint()]);
$connection->unprepared('CREATE TRIGGER composition_schema_append_only BEFORE UPDATE OR DELETE ON sgf_composition_schema FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit()');
// Rebind ONLY the newly created QA guards to this exact database,
// role and cluster. The source SQL and all other installations retain
// their original guard; no general local/development bypass is added.
foreach(['wo','sr','dc','ic'] as $prefix){
$connection->unprepared("CREATE OR REPLACE FUNCTION sgf_{$prefix}_environment() RETURNS void LANGUAGE plpgsql AS \$\$ BEGIN IF current_database() IS DISTINCT FROM '".$identity['database']."' OR current_user IS DISTINCT FROM '".$identity['role']."' OR inet_server_port() IS DISTINCT FROM 55439 OR current_setting('cluster_name') IS DISTINCT FROM '".$identity['cluster']."' THEN RAISE EXCEPTION 'SGF_COMPOSITION_OWNED_QA_ONLY'; END IF; END; \$\$;");
}
$after=array_column($connection->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"),'tablename');
$result=['identity'=>$identity,'replayed'=>false,'source_hash'=>$this->fingerprint(),'reused_tables'=>$connection->getSchemaBuilder()->reused,'new_tables'=>array_values(array_diff($after,$before)),'migrations'=>$migrations];
}
if($verify)$result['verification']=$verify($connection);
if($commit)$connection->commit();else $connection->rollBack();
$result['committed']=$commit;return $result;
}finally{
while($connection->transactionLevel()>0)$connection->rollBack();
DB::setDefaultConnection($original);Schema::clearResolvedInstance('db.schema');app()->forgetInstance('db.schema');DB::purge($name);
}
}
public function fingerprint():string
{
$files=[];foreach(self::MIGRATIONS as $file)$files['database/migrations/'.$file]=hash_file('sha256',database_path('migrations/'.$file));
foreach(glob(database_path('sql/*.sql')) as $file)$files['database/sql/'.basename($file)]=hash_file('sha256',$file);
foreach(['CompositionSchema.php','CompositionSchemaConnection.php','CompositionSchemaBuilder.php','CompositionRuntime.php'] as $file)$files[$file]=hash_file('sha256',__DIR__.'/'.$file);
return hash('sha256',json_encode($files,JSON_THROW_ON_ERROR));
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Shared\Installation;
use Illuminate\Database\Schema\PostgresBuilder;
/** Reuse only the four explicitly verified foundation tables; never skip arbitrary DDL. */
final class CompositionSchemaBuilder extends PostgresBuilder
{
public array $reused=[];
public function create($table,\Closure $callback)
{
if(in_array($table,['a_clients','a_services','a_service_drafts','a_billing_drafts'],true)&&$this->hasTable($table)){
$expected=['id','tenant_id','company_id','unit_id','name','version','data','created_at','updated_at'];
if(in_array($table,['a_service_drafts','a_billing_drafts'],true))$expected=[...$expected,'client_id','state'];
if($table==='a_billing_drafts')$expected[]='source_id';
$columns=$this->getColumns($table);$names=array_column($columns,'name');sort($expected);sort($names);
if($names!==$expected)throw new \LogicException('COMPOSITION_EXISTING_TABLE_INCOMPATIBLE: '.$table);
foreach($columns as $column){
$type=$column['type_name'];$name=$column['name'];
$valid=match($name){'data'=>$type==='jsonb','name','state'=>$type==='varchar','created_at','updated_at'=>$type==='timestamptz','version'=>in_array($type,['int4','integer'],true),default=>in_array($type,['int8','bigint'],true)};
if(!$valid)throw new \LogicException('COMPOSITION_EXISTING_COLUMN_INCOMPATIBLE: '.$table.'.'.$name);
}
$this->reused[]=$table;
return;
}
parent::create($table,$callback);
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Shared\Installation;
use Illuminate\Database\PostgresConnection;
/** Compatibility at installation time only; runtime business connections are untouched. */
final class CompositionSchemaConnection extends PostgresConnection
{
public string $originalPermissionCheck;
public ?CompositionSchemaBuilder $compositionBuilder=null;
public function getSchemaBuilder()
{
$this->useDefaultSchemaGrammar();
return $this->compositionBuilder??=new CompositionSchemaBuilder($this);
}
public function statement($query,$bindings=[])
{
// Older migrations replaced the permission whitelist. Preserve its original
// branches as well as the migration's additions, without granting any right.
if(preg_match('/^ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK \((.*)\)$/s',$query,$m)){
$query='ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK (('.$this->originalPermissionCheck.') OR ('.$m[1].'))';
}
return parent::statement($query,$bindings);
}
}
@@ -0,0 +1,25 @@
<?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];
});
}
}
@@ -0,0 +1,75 @@
<?php
namespace App\Shared\Installation;
use App\Models\User;
use App\Shared\Context\{CompanyContext,ModuleNavigation};
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/** One explicit chooser over approved local identities, not an authentication bypass outside QA. */
final class PresentationEntry
{
private const TOUR=[
'commercial'=>['sgf:read_only','/?start=service_invoice_setup','Comercial','Clientes, orçamentos, documentos e PDF; passagem explícita para recebíveis'],
'purchasing'=>['purchasing:actor','/purchasing/workspace','Compras','Fornecedores, requisição, propostas, ordem, recepção, factura e obrigação'],
'stock'=>['purchasing:validator','/stock/final','Stock','Artigos, armazéns, existências, trânsito, lotes, contagem e histórico'],
'workshop'=>['sgf:workshop_manage','/?start=workshop','Oficinas','Equipamento, cliente, agenda, ordem, autorização e apontamentos verificados'],
'finance'=>['sgf:read_only','/?start=service_receivables','Financeiro','Recebíveis, obrigações, Caixa, conferência de extracto e indicadores'],
'accounting'=>['sgf:read_only','/general-ledger','Contabilidade','Origens, diário integrado, plano, balancete, razão e mapas fiscais internos'],
'human_resources'=>['hr:hr_read','/?start=remunerations','Recursos Humanos','Cadastro fictício, regras artificiais, processamento interno e revisão distinta; acesso próprio'],
];
private function guard():void
{
abort_unless(in_array(request()->ip(),['127.0.0.1','::1'],true),404);
CompositionRuntime::assert();
abort_unless(is_string(config('sgf_single_installation.profiles_sha256'))&&preg_match('/^[a-f0-9]{64}$/D',config('sgf_single_installation.profiles_sha256')),503);
}
private function profiles():array
{
$pin=config('sgf_single_installation.profiles_sha256');$out=[];$context=app(CompanyContext::class);
$rows=DB::table('sgf_presentation_profiles')->orderBy('profile_key')->get();
abort_unless($rows->count()===10&&$rows->every(fn($r)=>hash_equals($r->source_hash,$pin)),503,'Perfis QA não conferidos.');
try{
foreach($rows as $row){
$context->selectFor((int)$row->user_id,(int)$row->tenant_id,(int)$row->company_id,(int)$row->unit_id);
$out['sgf:'.$row->profile_key]=['user_id'=>(int)$row->user_id,'label'=>$row->label,'scope'=>['tenant_id'=>(int)$row->tenant_id,'company_id'=>(int)$row->company_id,'unit_id'=>(int)$row->unit_id],'modules'=>app(ModuleNavigation::class)->available($context)];
}
if(config('human_resources.enabled')===true){foreach(HumanResourcesProfiles::registered() as$row){
$context->selectFor($row->user_id,$row->tenant_id,$row->company_id,$row->unit_id);
$out['hr:'.$row->profile_key]=['user_id'=>$row->user_id,'label'=>$row->label,'scope'=>['tenant_id'=>$row->tenant_id,'company_id'=>$row->company_id,'unit_id'=>$row->unit_id],'modules'=>app(ModuleNavigation::class)->available($context)];
}}
$scope=config('purchasing_workspace.scope');
foreach(config('purchasing_workspace.profiles',[]) as $key=>$profile){
$context->selectFor($profile['user_id'],$scope['tenant_id'],$scope['company_id'],$scope['unit_id']);
$out['purchasing:'.$key]=['user_id'=>$profile['user_id'],'label'=>$profile['label'].' — Compras','scope'=>$scope,'modules'=>app(ModuleNavigation::class)->available($context)];
}
}finally{$context->clear();}
return $out;
}
public function screen(Request $r)
{
$this->guard();abort_if($r->query()!==[],422);
$profiles=$this->profiles();$tour=[];foreach(self::TOUR as $module=>[$key,$url,$label,$description])if(isset($profiles[$key])&&in_array($module,array_column($profiles[$key]['modules'],'id'),true))$tour[$module]=['profile'=>$key,'function'=>$profiles[$key]['label'],'label'=>$label,'description'=>$description];
$primary=['commercial'=>['sgf:commercial_prepare','sgf:commercial_review'],'purchasing'=>array_map(fn($id)=>'purchasing:'.$id,['requester','approver','actor','issuer','validator','reviewer','auditor']),'stock'=>['purchasing:validator','sgf:workshop_verify','sgf:read_only'],'workshop'=>['sgf:workshop_manage','sgf:workshop_record','sgf:workshop_verify','sgf:finance_prepare','sgf:finance_review','purchasing:validator'],'finance'=>['sgf:finance_prepare','sgf:finance_review'],'accounting'=>['sgf:accounting_prepare','sgf:accounting_review','sgf:finance_prepare','sgf:finance_review']];
if(config('human_resources.enabled')===true)$primary['human_resources']=['hr:hr_prepare','hr:hr_review','hr:hr_read'];
$moduleProfiles=[];foreach($primary as $module=>$keys)foreach($keys as $key)if(isset($profiles[$key])&&in_array($module,array_column($profiles[$key]['modules'],'id'),true))$moduleProfiles[$module][$key]=$profiles[$key];
return response()->view('sgf-presentation-entry',['profiles'=>$profiles,'tour'=>$tour,'moduleProfiles'=>$moduleProfiles])->header('Cache-Control','no-store')->header('X-SGF-Installation',CompositionRuntime::physical()['composition']);
}
public function enter(Request $r)
{
$this->guard();$d=$r->validate(['profile'=>'required|string|max:100','module_id'=>'required|string|max:30','landing'=>'sometimes|in:'.implode(',',array_keys(self::TOUR))]);
abort_if(array_diff(array_keys($r->all()),['_token','profile','module_id','landing']),422);
$target='/';if(isset($d['landing'])){$route=self::TOUR[$d['landing']];abort_unless($d['profile']===$route[0]&&$d['module_id']===$d['landing'],403);$target=$route[1];}
$profile=$this->profiles()[$d['profile']]??null;
abort_unless($profile&&in_array($d['module_id'],array_column($profile['modules'],'id'),true),403,'Função ou módulo não autorizado.');
$user=User::findOrFail($profile['user_id']);auth()->login($user);$r->session()->regenerate();
$r->session()->put('auth_version',$user->auth_version);
$r->session()->put('company_context',$profile['scope']+['module_id'=>$d['module_id'],'token'=>bin2hex(random_bytes(24))]);
\App\Modules\Identity\Services\Audit::record('context.synthetic_profile_selected',$user->id,$profile['scope']['tenant_id'],$profile['scope']['company_id'],$profile['scope']['unit_id'],details:['module_id'=>$d['module_id'],'reason'=>'Escolha explícita da função QA aprovada, na instalação única.']);
return redirect($target);
}
}
@@ -0,0 +1,84 @@
<?php
namespace App\Shared\Installation;
use App\Models\User;
use App\Modules\Identity\Services\Audit;
use Illuminate\Support\Facades\DB;
/** Explicitly approved fixture installation. No runtime grants, role creation or legacy assignment. */
final class SyntheticProfiles
{
public static function policy():array
{
$p=json_decode(file_get_contents(base_path('docs/integration/sgf-8085-access-proposal.json')),true,64,JSON_THROW_ON_ERROR);
abort_unless(($p['approved']??false)===true&&($p['authorization']['decision']??null)==='autorizo'&&($p['scope']??null)===['tenant_id'=>1,'company_id'=>2,'unit_id'=>2],403);
return $p;
}
public static function fingerprint(array $p):string
{
return hash('sha256',json_encode(array_intersect_key($p,array_flip(['version','scope','profiles','existing_stock_profile','new_right_names','financial_source_policy'])),JSON_THROW_ON_ERROR));
}
public function install():array
{
CompositionSchema::identity();$p=self::policy();$pin=self::fingerprint($p);$scope=$p['scope'];
$known=[];
foreach([base_path('app/Modules/Services/DocumentPreview.php'),base_path('app/Modules/Purchasing/ReceiptRuntime.php'),...glob(database_path('migrations/*.php'))] as $file){preg_match_all("/'([a-z_]+\\.[a-z_.]+)'/",file_get_contents($file),$matches);$known=[...$known,...$matches[1]];}
foreach(\App\Modules\Services\ServicePermissions::AREAS as $area)foreach(\App\Modules\Services\ServicePermissions::ACTIONS as $action)$known[]=$area.'.'.$action;
foreach(['view','prepare','validate','post','correct_prepare','correct_validate','correct_post','period_prepare','period_validate','period_close'] as $action)$known[]='general_ledger.'.$action;
foreach(['view','prepare','confirm'] as $action)$known[]='workshop_stock.'.$action;
$known=[...$known,'stock.view','stock.operations.prepare','stock.operations.confirm'];
foreach($p['profiles'] as $profile)foreach($profile['rights'] as $right)abort_unless(in_array($right['permission'],$known,true),422,'Direito fora do catálogo existente/aprovado: '.$right['permission']);
foreach($p['existing_stock_profile']['rights'] as $right)abort_unless(in_array($right,$known,true),422);
return DB::transaction(function()use($p,$pin,$scope){
DB::selectOne("SELECT pg_advisory_xact_lock(hashtext('sgf-approved-presentation-profiles'))");
DB::table('companies')->where('id',$scope['company_id'])->where('tenant_id',$scope['tenant_id'])->lockForUpdate()->firstOrFail();
abort_unless(DB::table('units')->where(['tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id'],'id'=>$scope['unit_id']])->exists(),403);
if(!DB::getSchemaBuilder()->hasTable('sgf_presentation_profiles')){
DB::unprepared('CREATE TABLE sgf_presentation_profiles(profile_key varchar(80) PRIMARY KEY,user_id bigint NOT NULL UNIQUE 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())');
DB::statement('REVOKE ALL ON TABLE sgf_presentation_profiles FROM PUBLIC');
DB::statement('CREATE TRIGGER sgf_presentation_profiles_append_only BEFORE UPDATE OR DELETE ON sgf_presentation_profiles FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit()');
}
$existing=DB::table('sgf_presentation_profiles')->get();
if($existing->isNotEmpty()){
abort_unless($existing->count()===count($p['profiles'])&&$existing->every(fn($v)=>hash_equals($v->source_hash,$pin)),409,'A matriz de perfis mudou; não ampliar acessos por inferência.');
return ['profiles'=>$existing->keyBy('profile_key')->map(fn($r)=>['user_id'=>(int)$r->user_id,'label'=>$r->label])->all(),'replayed'=>true,'source_hash'=>$pin];
}
$company=[];$unit=[];
foreach($p['profiles'] as $profile)foreach($profile['rights'] as $r){
abort_unless(preg_match('/^[a-z][a-z_.]+$/D',$r['permission'])&&in_array($r['scope'],['company','unit'],true),422);
if($r['scope']==='company')$company[]=$r['permission'];else $unit[]=$r['permission'];
}
$unit=[...$unit,...$p['existing_stock_profile']['rights']];
$old=DB::selectOne("SELECT pg_get_constraintdef(oid) value FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check'")->value;
if(!preg_match('/^CHECK \((.*)\)$/s',$old,$m))throw new \LogicException('Permission schema changed');
$literal=fn($values)=>implode(',',array_map(fn($v)=>"'".$v."'",array_unique($values)));
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 NULL AND permission IN ('.$literal($company).')) OR (unit_id IS NOT NULL AND permission IN ('.$literal($unit).')))');
$profiles=[];
foreach($p['profiles'] as $profile){
abort_if(User::where('email','sgf-'.$profile['id'].'-'.CompositionRuntime::physical()['composition'].'@example.test')->exists(),409,'Conta existente não registada nesta matriz.');
$user=User::create(['name'=>$profile['label'],'email'=>'sgf-'.$profile['id'].'-'.CompositionRuntime::physical()['composition'].'@example.test','password'=>bin2hex(random_bytes(32))]);
DB::table('company_memberships')->insert(['user_id'=>$user->id,'tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id']]);
foreach($profile['rights'] as $right)$this->grant($user->id,$scope,$right['permission'],$right['scope']==='unit');
DB::table('sgf_presentation_profiles')->insert(['profile_key'=>$profile['id'],'user_id'=>$user->id]+$scope+['label'=>$profile['label'],'source_hash'=>$pin]);
Audit::record('access.synthetic_profile_created',null,$scope['tenant_id'],$scope['company_id'],$scope['unit_id'],$user->id,['operator'=>'Autorização expressa do proprietário em10/11/2026','reason'=>'Perfil exclusivamente sintético da matriz aprovada.']);
$profiles[$profile['id']]=['user_id'=>$user->id,'label'=>$profile['label']];
}
$stock=(int)$p['existing_stock_profile']['user_id'];
abort_unless(DB::table('company_memberships')->where(['user_id'=>$stock,'tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id']])->exists(),403);
foreach($p['existing_stock_profile']['rights'] as $right)$this->grant($stock,$scope,$right,true);
return ['profiles'=>$profiles,'replayed'=>false,'source_hash'=>$pin];
});
}
private function grant(int $user,array $scope,string $right,bool $unit):void
{
$values=['tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id'],'unit_id'=>$unit?$scope['unit_id']:null,'user_id'=>$user,'permission'=>$right];
if(DB::table('permission_grants')->where($values)->exists())return;
DB::table('permission_grants')->insert($values);
Audit::record('access.granted_on_creation',null,$scope['tenant_id'],$scope['company_id'],$values['unit_id'],$user,['permission'=>$right,'operator'=>'Autorização10/11/2026','reason'=>'Apenas perfil QA e âmbito da matriz aprovada; sem acesso real.']);
}
}