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

36 lines
5.2 KiB
PHP

<?php
// No command, grant change or new business fixture: reuse the completed receipt book.
$fingerprint=function($connection){$result=[];foreach($connection->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename")as$table){if(!preg_match('/^[a-z0-9_]+$/D',$table->tablename))throw new LogicException('Unexpected fixture table');$name=$table->tablename;$row=$connection->selectOne('SELECT count(*) n,md5(coalesce(string_agg(row_to_json(t)::text,\'\' ORDER BY row_to_json(t)::text),\'\')) h FROM "'.$name.'" t');$result[$name]=[(int)$row->n,$row->h];}$result['sequences']=$connection->select("SELECT sequencename,last_value FROM pg_sequences WHERE schemaname='public' ORDER BY sequencename");return $result;};
$before=['host'=>$fingerprint(DB::connection()),'native'=>$fingerprint($db)];
$movement=$db->table('stock_movements')->where('tenant_id',$n['tenant'])->sole();$line=$db->table('goods_receipt_items')->where('id',$movement->goods_receipt_item_id)->sole();$receipt=$db->table('goods_receipts')->where('id',$line->goods_receipt_id)->sole();
$url='/api/purchasing-receipts/movements/'.$movement->id.'/origin';
$resume=getenv('STOCK_TRACE_PRESERVATION_ONLY')==='true';
if($resume){$checks=json_decode(file_get_contents($s['root'].'/stock-trace-read-progress.json'),true,32,JSON_THROW_ON_ERROR);if(count($checks)!==14)throw new LogicException('Exact valid trace prefix required');}
if(!$resume){
$enter('validator',0,['module'=>'stock']);$r=$call('validator','/api/stock-read?view=movements');
$ok($r['status']===200&&$r['data']['capabilities']['receipt_trace']===true&&$r['data']['rows'][0]['id']===$movement->id,'Stock offers an explicit receipt handoff for the actual existing movement');
$ok(in_array($call('validator',$url)['status'],[403,409],true),'receipt resolver requires explicit Purchasing module selection');
$enter('validator');$r=$call('validator',$url);$resolved=$r['data'];
$ok($r['status']===200&&array_keys($resolved)===['movement_id','order_id','receipt_id','href','read_only'],'resolver returns only canonical ids and a local read destination');
$ok($resolved['movement_id']===$movement->id&&$resolved['receipt_id']===$receipt->id&&$resolved['order_id']===$receipt->purchase_order_id&&$resolved['read_only'],'resolved movement receipt and parent agree with the actual native book');
$expected='/purchasing/receipts?order='.$receipt->purchase_order_id.'&receipt='.$receipt->id;
$ok($resolved['href']===$expected,'destination uses the actual receipt and order rather than browser supplied parents');
$r=$call('validator',$expected);$ok($r['status']===200&&str_contains($r['body'],'data-focus-enabled="true"'),'existing focused receipt workspace is mounted at the exact resolved destination');
$r=$call('validator','/api/purchasing-receipts/history/'.$receipt->id);
$ok($r['status']===200&&$r['data']['receipt']['purchase_order_id']===$receipt->purchase_order_id&&$r['data']['movements'][0]['id']===$movement->id,'existing evidence API correlates the same receipt and physical movement');
$ok($call('actor',$url)['status']===403,'Purchasing receipt access alone does not grant Stock-origin tracing');
$ok($call('reviewer',$url)['status']===403,'invoice reviewer cannot inherit physical receipt trace authority');
$ok($call('validator',$url.'?order=999999')['status']===422&&$call('validator',$url.'?receipt=999999')['status']===422,'caller cannot replace the parent or receipt through query parameters');
$ok($call('validator','/api/purchasing-receipts/movements/999999/origin')['status']===403,'unknown or unmapped movement has no inferred origin');
$enter('validator',1,['foreign'=>1]);$ok($call('validator',$url,null,1)['status']===403,'another unit cannot reuse the physical origin mapping');
$ok($call('validator',$url,null,0,['X-SGF-Context'=>'obsolete'])['status']===409,'old context token is rejected at the destination');
config(['receipt_qa.stock_trace_enabled'=>false]);$status=0;try{(new App\Modules\Purchasing\StockReceiptTraceController)->show($movement->id,request(),new App\Modules\Purchasing\ReceiptRuntime,app(App\Shared\Context\CompanyContext::class));}catch(Symfony\Component\HttpKernel\Exception\HttpException $e){$status=$e->getStatusCode();}finally{config(['receipt_qa.stock_trace_enabled'=>true]);}
$ok($status===404,'disabled trace refuses before reading source or context');
}else{
$enter('validator',0,['module'=>'stock']);$r=$call('validator','/api/stock-read?view=movements');
$enter('validator');$origin=$call('validator',$url);
$ok($r['status']===200&&$origin['status']===200&&$r['data']['rows'][0]['id']===$origin['data']['movement_id'],'preservation suffix observes the same Stock movement and current receipt origin without command replay');
}
$after=['host'=>$fingerprint(DB::connection()),'native'=>$fingerprint($db)];$ok(json_encode($before,JSON_THROW_ON_ERROR)===json_encode($after,JSON_THROW_ON_ERROR),'all reused host/native tables and sequences remain unchanged');
file_put_contents($out,json_encode(['passed'=>true,'checks'=>$checks,'valid_prefix_reused'=>$resume?14:0,'source_receipt'=>1,'source_movement'=>1,'before'=>$before,'after'=>$after,'read_only_overlay'=>true,'business_commands'=>0,'direct_guard_probes'=>1,'visual_review'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo count($checks)." read-only stock trace checks passed.\n";