Files
SGF/scripts/sgf-composition-flow-proof.php
T
2026-11-11 09:00:17 +02:00

47 lines
6.6 KiB
PHP

<?php
use Illuminate\Support\Facades\{Auth,DB,Event};
use Illuminate\Support\Str;
use App\Shared\Context\{CompanyContext,ModuleRecords};
use App\Shared\Installation\CompositionSchema;
use App\Modules\Services\ServiceWorkflow;
use App\Modules\Workshop\WorkshopWorkflow;
require __DIR__.'/sgf-composition-bootstrap.php';
$f=json_decode(file_get_contents($s['root'].'/fixture.json'),true);
$checks=[];$ok=function($v,$name)use(&$checks,$proof){if(!$v)throw new RuntimeException($name);$checks[]=$name;file_put_contents($proof.'/progress.json',json_encode($checks));};
$snapshot=function($db){$tables=[];foreach($db->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename") as $r){$table=$r->tablename;if(!preg_match('/^[a-z_][a-z0-9_]*$/D',$table))throw new LogicException('Table');$tables[$table]=(array)$db->selectOne('SELECT count(*) rows,md5(coalesce(string_agg(row_to_json(t)::text,chr(10) ORDER BY row_to_json(t)::text),\'\')) hash FROM "'.$table.'" t');}return hash('sha256',json_encode($tables));};
$before=$snapshot($host);$installer=new CompositionSchema;
$result=$installer->run(false,function($db)use($app,$installer,$f,$ok){
config(['sgf_single_installation.enabled'=>true,'sgf_single_installation.schema_sha256'=>$installer->fingerprint()]);
$id=(int)$db->table('permission_grants')->max('id')+1000;
foreach(['clients.view','clients.create','services.view','services.create','service_drafts.view','service_drafts.create','billing_drafts.view','billing_drafts.create','workshop.view','workshop.open','workshop.assign','workshop.authorize','workshop.record','workshop.verify'] as $right){
$scope=['tenant_id'=>$f['tenant'],'company_id'=>$f['company'],'unit_id'=>str_starts_with($right,'clients.')||str_starts_with($right,'services.')?null:$f['unit'],'user_id'=>$f['actor'],'permission'=>$right];
if(!$db->table('permission_grants')->where($scope)->exists())$db->table('permission_grants')->insert(['id'=>$id++]+$scope);
}
$user=App\Models\User::findOrFail($f['actor']);Auth::guard('web')->setUser($user);$session=app('session')->driver();$session->start();$session->put('company_context',$f['scope']+['token'=>'single-composition-proof','module_id'=>'commercial']);
$request=Illuminate\Http\Request::create('/api/a/clients','POST');$request->setLaravelSession($session);$request->setUserResolver(fn()=>$user);$request->attributes->set('sgf_module','commercial');$app->instance('request',$request);
$ctx=app(CompanyContext::class);$ctx->selectFor($user->id,$f['tenant'],$f['company'],$f['unit']);
$service=app(ServiceWorkflow::class);$client=$service->save('clients',null,$user->id,['name'=>'Cliente novo do percurso único QA'],(string)Str::uuid())['id'];
$catalogue=$service->save('services',null,$user->id,['name'=>'Diagnóstico de ensaio'],(string)Str::uuid())['id'];
$draft=$service->save('service_drafts',null,$user->id,['client_id'=>$client,'currency'=>'MZN','lines'=>[['service_id'=>$catalogue,'description'=>'Diagnóstico QA','quantity'=>'1','unit_price'=>'100']]],(string)Str::uuid());
$billing=$service->save('billing_drafts',null,$user->id,['source_id'=>$draft['id']],(string)Str::uuid());
$ok($db->table('a_billing_drafts')->where('id',$billing['id'])->value('source_id')===$draft['id'],'Comercial preserves service-to-billing origin in the composed host');
$ok(ModuleRecords::query('a_clients')->where('id',$client)->exists(),'new client explicitly belongs to Comercial');
$request->attributes->set('sgf_module','workshop');$session->put('company_context.module_id','workshop');
$input=['reference'=>'OT-UNICA-'.Str::uuid(),'client_id'=>$client,'equipment_identifier'=>'EQ-'.Str::uuid(),'equipment_description'=>'Equipamento fictício','request'=>'Diagnóstico no mesmo SGF','location'=>'Unidade QA','evidence'=>'Pedido novo sintético, sem alterar pendentes'];
$workshop=app(WorkshopWorkflow::class);$blocked=false;try{$workshop->open($user->id,$input,(string)Str::uuid());}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){$blocked=$e->getStatusCode()===403;}
$ok($blocked,'cross-module client is refused before an explicit individual association');
ModuleRecords::associate('clients',$client,$user->id,['tenant_id'=>$f['tenant'],'company_id'=>$f['company'],'unit_id'=>null],'workshop','Decisão individual apenas sobre este cliente novo do ensaio.',false,1);
$key=(string)Str::uuid();$order=$workshop->open($user->id,$input,$key);$retry=$workshop->open($user->id,$input,$key);
$ok($order['id']===$retry['id']&&$retry['replayed'],'Oficinas opens once and recovers the same order without duplication');
$ok($db->table('wo_orders')->where('id',$order['id'])->value('client_id')===$client,'Oficinas retains the exact explicitly associated Comercial client');
foreach(['unit.view','workshop.view'] as $right){$grant=['tenant_id'=>$f['tenant'],'company_id'=>$f['company'],'unit_id'=>$f['foreign_unit'],'user_id'=>$user->id,'permission'=>$right];if(!$db->table('permission_grants')->where($grant)->exists())$db->table('permission_grants')->insert(['id'=>$id++]+$grant);}
$ctx->selectFor($user->id,$f['tenant'],$f['company'],$f['foreign_unit']);$foreign=$workshop->listing($user->id);$ok(!array_filter($foreign['orders'],fn($o)=>(int)$o['id']===$order['id']),'another authorized unit cannot see the new order');$ctx->selectFor($user->id,$f['tenant'],$f['company'],$f['unit']);
$destructiveBlocked=false;try{Event::dispatch(new Illuminate\Console\Events\CommandStarting('migrate:fresh',new Symfony\Component\Console\Input\ArrayInput([]),new Symfony\Component\Console\Output\NullOutput));}catch(RuntimeException $e){$destructiveBlocked=$e->getMessage()==='SGF_COMPOSITION_DESTRUCTIVE_COMMAND_REFUSED';}
$ok($destructiveBlocked,'destructive command is refused by the command-start guard without executing it');
$ok($db->table('si_invoices')->count()===0&&$db->table('sr_records')->count()===0,'new drafts and workshop order do not issue invoices or create financial effects automatically');
return ['synthetic_client'=>$client,'service_draft'=>$draft['id'],'billing_draft'=>$billing['id'],'workshop_order'=>$order['id'],'rollback'=>true];
});
$ok($snapshot($host)===$before,'all original table rows unchanged after rolling back the new composed flow');
$result=['passed'=>true,'checks'=>$checks,'schema'=>$result,'target8085_touched'=>false,'qa_sequences_may_advance'=>true,'previous_schema_proof_reused'=>'sgf-composition-proof-32c029d5b44c','new_database'=>false,'new_server'=>false];file_put_contents($proof.'/flow-result.json',json_encode($result,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$proof.'/flow-result.json']).PHP_EOL;