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

19 lines
10 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'].'/order-result.json';if(file_exists($out))throw new LogicException('Completed order 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.order_verify'=>$s['nativeConnection']]);$db=DB::connection('order_verify');$checks=[];$ok=function($v,$name)use(&$checks,$s){if(!$v)throw new RuntimeException($name);$checks[]=$name;file_put_contents($s['root'].'/order-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)use(&$sessions){$v=$sessions[$role.$i];$o=['cookies'=>$v['jar'],'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)use($clients,$options,$s){$r=$clients[$i]->request($data===null?'GET':'POST',$path,$options($role,$i,$data));$text=(string)$r->getBody();if($r->getStatusCode()>=500)file_put_contents($s['root'].'/order-last-500.txt',$text);return ['status'=>$r->getStatusCode(),'data'=>json_decode($text,true),'body'=>$text];};foreach(['actor','validator','issuer']as$role)$enter($role);$enter('actor',1);$base='/api/purchasing-orders';$url=$base.'/awards/'.$n['award'];
$ok($db->table('purchase_orders')->count()===0&&$db->table('stock_movements')->count()===0,'new composition starts before order issuance and receipt');$r=$call('actor',$base);$ok($r['status']===200&&count($r['data']['data'])===1,'SGF lists only the explicitly mapped approved award');$ok($call('actor','/purchasing/orders')['status']===200,'functional SGF order workspace is mounted');$d=$call('actor',$url);$ok($d['status']===200&&$d['data']['can_issue']===true&&$d['data']['existing']===null,'procurement profile can confer an unissued approved award');$ok($call('validator',$url)['status']===403,'warehouse profile cannot issue an order');$ok($call('actor',$base.'/awards/'.$n['foreign_award'])['status']===403,'another tenant award cannot enter the bound unit');$ok($call('validator',$url.'/preview',['revision'=>$d['data']['revision']])['status']===403,'read-only order right cannot prepare issuance');
$p=$call('actor',$url.'/preview',['revision'=>$d['data']['revision']]);$ok($p['status']===200&&!$p['data']['impact']['stock']&&!$p['data']['impact']['invoice']&&!$p['data']['impact']['payment'],'preview describes approved data without downstream effects');$preview=$p['data']['preview'];$ok($db->table('purchase_orders')->count()===0,'preview creates no order');$ok($call('actor',$url.'/issue',['preview'=>$preview.'x'])['status']===409,'tampered issuance preview is refused');
$supplier=$db->table('quotations')->where('id',$n['quote'])->value('supplier_id');$contact=$db->table('suppliers')->where('id',$supplier)->value('contact');$db->table('suppliers')->where('id',$supplier)->update(['contact'=>'Changed synthetic source']);try{$ok($call('actor',$url.'/issue',['preview'=>$preview])['status']===409,'changed agreed source requires a new conference');}finally{$db->table('suppliers')->where('id',$supplier)->update(['contact'=>$contact]);}
$d=$call('actor',$url);$p=$call('actor',$url.'/preview',['revision'=>$d['data']['revision']]);$preview=$p['data']['preview'];$grant=DB::table('permission_grants')->where($f['scope'])->where('user_id',$f['actor'])->where('permission','purchasing_orders.issue')->first();DB::table('permission_grants')->where('id',$grant->id)->delete();try{$ok($call('actor',$url.'/issue',['preview'=>$preview])['status']===403,'SGF issue permission is revalidated after preview');}finally{DB::table('permission_grants')->insert((array)$grant);}
$db->table('users')->where('id',$n['buyer'])->update(['purchasing_role'=>'finance']);try{$ok($call('actor',$url.'/issue',['preview'=>$preview])['status']===403,'native procurement eligibility is revalidated on issuance');}finally{$db->table('users')->where('id',$n['buyer'])->update(['purchasing_role'=>'procurement']);}
$db->table('users')->where('id',$n['head'])->update(['purchasing_role'=>'requester']);try{$ok($call('actor',$url.'/issue',['preview'=>$preview])['status']===422,'final approver must remain eligible in the original workflow');}finally{$db->table('users')->where('id',$n['head'])->update(['purchasing_role'=>'department_head']);}
$fingerprint=function()use($db){$h=hash_init('sha256');foreach(['purchase_orders','purchase_order_items','budget_entries','audit_logs']as$table){hash_update($h,$table);foreach($db->table($table)->orderBy('id')->get()as$row)hash_update($h,json_encode((array)$row,JSON_THROW_ON_ERROR));}return hash_final($h);};$before=$fingerprint();$db->unprepared("CREATE FUNCTION public.order_qa_fail() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN IF NEW.action='sgf.order.issued' THEN RAISE EXCEPTION 'synthetic order provenance failure'; END IF; RETURN NEW; END \$\$; CREATE TRIGGER order_qa_fail BEFORE INSERT ON audit_logs FOR EACH ROW EXECUTE FUNCTION public.order_qa_fail()");try{$ok($call('actor',$url.'/issue',['preview'=>$preview])['status']===500,'failure of SGF provenance prevents a successful issuance response');$ok($fingerprint()===$before,'order lines budget commitment and original audit roll back together');}finally{$db->unprepared('DROP TRIGGER order_qa_fail ON audit_logs; DROP FUNCTION public.order_qa_fail()');}
$responses=GuzzleHttp\Promise\Utils::unwrap([$clients[0]->postAsync($url.'/issue',$options('actor',0,['preview'=>$preview])),$clients[1]->postAsync($url.'/issue',$options('actor',1,['preview'=>$preview]))]);$statuses=array_map(fn($r)=>$r->getStatusCode(),$responses);sort($statuses);$ok($statuses===[200,201],'two independent workers recover one canonical issuance');$results=array_map(fn($r)=>json_decode((string)$r->getBody(),true),$responses);$order=$results[0]['order'];$ok($order['id']===$results[1]['order']['id']&&$db->table('purchase_orders')->count()===1,'concurrent requests never create a second order');$ok($db->table('budget_entries')->where('tenant_id',$n['tenant'])->where('kind','commit')->count()===1&&(int)$db->table('budget_entries')->where('tenant_id',$n['tenant'])->sum('committed_minor')===10000,'original common budget records exactly one100MZN commitment');$ok($db->table('audit_logs')->where('action','purchase_order.issued')->count()===1&&$db->table('audit_logs')->where('action','sgf.order.issued')->count()===1,'original issuance and SGF provenance are each recorded once');$ok($db->table('goods_receipts')->count()===0&&$db->table('stock_movements')->count()===0&&$db->table('invoices')->count()===0,'order issuance does not receive stock or create an invoice');$r=$call('actor',$url);$ok($r['data']['existing']['order']['id']===$order['id']&&$r['data']['can_issue']===false,'issued original document replaces the issue action');
$receiptBase='/api/purchasing-receipts';$r=$call('validator',$receiptBase);$ok(count($r['data']['data'])===1&&$r['data']['data'][0]['id']===$order['id'],'order becomes visible to warehouse through explicit source requisition binding');$r=$call('validator',$receiptBase.'/orders/'.$order['id']);$ok($r['data']['can_confirm']===true&&$call('actor',$receiptBase.'/orders/'.$order['id'])['data']['can_confirm']===false,'issuing and receiving functions remain distinct');$line=$order['items'][0];$payload=['revision'=>$r['data']['revision'],'idempotency_key'=>(string)Str::uuid(),'delivery_note'=>'SGF-ORDER-RECEIPT','received_on'=>date('Y-m-d'),'warehouse_id'=>$n['warehouse'],'items'=>[['purchase_order_item_id'=>$line['id'],'received_quantity'=>'1.000','accepted_quantity'=>'1.000','quality_status'=>'compliant','stock_item_id'=>$n['stock'],'notes'=>null]]];$p=$call('validator',$receiptBase.'/orders/'.$order['id'].'/preview',$payload);$r=$call('validator',$receiptBase.'/orders/'.$order['id'].'/confirm',['preview'=>$p['data']['preview']]);$ok($r['status']===201&&count($r['data']['movements'])===1&&(int)$r['data']['movements'][0]['quantity_milli']===1000,'SGF-issued order receives once through the original receipt workflow');$history=$call('validator',$receiptBase.'/history/'.$r['data']['receipt']['id']);$ok($history['status']===200&&$history['data']['receipt']['purchase_order_id']===$order['id'],'receipt history traces back to the actually issued SGF order');$r=$call('validator',$receiptBase.'/orders/'.$order['id']);$ok($r['data']['progress']['state']==='complete','complete approved order-to-receipt path reaches completion');$ok($db->table('invoices')->count()===0&&$db->table('budget_consumptions')->count()===0,'receipt creates neither supplier invoice nor budget consumption');
file_put_contents($out,json_encode(['passed'=>true,'checks'=>$checks,'orders'=>1,'commit_minor'=>10000,'receipts'=>1,'movements'=>1,'physical_milli'=>1000,'order_to_receipt_complete'=>true,'original_workflows'=>true,'concurrency_statuses'=>$statuses,'visual_review'=>false,'deployment_capacity_validated'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo count($checks)." order-to-receipt checks passed.\n";
}finally{foreach($servers as$i=>$server){$server->stop(2);file_put_contents($s['root'].'/order-http-'.$i.'.log',$server->getOutput()."\n".$server->getErrorOutput());}}