20 lines
7.3 KiB
PHP
20 lines
7.3 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'].'/extra.json';if(file_exists($out))throw new LogicException('Completed extra 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,64,JSON_THROW_ON_ERROR);$n=json_decode(file_get_contents($s['root'].'/native-extra-fixture.json'),true,64,JSON_THROW_ON_ERROR);$bindingFile=$s['root'].'/binding.json';$original=file_get_contents($bindingFile);$binding=json_decode($original,true,64,JSON_THROW_ON_ERROR);$binding['bindings'][implode(':',array_values($f['scope'])).':'.$f['actor']]=['tenant_id'=>$n['tenant'],'actor_id'=>$n['receiver'],'order_ids'=>[$n['order']],'warehouse_ids'=>[$n['warehouse']]];file_put_contents($bindingFile,json_encode($binding,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
|
config(['database.connections.receipt_verify'=>$s['nativeConnection']]);$db=DB::connection('receipt_verify');$checks=[];$ok=function($v,$name)use(&$checks,$s){if(!$v)throw new RuntimeException($name);$checks[]=$name;file_put_contents($s['root'].'/extra-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);}$client=new GuzzleHttp\Client(['base_uri'=>'http://127.0.0.1:'.$port,'http_errors'=>false,'timeout'=>30]);$jar=new GuzzleHttp\Cookie\CookieJar;$r=$client->get('/qa/enter/actor',['cookies'=>$jar]);$sessions[$i]=['jar'=>$jar]+json_decode((string)$r->getBody(),true,32,JSON_THROW_ON_ERROR);$clients[]=$client;}
|
|
$options=function($i,$data=null)use(&$sessions){$session=$sessions[$i];$o=['cookies'=>$session['jar'],'headers'=>['Accept'=>'application/json','X-CSRF-TOKEN'=>$session['csrf'],'X-SGF-Context'=>$session['context']]];if($data!==null)$o['json']=$data;return $o;};$call=function($path,$data=null,$i=0)use($clients,$options){$r=$clients[$i]->request($data===null?'GET':'POST',$path,$options($i,$data));return ['status'=>$r->getStatusCode(),'data'=>json_decode((string)$r->getBody(),true)];};$url='/api/purchasing-receipts/orders/'.$n['order'];
|
|
$intent=function($reference)use($call,$url,$n){$d=$call($url);if($d['status']!==200)throw new RuntimeException('Extra order unavailable');$data=['revision'=>$d['data']['revision'],'idempotency_key'=>(string)Str::uuid(),'delivery_note'=>$reference,'received_on'=>date('Y-m-d'),'warehouse_id'=>$n['warehouse'],'items'=>[['purchase_order_item_id'=>$n['line'],'received_quantity'=>'0.500','accepted_quantity'=>'0.500','quality_status'=>'compliant','stock_item_id'=>$n['stock'],'notes'=>null]]];$r=$call($url.'/preview',$data);if($r['status']!==200)throw new RuntimeException('Extra preview failed');return $r['data']['preview'];};
|
|
$preview=$intent('SGF-QA-CONCURRENT-SAME');$promises=[];foreach([0,1]as$i)$promises[]=$clients[$i]->postAsync($url.'/confirm',$options($i,['preview'=>$preview]));$responses=GuzzleHttp\Promise\Utils::unwrap($promises);$statuses=array_map(fn($r)=>$r->getStatusCode(),$responses);sort($statuses);$ok($statuses===[200,201],'two independent HTTP workers recover the same key once');$receipts=$db->table('goods_receipts')->where('tenant_id',$n['tenant'])->count();$ok($receipts===1&&(int)$db->table('stock_movements')->where('tenant_id',$n['tenant'])->sum('quantity_milli')===500,'concurrent repetition has one canonical receipt and movement');
|
|
$a=$intent('SGF-QA-CONCURRENT-A');$b=$intent('SGF-QA-CONCURRENT-B');$responses=GuzzleHttp\Promise\Utils::unwrap([$clients[0]->postAsync($url.'/confirm',$options(0,['preview'=>$a])),$clients[1]->postAsync($url.'/confirm',$options(1,['preview'=>$b]))]);$statuses=array_map(fn($r)=>$r->getStatusCode(),$responses);sort($statuses);$ok($statuses===[201,409],'different intentions on one revision serialize and stale intention is refused');$ok($db->table('goods_receipts')->where('tenant_id',$n['tenant'])->count()===2&&(int)$db->table('stock_movements')->where('tenant_id',$n['tenant'])->sum('quantity_milli')===1000,'competing decisions do not duplicate accepted physical quantity');
|
|
$preview=$intent('SGF-QA-FAIL-RECOVER');$fingerprint=function()use($db){$h=hash_init('sha256');foreach(['goods_receipts','goods_receipt_items','stock_movements','audit_logs','replenishment_work']as$table){if(!$db->getSchemaBuilder()->hasTable($table))continue;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.receipt_qa_fail() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN RAISE EXCEPTION 'synthetic receipt writer failure'; END \$\$; CREATE TRIGGER receipt_qa_fail AFTER INSERT ON stock_movements FOR EACH ROW EXECUTE FUNCTION public.receipt_qa_fail()");
|
|
try{$r=$call($url.'/confirm',['preview'=>$preview]);$ok($r['status']===500,'injected failure after physical insert returns an unsuccessful response');$ok($fingerprint()===$before,'native transaction rolls back parent, items, movement, audit and work signal together');}finally{$db->unprepared('DROP TRIGGER receipt_qa_fail ON stock_movements; DROP FUNCTION public.receipt_qa_fail()');}
|
|
$r=$call($url.'/confirm',['preview'=>$preview]);$ok($r['status']===201,'same prepared intention succeeds after the cause is removed');$again=$call($url.'/confirm',['preview'=>$preview]);$ok($again['status']===200&&$again['data']['receipt']['id']===$r['data']['receipt']['id'],'recovery repetition returns the same immutable receipt');$ok($db->table('goods_receipts')->where('tenant_id',$n['tenant'])->count()===3&&(int)$db->table('stock_movements')->where('tenant_id',$n['tenant'])->sum('quantity_milli')===1500,'recovery leaves exactly three half-unit movements');
|
|
$reader=$clients[0]->get('/qa/enter/validator',['cookies'=>$sessions[0]['jar']]);$sessions[0]=['jar'=>$sessions[0]['jar']]+json_decode((string)$reader->getBody(),true);$r=$call('/api/purchasing-receipts/orders/'.json_decode(file_get_contents($s['root'].'/native-fixture.json'),true)['order']);$ok($r['status']===200&&$r['data']['can_confirm']===false,'read-only SGF profile receives no confirmation capability');
|
|
file_put_contents($out,json_encode(['passed'=>true,'checks'=>$checks,'two_independent_http_workers'=>true,'same_key_statuses'=>[200,201],'competing_key_statuses'=>[201,409],'fault_after_insert'=>true,'rollback_fingerprint_equal'=>true,'recovery_receipts'=>3,'recovery_quantity_milli'=>1500,'host_profile_changes_real'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo count($checks)." concurrency/recovery checks passed.\n";
|
|
}finally{file_put_contents($bindingFile,$original);foreach($servers as$i=>$server){$server->stop(2);file_put_contents($s['root'].'/extra-http-'.$i.'.log',$server->getOutput()."\n".$server->getErrorOutput());}}
|