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

61 lines
8.9 KiB
PHP

<?php
if(!($s['stockComposition']??false))throw new LogicException('Stock composition fixture required');
$origin=$s['root'].'/composition-stock-origin.json';
if(is_file($origin)){$order=json_decode(file_get_contents($origin),true,32,JSON_THROW_ON_ERROR);}
elseif($db->table('purchase_orders')->where('tenant_id',$n['tenant'])->count()===1&&is_file($s['root'].'/composition-stock-progress.json')){
// Recover the completed source setup after an assertion failure; do not issue another order.
$id=$db->table('purchase_orders')->where('tenant_id',$n['tenant'])->value('id');
$order=['id'=>$id,'items'=>[['id'=>$db->table('purchase_order_items')->where('purchase_order_id',$id)->value('id')]]];
file_put_contents($origin,json_encode($order,JSON_THROW_ON_ERROR));
}else{require __DIR__.'/receipt-workspace-source.php';file_put_contents($origin,json_encode($order,JSON_THROW_ON_ERROR));}
$enter('validator',0,['module'=>'stock']);$stock='/api/stock-read';
$read=fn($query=[])=>$call('validator',$stock.'?'.http_build_query($query));
$r=$read();$ok($r['status']===200&&$r['data']['rows']===[]&&$r['data']['status']==='available','composed stock starts with a genuine empty physical book');$before=$r['data']['snapshot'];
$ok(in_array($call('actor',$stock)['status'],[403,409],true),'purchasing authority does not grant stock access');
$enter('validator',1);$ok(in_array($call('validator',$stock,null,1)['status'],[403,409],true),'stock endpoint refuses Purchasing module context');
$enter('validator',1,['module'=>'stock','foreign'=>1]);$ok($call('validator',$stock,null,1)['status']===403,'another unit cannot inherit the source binding');
$ok($read(['warehouse_id'=>$n['foreign_warehouse']])['status']===403,'foreign warehouse explicitly refused');
$ok($read(['per_page'=>101])['status']===422&&$read(['per_page'=>1])['status']===422&&$read(['unknown'=>1])['status']===422,'page bounds and unknown filters rejected');
$enter('validator',1,['module'=>'stock']);
$r=$call('validator',$stock,null,1,['X-SGF-Context'=>'stale']);$ok($r['status']===409,'stale HTTP context refused before source observation');
// Catalogue volume is fixture metadata only, not artificial physical movements.
$base=(array)$db->table('stock_items')->where('id',$n['stock'])->first();unset($base['id']);
$db->transaction(function()use($db,$base){for($i=1;$i<=102;$i++){$row=$base;$row['code']='QA-COMPOSED-'.str_pad((string)$i,3,'0',STR_PAD_LEFT);$row['name']=$i===102?'Literal%_marker':'Composed QA article '.$i;$db->table('stock_items')->insert($row);}});
$r=$read(['view'=>'items','per_page'=>100]);$r2=$read(['view'=>'items','per_page'=>100,'page'=>2,'snapshot'=>$r['data']['snapshot']]);
$total=$db->table('stock_items')->where('tenant_id',$n['tenant'])->count();
$ok($r['status']===200&&$r2['status']===200&&count($r['data']['rows'])===100&&count($r2['data']['rows'])===$total-100,'catalogue uses bounded pages100 and remainder');
$ids=[];for($page=1;$page<=ceil($total/25);$page++){$r=$read(['view'=>'items','page'=>$page]);$ids=array_merge($ids,array_column($r['data']['rows'],'id'));}
$ok(count($ids)===$total&&count(array_unique($ids))===$total&&!in_array($n['foreign_stock'],$ids,true),'pages25 cover the same tenant catalogue once without foreign articles');
$r=$read(['view'=>'items','search'=>'%_']);$ok($r['status']===200&&count($r['data']['rows'])===1&&$r['data']['rows'][0]['name']==='Literal%_marker','search preserves literal wildcard semantics');
$r=$read(['view'=>'warehouses']);$ok($r['status']===200&&array_column($r['data']['rows'],'id')===[$n['warehouse']],'only explicitly mapped warehouse is visible');
$r=$read();$before=$r['data']['snapshot'];
$id=$order['id'];$line=$order['items'][0]['id'];$url='/api/purchasing-receipts/orders/'.$id;
$enter('validator',1);$r=$call('validator',$url,null,1);
$payload=['revision'=>$r['data']['revision'],'idempotency_key'=>(string)Str::uuid(),'delivery_note'=>'QA-COMPOSED-STOCK','received_on'=>date('Y-m-d'),'warehouse_id'=>$n['warehouse'],
'items'=>[['purchase_order_item_id'=>$line,'received_quantity'=>'1.000','accepted_quantity'=>'1.000','quality_status'=>'compliant','stock_item_id'=>$n['stock'],'notes'=>null]]];
$p=$call('validator',$url.'/preview',$payload,1);$ok($p['status']===200&&$db->table('stock_movements')->count()===0,'original receipt preview has no physical effect');
$r=$call('validator',$url.'/confirm',['preview'=>$p['data']['preview']],1);$ok($r['status']===201&&count($r['data']['movements'])===1,'original receipt confirmation creates exactly one native movement');$movement=$r['data']['movements'][0]['id'];
$r=$call('validator',$url.'/confirm',['preview'=>$p['data']['preview']],1);$ok($r['status']===200&&$r['data']['replayed']&&$db->table('stock_movements')->count()===1,'receipt replay remains idempotent across the integration');
$ok($read(['snapshot'=>$before])['status']===409,'new native movement invalidates an earlier stock snapshot');
$r=$read(['view'=>'movements']);$ok($r['status']===200&&count($r['data']['rows'])===1&&$r['data']['rows'][0]['id']===$movement&&$r['data']['rows'][0]['quantity_milli']==='1000','Stock reads the exact canonical receipt movement and rational quantity');
$ok(!array_intersect(['snapshot','cost','unit_price','supplier_id'],array_keys($r['data']['rows'][0])),'stock projection exposes no raw document or financial snapshot');
$r=$read();$ok($r['data']['rows'][0]['physical_milli']==='1000'&&$r['data']['rows'][0]['available_milli']===null&&!$r['data']['capabilities']['write'],'Stock derives physical balance without inventing availability or another writer');$token=$r['data']['snapshot'];
$enter('validator',0,['module'=>'stock']);$ok($read(['snapshot'=>$token])['status']===409,'renewed session context invalidates prior pagination token');
$grant=DB::table('permission_grants')->where($f['scope']+['user_id'=>$f['validator'],'permission'=>'stock.view'])->first();
DB::table('permission_grants')->where('id',$grant->id)->delete();try{$ok($read()['status']===403,'revoked stock grant is rechecked on every page');}finally{DB::table('permission_grants')->insert((array)$grant);}
$profile=$s['root'].'/stock-binding.json';$original=file_get_contents($profile);$binding=json_decode($original,true);$key=array_key_first($binding['bindings']);
$binding['bindings'][$key]['warehouse_ids']=[$n['foreign_warehouse']];file_put_contents($profile,json_encode($binding));
try{$r=$read();$ok($r['status']===503,'incompatible tenant warehouse mapping returns unavailable rather than a zero balance');}finally{file_put_contents($profile,$original);}
$ok($read()['status']===200,'valid private mapping recovers without changing the book');
$binding=json_decode($original,true);$binding['connection']['password']='invalid-qa-only';file_put_contents($profile,json_encode($binding));
try{$r=$read();$ok($r['status']===503&&!str_contains($r['body'],'stock_receipt_read_')&&!str_contains($r['body'],'SQLSTATE'),'source authentication failure is generic and does not expose credentials');}finally{file_put_contents($profile,$original);}
$ok($read()['status']===200,'source authentication recovers with original profile');
config(['database.connections.composition_security'=>json_decode($original,true)['connection']]);$reader=DB::connection('composition_security');
foreach(['DELETE FROM stock_movements WHERE false','SELECT * FROM goods_receipts LIMIT 0','SELECT * FROM audit_logs LIMIT 0','SELECT * FROM pa_obligations LIMIT 0']as$sql){$refused=false;try{$reader->statement($sql);}catch(\PDOException $e){$refused=in_array($e->errorInfo[0]??'',['42501','25006'],true);}$ok($refused,'technical reader refuses '.strtok($sql,' ').' outside its capability: '.explode(' ',$sql)[2]);}
$refused=false;try{$db->table('stock_movements')->where('id',$movement)->update(['quantity_milli'=>1001]);}catch(\PDOException $e){$refused=($e->errorInfo[0]??'')==='P0001';}
$ok($refused&&(int)$db->table('stock_movements')->sum('quantity_milli')===1000,'new QA append-only trigger preserves revision assumptions');
// An outstanding native writer mutex must not block the observational connection.
$db->beginTransaction();try{$db->table('tenants')->where('id',$n['tenant'])->lockForUpdate()->first();$ok($read()['status']===200,'Stock read does not acquire the native tenant writer mutex');}finally{$db->rollBack();}
$ok($db->table('goods_receipts')->count()===1&&$db->table('stock_movements')->count()===1&&$db->table('invoices')->where('tenant_id',$n['tenant'])->count()===0&&$db->table('pa_obligations')->where('tenant_id',$n['tenant'])->count()===0&&$db->table('external_payments')->where('tenant_id',$n['tenant'])->count()===0,'integration leaves one receipt/movement and creates no invoice debt or payment');
file_put_contents($out,json_encode(['passed'=>true,'checks'=>$checks,'same_native_book'=>true,'receipts'=>1,'movements'=>1,'physical_milli'=>1000,'catalogue_rows'=>$total,'page_sizes'=>[25,100],'new_permission_names'=>[],'visual_review'=>false,'deployment_capacity_validated'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo count($checks)." composed stock checks passed.\n";