(string)Str::uuid(),'revision'=>$id?$r['data']['revision']:$r['data']['new_revision'],'action'=>$action,'data'=>$data]);if($p['status']!==200)throw new RuntimeException('Request setup preview');return $call($role,$base.'/'.$id.'/confirm',['preview'=>$p['data']['preview']]);}; $r=$command('requester',0,'save',['cost_center_id'=>$n['centre'],'priority'=>'normal','needed_by'=>date('Y-m-d',strtotime('+30 days')),'justification'=>'Necessidade QA para seleccao integral de fornecedores.','items'=>[['description'=>'Material selector QA','category'=>'Material','purchase_type'=>'consumable','quantity'=>'1.000','unit'=>'UN','unit_price'=>'100.00','tax_rate'=>'0']]]);$id=$r['data']['request_id'];$command('requester',$id,'submit',[]);$r=$command('approver',$id,'decide',['decision'=>'approved','comment'=>'Conferencia distinta da necessidade de ensaio.']);$ok($r['status']===201&&$r['data']['status']==='approved','selector source uses genuinely prepared and approved request'); $base='/api/purchasing-sourcing/requests/'.$id;$url=$base.'/suppliers';$all=[];for($page=1;$page<=5;$page++){$r=$call('actor',$url.'?page='.$page);$ok($r['status']===200&&$r['data']['total']===125&&$r['data']['per_page']===25&&count($r['data']['data'])===25,'server supplier page'.$page.' contains authorised bounded window');$all=array_merge($all,$r['data']['data']);} $ids=array_column($all,'id');$ok(count(array_unique($ids))===125&&in_array($seed['ids'][110],$ids,true),'all125 suppliers reachable beyond previous100 cap without overlap'); $ok(in_array($seed['ids'][125],$ids,true)&&!in_array($seed['ids'][124],$ids,true)&&!in_array($seed['ids'][126],$ids,true),'explicit and current-unit audited suppliers admitted; unbound and another-unit origins excluded'); $ok(!array_intersect([$seed['ids'][121],$seed['ids'][122],$seed['ids'][123],$n['foreign_supplier']],$ids),'blocked suspended overdue and foreign-tenant suppliers excluded'); $r=$call('actor',$url.'?per_page=100');$r2=$call('actor',$url.'?per_page=100&page=2');$ok(count($r['data']['data'])===100&&count($r2['data']['data'])===25,'pages100 and25 cover same eligible universe'); $r=$call('actor',$url.'?search=900000110');$selected=$r['data']['data'][0];$ok($r['status']===200&&$r['data']['total']===1&&$selected['id']===$seed['ids'][110]&&strlen($selected['selection_revision'])===64,'NUIT search reaches supplier beyond old cap with exact selection revision'); $ok(array_keys($selected)===['id','name','nuit','version','selection_revision'],'selector response exposes no contact bank or diligence details'); $r=$call('actor',$url.'?search='.rawurlencode('%_'));$ok($r['status']===200&&$r['data']['total']===1&&$r['data']['data'][0]['id']===$seed['ids'][127],'percent and underscore search are literal rather than wildcards'); $r=$call('actor',$url.'?search=qa%20catalog0110');$ok($r['status']===200&&$r['data']['total']===1,'name search ignores case without broadening scope'); $r=$call('actor',$url.'?search=not-found');$ok($r['status']===200&&$r['data']['data']===[],'empty search returns no invented alternative selection'); $ok($call('actor',$url.'?per_page=500')['status']===422&&$call('actor',$url.'?page=0')['status']===422&&$call('actor',$url.'?unknown=1')['status']===422,'invalid pages sizes and unsupported filters rejected'); $ok($call('issuer',$url)['status']===403&&$call('validator',$url)['status']===403&&$call('requester',$url)['status']===403,'browse requires existing scoped sourcing preparation right'); $ok($call('actor','/api/purchasing-sourcing/requests/'.$n['foreign_requisition'].'/suppliers')['status']===403,'foreign request cannot be used as catalogue entry point'); $line=(int)$db->table('requisition_items')->where('requisition_id',$id)->value('id');$data=['supplier_id'=>$selected['id'],'supplier_revision'=>$selected['selection_revision'],'reference'=>'QA-SELECTOR-110','delivery_days'=>5,'valid_until'=>date('Y-m-d',strtotime('+30 days')),'payment_terms'=>'Condicoes QA','no_conflict'=>true,'items'=>[['requisition_item_id'=>$line,'unit_price'=>'100.00','discount_type'=>'none','discount_value'=>'0']]]; $preview=function($data,$key=null)use($call,$base){$r=$call('actor',$base);return $call('actor',$base.'/preview',['operation_key'=>$key??(string)Str::uuid(),'revision'=>$r['data']['revision'],'action'=>'quote','data'=>$data]);};$confirm=fn($cut,$i=0)=>$call('actor',$base.'/confirm',['preview'=>$cut],$i); $bad=$data;$bad['supplier_revision']=str_repeat('0',64);$ok($preview($bad)['status']===409,'stale or substituted selection revision requires new lookup');$bad=$data;$bad['supplier_id']=$n['foreign_supplier'];unset($bad['supplier_revision']);$ok($preview($bad)['status']===404,'forged foreign supplier id cannot bypass selector'); $bad=$data;$bad['supplier_id']=$seed['ids'][121];unset($bad['supplier_revision']);$ok($preview($bad)['status']===422,'forged blocked id remains refused by original native workflow'); $before=hash('sha256',json_encode([$db->table('quotations')->get(),$db->table('quotation_items')->get(),$db->table('audit_logs')->orderBy('id')->get()]));$p=$preview($data);$ok($p['status']===200&&$p['data']['impact']['total_minor']===10000,'supplier beyond100 receives original pricing and impact preview');$ok($db->table('quotations')->count()===0&&hash('sha256',json_encode([$db->table('quotations')->get(),$db->table('quotation_items')->get(),$db->table('audit_logs')->orderBy('id')->get()]))===$before,'selector browsing and quotation preview persist no business decision'); $version=$db->table('suppliers')->where('id',$selected['id'])->value('version');$db->table('suppliers')->where('id',$selected['id'])->update(['version'=>$version+1]);try{$ok($confirm($p['data']['preview'])['status']===409&&$db->table('quotations')->count()===0,'supplier change after preview refuses confirmation without quote');}finally{$db->table('suppliers')->where('id',$selected['id'])->update(['version'=>$version]);} $db->table('suppliers')->where('id',$selected['id'])->update(['blocked'=>true]);try{$ok($confirm($p['data']['preview'])['status']===409,'eligibility change after preview requires fresh conference');}finally{$db->table('suppliers')->where('id',$selected['id'])->update(['blocked'=>false]);} $grant=DB::table('permission_grants')->where($f['scope'])->where('user_id',$f['actor'])->where('permission','purchasing_sourcing.prepare')->first();DB::table('permission_grants')->where('id',$grant->id)->delete();try{$ok($confirm($p['data']['preview'])['status']===403&&$call('actor',$url)['status']===403,'revoked right prevents browsing and confirmation');}finally{DB::table('permission_grants')->insert((array)$grant);} $responses=GuzzleHttp\Promise\Utils::unwrap([$clients[0]->postAsync($base.'/confirm',$options('actor',0,['preview'=>$p['data']['preview']])),$clients[1]->postAsync($base.'/confirm',$options('actor',1,['preview'=>$p['data']['preview']]))]);$statuses=array_map(fn($r)=>$r->getStatusCode(),$responses);sort($statuses);$ok($statuses===[200,201]&&$db->table('quotations')->count()===1,'concurrent same-key confirmation still produces exactly one native quote'); $quote=$db->table('quotations')->first();$ok((int)$quote->supplier_id===$selected['id']&&$db->table('quotation_items')->count()===1&&$confirm($p['data']['preview'])['status']===200,'explicit beyond100 choice and historical receipt recover without duplication'); $enter('actor',1,['module'=>'commercial']);$ok($call('actor',$url,null,1)['status']===409,'different available module cannot browse procurement catalogue');$enter('actor',1,['foreign'=>1]);$ok($call('actor',$url,null,1)['status']===403,'unmapped unit cannot reuse supplier search'); $ok($db->table('purchase_orders')->count()===0&&$db->table('invoices')->count()===0&&$db->table('pa_obligations')->count()===0&&$db->table('external_payments')->count()===0&&DB::table('geo_locations')->count()===0,'selection adds no order invoice debt payment or localisation'); file_put_contents($out,json_encode(['passed'=>true,'checks'=>$checks,'eligible_suppliers'=>125,'page_sizes'=>[25,100],'beyond100_selected'=>true,'quote_count'=>1,'concurrency_statuses'=>$statuses,'new_right_names'=>[],'visual_review'=>false,'deployment_capacity_validated'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo count($checks)." supplier selector checks passed.\n";