water_fulfillment_http_test.php 6.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. <?php
  2. /** Local HTTP smoke test; temporary tagged fixtures are removed in finally. Tokens never leave memory. */
  3. require dirname(__DIR__).'/vendor/autoload.php';
  4. \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
  5. \support\App::loadAllConfig(['route']);
  6. date_default_timezone_set('Asia/Shanghai');
  7. use plugin\saiadmin\app\model\water\{WaterModel,WaterSku,WaterEnterprise,WaterAccount,WaterDelivery,WaterDriver,WaterDriverShift,WaterRoute,WaterLedger,WaterDeliveryEvent};
  8. use plugin\saiadmin\app\service\{WaterBusiness,WaterDispatch};
  9. use Tinywan\Jwt\JwtToken;
  10. $tag='HTTP-FULFILL-'.bin2hex(random_bytes(5)); $date=date('Y-m-d'); $checks=0;
  11. $skuId=null; $enterpriseId=null; $driverId=null; $routeIds=[]; $taskIds=[];
  12. // A distinct short-lived client avoids replacing any user's single-device browser session.
  13. $token=JwtToken::generateToken(['id'=>1,'username'=>'admin','plat'=>'saiadmin','type'=>'web','client'=>$tag,'access_exp'=>120,'refresh_exp'=>120])['access_token'];
  14. function api(string $path,?array $data=null): array {
  15. global $token,$checks;
  16. $ch=curl_init('http://127.0.0.1:8787/core/water/'.$path);
  17. $options=[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>15,CURLOPT_HTTPHEADER=>['Content-Type: application/json','Authorization: Bearer '.$token]];
  18. if ($data!==null) $options += [CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>json_encode($data)];
  19. curl_setopt_array($ch,$options); $raw=curl_exec($ch); $status=curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch);
  20. $result=json_decode($raw,true);
  21. if ($status!==200 || ($result['code']??null)!==200) throw new RuntimeException('HTTP '.$status.' '.$path.': '.($result['message']??'invalid response'));
  22. $checks++; return $result['data'];
  23. }
  24. function verify(bool $value,string $message): void { global $checks; if (!$value) throw new RuntimeException($message); $checks++; }
  25. try {
  26. WaterModel::transaction(function () use ($tag,$date,&$skuId,&$enterpriseId,&$driverId,&$taskIds,&$routeIds) {
  27. $b=new WaterBusiness(); $d=new WaterDispatch();
  28. $skuId=$b->save('sku',['name'=>$tag,'liters'=>18.9],1)['id'];
  29. $b->stock(['sku_id'=>$skuId,'mode'=>'inbound','quantity'=>10,'reference'=>$tag,'note'=>'temporary HTTP fixture'],1);
  30. $enterpriseId=$b->save('enterprise',['name'=>$tag,'contact'=>'test','phone'=>'TEST','address'=>'TEST DO NOT DELIVER','region'=>$tag],1)['id'];
  31. WaterAccount::query()->insert(['enterprise_id'=>$enterpriseId,'sku_id'=>$skuId,'entitlement'=>10,'empty_qty'=>2,'updated_at'=>date('Y-m-d H:i:s')]);
  32. $taskIds[]=$b->createDelivery(['enterprise_id'=>$enterpriseId,'sku_id'=>$skuId,'quantity'=>4,'date'=>$date],1)['id'];
  33. $driverId=$d->saveDriver(['name'=>$tag,'phone'=>$tag,'region'=>$tag,'capacity'=>10],1)['id'];
  34. $d->shift(['driver_id'=>$driverId,'date'=>$date,'state'=>'on'],1);
  35. $routeIds=$d->commit(['date'=>$date,'request_key'=>$tag,'routes'=>[['driver_id'=>$driverId,'task_ids'=>$taskIds]]],1)['route_ids'];
  36. });
  37. api('dispatch',['action'=>'depart','id'=>$taskIds[0]]);
  38. $receipt=['action'=>'receive','id'=>$taskIds[0],'accepted_qty'=>2,'empty_returned'=>2,'receiver'=>'HTTP test receiver','reason'=>'test partial receipt'];
  39. verify(api('dispatch',$receipt)['status']==='returning','partial status');
  40. verify(api('dispatch',$receipt)['replayed']===true,'HTTP receipt replay');
  41. $board=api('dispatch/board?keyword='.rawurlencode($tag));
  42. verify($board['summary']['returning']===1,'HTTP exception filter visible');
  43. api('dispatch',['action'=>'return_receive','id'=>$taskIds[0],'good_qty'=>1,'damaged_qty'=>1,'note'=>'test warehouse check']);
  44. $route=api('dispatch/route?id='.$routeIds[0]);
  45. verify($route['fulfillment']['can_settle']===true && $route['fulfillment']['empty_items'][0]['expected']===2,'HTTP route custody');
  46. api('dispatch',['action'=>'route_settle','route_id'=>$routeIds[0],'items'=>[['sku_id'=>$skuId,'actual'=>2,'lost'=>0]],'note'=>'test warehouse handover']);
  47. $new=api('dispatch',['action'=>'redeliver','id'=>$taskIds[0],'date'=>$date,'note'=>'test reschedule']); $taskIds[]=$new['id'];
  48. verify((int)WaterDelivery::query()->find($new['id'])->redelivery_of===$taskIds[0],'HTTP linked retry');
  49. api('dispatch',['action'=>'cancel','id'=>$new['id']]);
  50. verify(count(api('dispatch/history?id='.$taskIds[0]))>=3,'HTTP audit history');
  51. $s=WaterSku::query()->find($skuId); $a=WaterAccount::query()->where('enterprise_id',$enterpriseId)->first();
  52. verify((int)$s->available===7 && (int)$s->transit===0 && (int)$s->locked===0 && (int)$s->damaged_qty===1 && (int)$s->empty_qty===2,'HTTP full and empty stock reconciled');
  53. verify((int)$a->entitlement===8 && (int)$a->frozen===0 && (int)$a->full_qty===2,'HTTP rights reconciled');
  54. } finally {
  55. WaterModel::transaction(function () use ($tag,$skuId,$enterpriseId,$driverId) {
  56. if (!$skuId) return;
  57. $sku=WaterSku::query()->find($skuId);
  58. if (!$sku) return;
  59. if ($sku->name!==$tag) throw new RuntimeException('Fixture ownership changed; cleanup stopped');
  60. $tasks=WaterDelivery::query()->where('sku_id',$skuId)->get();
  61. if ($tasks->contains(fn($task)=>(int)$task->enterprise_id!==$enterpriseId)) throw new RuntimeException('Unexpected task attached to test SKU');
  62. $ids=$tasks->pluck('id')->all(); $routes=$tasks->pluck('route_id')->filter()->unique()->all();
  63. WaterDeliveryEvent::query()->whereIn('delivery_id',$ids)->orWhereIn('route_id',$routes)->delete();
  64. WaterLedger::query()->where('sku_id',$skuId)->delete();
  65. $refs=array_merge(array_map(fn($id)=>'route:'.$id,$routes),array_map(fn($id)=>'delivery:'.$id,$ids),['driver:'.$driverId]);
  66. WaterLedger::query()->where('kind','dispatch_config')->whereIn('reference',$refs)->delete();
  67. WaterDelivery::query()->whereIn('id',$ids)->delete();
  68. WaterRoute::query()->whereIn('id',$routes)->delete();
  69. WaterAccount::query()->where('sku_id',$skuId)->delete();
  70. if ($enterpriseId) WaterEnterprise::query()->where('id',$enterpriseId)->where('name',$tag)->delete();
  71. if ($driverId) { WaterDriverShift::query()->where('driver_id',$driverId)->delete(); WaterDriver::query()->where('id',$driverId)->where('name',$tag)->delete(); }
  72. WaterSku::query()->where('id',$skuId)->where('name',$tag)->delete();
  73. });
  74. }
  75. verify(!WaterSku::query()->where('name',$tag)->exists() && !WaterEnterprise::query()->where('name',$tag)->exists(),'temporary fixtures removed');
  76. echo "PASS: $checks local HTTP fulfillment checks; temporary fixtures removed; normal operation audit retained.\n";