Files
SGF/scripts/sgf-stock-final-install.php
2026-11-11 09:00:17 +02:00

53 lines
7.3 KiB
PHP

<?php
// Only the two existing QA books. No server, database, PostgreSQL role or public grant.
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';
$policy=json_decode(file_get_contents($repo.'/docs/integration/sgf-stock-final-access-proposal.json'),true);
if(($policy['approved']??false)!==true)throw new LogicException('Explicit approval required');
$profiles=json_decode(file_get_contents($s['root'].'/single-profiles.json'),true)['profiles'];$reviewer=$profiles['workshop_verify']['user_id'];$ledger=json_decode(file_get_contents($s['root'].'/single-native-ledger.json'),true);
$native->transaction(function()use($native,$policy,$tag){
// Legacy QA role mode ignores organization profiles. Preserve every existing role verbatim
// before enabling the native profile mechanism required by the approved seven functions.
if(!$native->table('organization_access')->where(['tenant_id'=>1,'active'=>true])->exists()){
foreach($native->table('users')->where('tenant_id',1)->get(['id','purchasing_role']) as $u){
$rights=$u->purchasing_role===null||$u->purchasing_role==='configured'?[]:[$u->purchasing_role];
if($rights===[])continue;$name='SGF-PRESERVE-QA-ROLE-'.$tag.'-'.$u->id;
$id=$native->table('organization_profiles')->where(['tenant_id'=>1,'name'=>$name])->value('id');
if(!$id){$id=$native->table('organization_profiles')->insertGetId(['tenant_id'=>1,'name'=>$name,'permissions'=>json_encode($rights),'version'=>1,'created_at'=>now(),'updated_at'=>now()]);$native->table('organization_memberships')->insert(['tenant_id'=>1,'user_id'=>$u->id,'profile_id'=>$id,'maintenance'=>false,'created_at'=>now(),'updated_at'=>now()]);}
}
$native->table('organization_access')->updateOrInsert(['tenant_id'=>1],['active'=>true,'version'=>1,'created_at'=>now(),'updated_at'=>now()]);
$native->table('audit_logs')->insert(['tenant_id'=>1,'user_id'=>6,'action'=>'sgf.stock.final.qa_profile_mode','before'=>json_encode(['active'=>false]),'after'=>json_encode(['active'=>true,'existing_roles_preserved'=>true,'approval'=>$policy['approval']]),'created_at'=>now()]);
}
if(!$native->getSchemaBuilder()->hasTable('sgf_stock_transit_events')){
$native->unprepared("CREATE TABLE sgf_stock_transit_events(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,transfer_key uuid NOT NULL,command_key uuid NOT NULL UNIQUE,request_hash text NOT NULL,action text NOT NULL CHECK(action IN ('dispatch','receive','return')),actor_id bigint NOT NULL,host_actor_id bigint NOT NULL,stock_item_id bigint NOT NULL REFERENCES stock_items(id),source_warehouse_id bigint NOT NULL REFERENCES warehouses(id),destination_warehouse_id bigint NOT NULL REFERENCES warehouses(id),quantity_milli bigint NOT NULL CHECK(quantity_milli>0),movement_id bigint NOT NULL UNIQUE REFERENCES stock_movements(id),reason text NOT NULL,snapshot jsonb NOT NULL,created_at timestamp NOT NULL,CHECK(source_warehouse_id<>destination_warehouse_id))");
$native->unprepared("CREATE UNIQUE INDEX sgf_transit_one_dispatch ON sgf_stock_transit_events(tenant_id,company_id,unit_id,transfer_key) WHERE action='dispatch'");
$native->unprepared('CREATE INDEX sgf_transit_scope ON sgf_stock_transit_events(tenant_id,company_id,unit_id,transfer_key,id)');
$native->unprepared('CREATE TRIGGER sgf_transit_immutable BEFORE UPDATE OR DELETE ON sgf_stock_transit_events FOR EACH ROW EXECUTE FUNCTION gl_append_only()');
$native->statement('REVOKE ALL ON TABLE sgf_stock_transit_events FROM PUBLIC');$native->statement('REVOKE ALL ON SEQUENCE sgf_stock_transit_events_id_seq FROM PUBLIC');
}
foreach($policy['existing_native_functions_to_grant_only_in_qa'] as $grant){
$name='SGF-STOCK-FINAL-QA-'.$tag.'-'.$grant['native_user_id'];$profile=$native->table('organization_profiles')->where(['tenant_id'=>1,'name'=>$name])->first();
if($profile){if(json_decode($profile->permissions,true)!==$grant['functions'])throw new LogicException('Preserve differing native profile');continue;}
$id=$native->table('organization_profiles')->insertGetId(['tenant_id'=>1,'name'=>$name,'permissions'=>json_encode($grant['functions']),'version'=>1,'created_at'=>now(),'updated_at'=>now()]);
$native->table('organization_memberships')->insert(['tenant_id'=>1,'user_id'=>$grant['native_user_id'],'profile_id'=>$id,'maintenance'=>false,'created_at'=>now(),'updated_at'=>now()]);
$native->table('audit_logs')->insert(['tenant_id'=>1,'user_id'=>$grant['native_user_id'],'action'=>'sgf.stock.final.qa_access','before'=>null,'after'=>json_encode(['approval'=>$policy['approval'],'functions'=>$grant['functions'],'real_access'=>false]),'created_at'=>now()]);
}
});
$host->transaction(function()use($host,$f,$reviewer,$policy){
$old=$host->selectOne("SELECT pg_get_constraintdef(oid) value FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check'")->value;
if(!str_contains($old,'stock.transfers.receive')){
if(!preg_match('/^CHECK \((.*)\)$/s',$old,$m))throw new LogicException('Grant schema changed');
$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 ('stock.transfers.receive','stock.lots.review')))");
}
foreach($policy['new_host_rights'] as $grant){$values=$f['scope']+['user_id'=>$reviewer,'permission'=>$grant['permission']];if($host->table('permission_grants')->where($values)->exists())continue;
$host->table('permission_grants')->insert($values);App\Modules\Identity\Services\Audit::record('access.granted_on_creation',null,1,2,2,$reviewer,['permission'=>$grant['permission'],'reason'=>$policy['approval'],'limits'=>$grant,'real_access'=>false]);}
});
$bindingsFile=$s['root'].'/single-bindings.json';$b=json_decode(file_get_contents($bindingsFile),true);$key='1:2:2:'.$reviewer;copy($bindingsFile,$work.'/single-bindings-before.json');
if(!in_array($b['bindings'][$key]['actor_id'],[2,3],true))throw new LogicException('Unexpected reviewer binding');$b['bindings'][$key]['actor_id']=3;
file_put_contents($bindingsFile,json_encode($b,JSON_PRETTY_PRINT));
$value=['version'=>$policy['version'],'composition'=>$tag,'scope'=>$f['scope'],'dispatcher_host_id'=>$f['validator'],'reviewer_host_id'=>$reviewer,'dispatcher_native_id'=>6,'reviewer_native_id'=>3,'source_warehouse_id'=>$ledger['warehouse_id'],'destination_warehouse_id'=>$nf['warehouse']];
if(is_file($s['root'].'/stock-final-binding.json')){$old=json_decode(file_get_contents($s['root'].'/stock-final-binding.json'),true);foreach(['lot_item_id','transfer_item_id'] as $field)if(isset($old[$field]))$value[$field]=$old[$field];}
file_put_contents($s['root'].'/stock-final-binding.json',json_encode($value,JSON_PRETTY_PRINT));
$pins=json_decode(file_get_contents($s['root'].'/single-binding-pins.json'),true);foreach(['single-bindings.json','stock-final-binding.json'] as $name)$pins[$name]=hash_file('sha256',$s['root'].'/'.$name);file_put_contents($s['root'].'/single-binding-pins.json',json_encode($pins,JSON_PRETTY_PRINT));
echo json_encode(['passed'=>true,'book'=>$book,'approved_qa_rights'=>2,'native_functions'=>7,'new_pg_roles'=>0,'public_grants'=>0,'proof_directory'=>$work]).PHP_EOL;