'public','source_tenant_id'=>$nf['tenant'],'warehouse_ids'=>[$nf['warehouse']],'append_only'=>true]; $seedPath=$s['root'].'/stock-inactive-fixtures.json'; if(!is_file($seedPath)){ $before=$native->table('stock_movements')->orderBy('id')->get()->toJson(); $seed=$native->transaction(function()use($native,$binding){ $wh=$binding['warehouse_ids'][0];$tenant=$binding['source_tenant_id'];$base=(array)$native->table('stock_items')->where('tenant_id',$tenant)->first();unset($base['id']);$actor=(int)$native->table('users')->where('tenant_id',$tenant)->value('id'); $item=function($code,$name)use($native,$base){return $native->table('stock_items')->insertGetId(array_replace($base,['code'=>$code,'name'=>$name,'unit'=>'UN']));}; $move=function($id,$at,$q='1000',$warehouse=null)use($native,$wh,$tenant,$actor){$native->table('stock_movements')->insert(['tenant_id'=>$tenant,'stock_item_id'=>$id,'warehouse_id'=>$warehouse??$wh,'moved_by'=>$actor,'movement_type'=>'qa_inactive_fixture','quantity_milli'=>$q,'moved_at'=>$at,'snapshot'=>json_encode(['reference'=>'QA-INACTIVE-METADATA-NOT-WORKFLOW'])]);}; $ids=[];for($i=0;$i<127;$i++){$id=$item('QA-INACT-'.str_pad((string)$i,3,'0',STR_PAD_LEFT),$i===0?'Literal%_! known history':'Inactivity fixture '.$i);$move($id,'2026-10-01 12:00:00');$ids[]=$id;} $boundary=$item('QA-INACT-BOUND','Thirty days');$move($boundary,'2026-10-11 23:59:59'); $hot=$item('QA-INACT-HOT','Twenty nine days');$move($hot,'2026-10-12 00:00:00'); $future=$item('QA-INACT-FUT','Future only');$move($future,'2026-11-11 00:00:00'); $never=$item('QA-INACT-NEVER','No physical history');$move($ids[0],'2026-11-11 00:00:00','9007199254740994'); return compact('ids','wh','tenant','actor','boundary','hot','future','never'); }); $original=json_decode($before,true);if($native->table('stock_movements')->whereIn('id',array_column($original,'id'))->orderBy('id')->get()->toJson()!==$before)throw new LogicException('Original movements changed'); file_put_contents($seedPath,json_encode($seed,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)); } $seed=json_decode(file_get_contents($seedPath),true);$read=$native; $checks=[];$ok=function($v,$n)use(&$checks){if(!$v)throw new RuntimeException($n);$checks[]=$n;}; $f=['as_of'=>'2026-11-10','days'=>30,'warehouse_id'=>null,'search'=>'QA-INACT-','per_page'=>25,'page'=>1]; $report=function($extra=[], $token=null,$fp=null)use($read,$binding,$f){$q=array_replace($f,$extra);$fp??=hash('sha256',json_encode(array_diff_key($q,['page'=>true])));return(new StockInactiveReport($read))->read($binding,$fp,$q,$token);}; $deny=function($fn,$status)use($ok){try{$fn();throw new RuntimeException('Expected refusal');}catch(Symfony\Component\HttpKernel\Exception\HttpException $e){$ok($e->getStatusCode()===$status,'refusal '.$status.' #'.count($GLOBALS['checks']));}}; $first=$report();$ok($first['pagination']['total']===128&&count($first['rows'])===25,'128 known cold pairs; SQL page25');$all=[];for($i=1;$i<=6;$i++)$all=array_merge($all,$report(['page'=>$i],$first['snapshot'])['rows']);$ok(count($all)===128&&count(array_unique(array_column($all,'stock_item_id')))===128,'six pages stable and no overlap'); $a=$report(['per_page'=>100]);$b=$report(['per_page'=>100,'page'=>2],$a['snapshot']);$ok(count($a['rows'])===100&&count($b['rows'])===28,'pages100 and28 same universe'); $row=array_values(array_filter($all,fn($r)=>$r['stock_item_id']===$seed['boundary']))[0];$ok($row['days_without_record']===30,'UTC last second at30-day boundary included'); $ok(!array_intersect([$seed['hot'],$seed['future'],$seed['never']],array_column($all,'stock_item_id')),'recent future-only and unknown-history excluded'); $literal=$report(['search'=>'%_!']);$ok(count($literal['rows'])===1&&$literal['rows'][0]['physical_quantity_milli']==='1000','literal search and future append excluded from physical quantity'); $future=$report(['as_of'=>'2026-12-11','search'=>'%_!']);$ok($future['rows'][0]['physical_quantity_milli']==='9007199254741994','exact numeric quantity beyond JS integer'); $read->enableQueryLog();$report();$queries=$read->getQueryLog();$ok(count(array_filter($queries,fn($q)=>str_starts_with(strtolower($q['query']),'select')))===5,'five projection SELECTs page25');$read->flushQueryLog();$report(['per_page'=>100]);$ok(count(array_filter($read->getQueryLog(),fn($q)=>str_starts_with(strtolower($q['query']),'select')))===5,'five projection SELECTs page100');$read->disableQueryLog(); $deny(fn()=>$report(['warehouse_id'=>PHP_INT_MAX]),403);$deny(fn()=>$report([], 'tampered'),409);$deny(fn()=>$report(['days'=>29],$first['snapshot']),409);$deny(fn()=>$report([],$first['snapshot'],'different-context'),409); $decoded=json_decode(Crypt::decryptString($first['snapshot']),true);$decoded['observed']=time()-601;$deny(fn()=>$report([],Crypt::encryptString(json_encode($decoded))),409); // Exercise the actual host permission/context/validation gate, not a duplicated policy. config(['receipt_qa.enabled'=>true,'receipt_qa.stock_read_enabled'=>true,'receipt_qa.stock_inactive_enabled'=>true]); $api=function($query,$actor=2,$unit=2)use($app){Auth::forgetGuards();Auth::shouldUse('web');$user=App\Models\User::findOrFail($actor);Auth::guard('web')->setUser($user);$session=app('session')->driver();$session->start();$session->put('company_context',['tenant_id'=>1,'company_id'=>2,'unit_id'=>$unit,'module_id'=>'stock','token'=>'inactive-proof']);$r=Illuminate\Http\Request::create('/api/stock-read','GET',$query);$r->setLaravelSession($session);$r->setUserResolver(fn()=>$user);$app->instance('request',$r);app(CompanyContext::class)->selectFor($actor,1,2,$unit);try{return app(CompositionStockRead::class)->index($r,app(CompanyContext::class),app(App\Modules\Identity\Services\Permissions::class))->getData(true);}finally{app(CompanyContext::class)->clear();}}; $q=['view'=>'inactive','as_of'=>'2026-11-10','days'=>30,'search'=>'QA-INACT-'];$deny(fn()=>$api($q,4),403);$deny(fn()=>$api($q,2,1),403); foreach([['days'=>0],['as_of'=>'2026-02-30'],['per_page'=>500],['days'=>null]]as$bad){try{$api(array_replace($q,$bad));throw new RuntimeException('Expected validation failure');}catch(Illuminate\Validation\ValidationException){$ok(true,'input validation #'.count($checks));}} $hash=fn()=>hash('sha256',$native->table('stock_movements')->orderBy('id')->get()->toJson());$before=$hash();$report();$ok($hash()===$before,'report has no physical writes'); // A single explicit revision fixture append, only once per regression proof. $appendMarker=$s['root'].'/stock-inactive-revision.json';if(!is_file($appendMarker)){$native->table('stock_movements')->insert(['tenant_id'=>$seed['tenant'],'stock_item_id'=>$seed['ids'][1],'warehouse_id'=>$seed['wh'],'moved_by'=>$seed['actor'],'movement_type'=>'qa_inactive_fixture','quantity_milli'=>'1','moved_at'=>'2026-11-12 00:00:00','snapshot'=>'{"reference":"QA-INACTIVE-REVISION"}']);file_put_contents($appendMarker,json_encode(['explicit_fixture_append'=>1]));$deny(fn()=>$report([],$first['snapshot']),409);$ok($report()['pagination']['total']===128,'fresh reference recovers after future append');} $out=['passed'=>true,'checks'=>$checks,'fixture_pairs'=>128,'sql_fixture_not_workflow'=>true,'selected_8085_writes'=>0,'new_rights'=>0,'new_demo'=>false,'visual_review'=>false];file_put_contents($work.'/stock-inactive-result.json',json_encode($out,JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/stock-inactive-result.json']).PHP_EOL;