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;