Files
SGF/scripts/supplier-composition-proof.php
2026-11-04 09:37:20 +02:00

27 lines
12 KiB
PHP

<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
$s=json_decode(file_get_contents(getenv('RECEIPT_COMPOSITION_STATE')),true,64,JSON_THROW_ON_ERROR);$out=$s['root'].'/supplier-result.json';if(file_exists($out))throw new LogicException('Completed sourcing proof preserved');require $s['app'].'/vendor/autoload.php';$app=require $s['app'].'/bootstrap/app.php';$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();$f=json_decode(file_get_contents($s['root'].'/fixture.json'),true);$n=json_decode(file_get_contents($s['root'].'/native-fixture.json'),true);config(['database.connections.sourcing_verify'=>$s['nativeConnection']]);$db=DB::connection('sourcing_verify');$checks=[];$ok=function($v,$name)use(&$checks,$s){if(!$v)throw new RuntimeException($name);$checks[]=$name;file_put_contents($s['root'].'/supplier-progress.json',json_encode($checks,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));};$servers=[];$clients=[];$sessions=[];
try{
foreach([0,1]as$i){$socket=stream_socket_server('tcp://127.0.0.1:0');$port=(int)substr(strrchr(stream_socket_get_name($socket,false),':'),1);fclose($socket);$server=new Symfony\Component\Process\Process([PHP_BINARY,'-S','127.0.0.1:'.$port,'-t',$s['app'].'/public',dirname(__DIR__).'/scripts/receipt-composition-http.php'],$s['app'],null,null,180);$server->start();$servers[]=$server;for($j=0;$j<80;$j++){if($socket=@fsockopen('127.0.0.1',$port,$errno,$error,.1)){fclose($socket);break;}usleep(50000);}$clients[]=new GuzzleHttp\Client(['base_uri'=>'http://127.0.0.1:'.$port,'http_errors'=>false,'timeout'=>25]);}
$enter=function($role,$i=0,$query=[])use(&$sessions,$clients){$jar=new GuzzleHttp\Cookie\CookieJar;$r=$clients[$i]->get('/qa/enter/'.$role,['cookies'=>$jar,'query'=>$query]);$sessions[$role.$i]=['jar'=>$jar]+json_decode((string)$r->getBody(),true,32,JSON_THROW_ON_ERROR);};$options=function($role,$i=0,$data=null,$headers=[])use(&$sessions){$v=$sessions[$role.$i];$o=['cookies'=>$v['jar'],'headers'=>$headers+['Accept'=>'application/json','X-SGF-Context'=>$v['context'],'X-CSRF-TOKEN'=>$v['csrf']]];if($data!==null)$o['json']=$data;return $o;};$call=function($role,$path,$data=null,$i=0,$headers=[])use($clients,$options,$s){$r=$clients[$i]->request($data===null?'GET':'POST',$path,$options($role,$i,$data,$headers));$text=(string)$r->getBody();if($r->getStatusCode()>=500)file_put_contents($s['root'].'/supplier-last-500.txt',$text);return ['status'=>$r->getStatusCode(),'data'=>json_decode($text,true),'body'=>$text];};foreach(['actor','issuer','validator']as$role)$enter($role);$enter('actor',1);$base='/api/purchasing-suppliers';
$fingerprint=function()use($db){$h=hash_init('sha256');foreach(['suppliers','audit_logs','quotations','quotation_items','budget_entries']as$t)foreach($db->table($t)->orderBy('id')->get()as$row)hash_update($h,json_encode((array)$row));return hash_final($h);};
$preview=function($role,$id,$action,$data,$key=null)use($call,$base){$r=$call($role,$id?$base.'/'.$id:$base);if($r['status']!==200)throw new RuntimeException('Supplier state unavailable');$revision=$id?$r['data']['revision']:$r['data']['new_revision'];return $call($role,$base.'/'.$id.'/preview',['operation_key'=>$key??(string)Str::uuid(),'revision'=>$revision,'action'=>$action,'data'=>$data]);};$confirm=fn($role,$id,$cut)=>$call($role,$base.'/'.$id.'/confirm',['preview'=>$cut]);
$data=['name'=>'Fornecedor novo QA','nuit'=>'444555666','contact'=>'Contacto QA','email'=>'supplier@example.invalid','phone'=>'000','address'=>'Endereço sintético','category'=>'Material','beneficiaries'=>'Beneficiário sintético','document_references'=>'Referências exclusivamente sintéticas','diligence_notes'=>'Conferência documental sintética, sem operação real.','blocked'=>false,'next_review'=>date('Y-m-d',strtotime('+1 year'))];
$ok($call('actor','/purchasing/suppliers')['status']===200,'supplier workspace served');$r=$call('actor',$base);$ok($r['status']===200&&$r['data']['can_prepare']===true&&$r['data']['suppliers']['total']===3,'only three explicitly bound seed suppliers are listed');$ok($call('validator',$base)['status']===403,'warehouse cannot view supplier economic registry');$ok($call('actor',$base.'/'.$n['foreign_supplier'])['status']===404,'foreign tenant supplier is inaccessible');$enter('actor',1,['foreign'=>1]);$ok($call('actor',$base,null,1)['status']===403,'unmapped unit is refused');$enter('actor',1);$ok($call('actor',$base,null,0,['X-SGF-Context'=>'old'])['status']===409,'outdated company context is refused');$ok($call('actor',$base.'?status=invalid')['status']===422,'invalid status filter is rejected');
$before=$fingerprint();$key=(string)Str::uuid();$p=$preview('actor',0,'create',$data,$key);$ok($p['status']===200&&$p['data']['impact']['after']['status']==='pending'&&$p['data']['impact']['before']===null,'native create preview displays pending state without activation');$ok($fingerprint()===$before,'supplier preview commits neither supplier nor history');$cut=$p['data']['preview'];$other=$data;$other['name']='Outra ficha';$alternate=$preview('actor',0,'create',$other,$key)['data']['preview'];$ok($confirm('actor',0,$cut.'broken')['status']===409,'tampered preview is refused');
$db->unprepared("CREATE FUNCTION public.supplier_qa_fail() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN IF NEW.action='sgf.supplier.command' THEN RAISE EXCEPTION 'synthetic receipt failure'; END IF; RETURN NEW; END \$\$; CREATE TRIGGER supplier_qa_fail BEFORE INSERT ON audit_logs FOR EACH ROW EXECUTE FUNCTION public.supplier_qa_fail()");try{$ok($confirm('actor',0,$cut)['status']===500,'history receipt failure aborts supplier confirmation');$ok($fingerprint()===$before,'supplier and original audit roll back together');}finally{$db->unprepared('DROP TRIGGER supplier_qa_fail ON audit_logs; DROP FUNCTION public.supplier_qa_fail()');}
$responses=GuzzleHttp\Promise\Utils::unwrap([$clients[0]->postAsync($base.'/0/confirm',$options('actor',0,['preview'=>$cut])),$clients[1]->postAsync($base.'/0/confirm',$options('actor',1,['preview'=>$cut]))]);$statuses=array_map(fn($r)=>$r->getStatusCode(),$responses);sort($statuses);$ok($statuses===[200,201],'independent workers recover one supplier after failure');$id=json_decode((string)$responses[0]->getBody(),true)['supplier_id'];$ok($db->table('suppliers')->where('nuit',$data['nuit'])->count()===1&&$db->table('audit_logs')->where('action','sgf.supplier.command')->count()===1,'one supplier and one immutable command receipt');$ok($confirm('actor',0,$alternate)['status']===409,'same key with other contents is refused');
$r=$call('issuer',$base.'/'.$id);$ok($r['status']===200&&$r['data']['supplier']['status']==='pending'&&$r['data']['capabilities']['review']===true,'distinct finance identity can inspect newly prepared supplier without rebinding');$ok($preview('actor',$id,'review',['status'=>'active','comment'=>'Tentativa de auto revisão.','version'=>1])['status']===403,'preparer cannot review own supplier');
$url='/api/purchasing-sourcing/requests/'.$n['requisition'];$quote=['supplier_id'=>$id,'reference'=>'QA-NEW-SUPPLIER','delivery_days'=>5,'valid_until'=>date('Y-m-d',strtotime('+30 days')),'payment_terms'=>'QA30dias','no_conflict'=>true,'items'=>[['requisition_item_id'=>$n['line'],'unit_price'=>'100','discount_type'=>'none','discount_value'=>'0']]];$qp=function()use($call,$url,$quote){$r=$call('actor',$url);return $call('actor',$url.'/preview',['operation_key'=>(string)Str::uuid(),'revision'=>$r['data']['revision'],'action'=>'quote','data'=>$quote]);};$r=$call('actor',$url);$ok(!in_array($id,array_column($r['data']['suppliers'],'id'),true)&&$qp()['status']===422,'pending supplier is neither selectable nor accepted by native quotation rules');
$review=['status'=>'active','comment'=>'Revisão independente exclusivamente sintética.','version'=>1];$p=$preview('issuer',$id,'review',$review);$ok($p['status']===200&&$p['data']['impact']['before']['status']==='pending'&&$p['data']['impact']['after']['status']==='active','independent review displays before and after states');$reviewCut=$p['data']['preview'];$update=$data;$update['version']=1;$update['contact']='Contacto corrigido';$p=$preview('actor',$id,'update',$update);$r=$confirm('actor',$id,$p['data']['preview']);$ok($r['status']===201&&$r['data']['version']===2&&$r['data']['status']==='pending','native update increments version and retains pending review');$ok($confirm('issuer',$id,$reviewCut)['status']===409,'review prepared against old version is refused');
$review['version']=2;$p=$preview('issuer',$id,'review',$review);$grant=DB::table('permission_grants')->where($f['scope'])->where('user_id',$f['issuer'])->where('permission','purchasing_suppliers.review')->first();DB::table('permission_grants')->where('id',$grant->id)->delete();try{$ok($confirm('issuer',$id,$p['data']['preview'])['status']===403,'review right revalidated on confirmation');}finally{DB::table('permission_grants')->insert((array)$grant);}$r=$confirm('issuer',$id,$p['data']['preview']);$ok($r['status']===201&&$r['data']['status']==='active'&&$r['data']['version']===3,'finance activates exact current version');$ok($confirm('issuer',$id,$p['data']['preview'])['status']===200,'review repetition does not increment version twice');$r=$call('actor',$url);$ok(in_array($id,array_column($r['data']['suppliers'],'id'),true),'reviewed supplier automatically becomes eligible in the same scoped sourcing registry');$p=$qp();$ok($p['status']===200&&$p['data']['impact']['total_minor']===10000,'reviewed supplier reaches original quotation pricing');$quoteCut=$p['data']['preview'];
$review=['status'=>'suspended','comment'=>'Suspensão sintética antes da confirmação da proposta.','version'=>3];$p=$preview('issuer',$id,'review',$review);$ok($confirm('issuer',$id,$p['data']['preview'])['status']===201,'finance suspension is explicit and versioned');$ok($call('actor',$url.'/confirm',['preview'=>$quoteCut])['status']===422&&$db->table('quotations')->count()===0,'supplier suspension after preview is revalidated before quote commit');
$review['status']='active';$review['version']=4;$p=$preview('issuer',$id,'review',$review);$confirm('issuer',$id,$p['data']['preview']);$p=$qp();$r=$call('actor',$url.'/confirm',['preview'=>$p['data']['preview']]);$ok($r['status']===201&&$db->table('quotations')->where('supplier_id',$id)->count()===1,'new supplier reviewed in SGF supplies an actual native quotation');
$update['version']=5;$update['blocked']=true;$p=$preview('actor',$id,'update',$update);$confirm('actor',$id,$p['data']['preview']);$review['version']=6;$ok($preview('issuer',$id,'review',$review)['status']===422,'blocked supplier cannot be reactivated');$r=$call('issuer',$base.'/'.$id);$ok($r['data']['supplier']['status']==='pending'&&$r['data']['supplier']['reviewed_by']===null,'editing active supplier clears earlier reviewer and status');
for($i=0;$i<16;$i++){$d=$data;$d['name']='Página QA '.str_pad((string)$i,2,'0',STR_PAD_LEFT);$d['nuit']=(string)(700000000+$i);$p=$preview('actor',0,'create',$d);if($p['status']!==200||$confirm('actor',0,$p['data']['preview'])['status']!==201)throw new RuntimeException('Pagination fixture command failed');}
$p1=$call('actor',$base.'?search=P%C3%A1gina&status=pending&page=1');$p2=$call('actor',$base.'?search=P%C3%A1gina&status=pending&page=2');$ok($p1['data']['suppliers']['total']===16&&count($p1['data']['suppliers']['data'])===15&&count($p2['data']['suppliers']['data'])===1&&array_intersect(array_column($p1['data']['suppliers']['data'],'id'),array_column($p2['data']['suppliers']['data'],'id'))===[],'server filters and stable pagination produce 15 plus 1 disjoint rows');
$h1=$call('issuer',$base.'/'.$id.'/history?page=1');$h2=$call('issuer',$base.'/'.$id.'/history?page=2');$ok($h1['status']===200&&$h1['data']['total']===12&&count($h2['data']['data'])===0,'original and SGF history retain author date before and after without unrelated suppliers');$ok($db->table('purchase_orders')->count()===0&&$db->table('stock_movements')->count()===0&&(int)$db->table('budget_entries')->sum('committed_minor')===0,'supplier integration adds no order stock or budget commitment');
file_put_contents($out,json_encode(['passed'=>true,'checks'=>$checks,'concurrency_statuses'=>$statuses,'preview_no_persistent_changes'=>true,'atomic_failure_recovery'=>true,'new_supplier_reaches_native_quotation'=>true,'selected_supplier_history_count'=>$h1['data']['total'],'visual_review'=>false,'deployment_capacity_validated'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo count($checks)." supplier checks passed.\n";
}finally{foreach($servers as$i=>$server){$server->stop(2);file_put_contents($s['root'].'/supplier-http-'.$i.'.log',$server->getOutput()."\n".$server->getErrorOutput());}}