water_alert_settings_test.php 7.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. <?php
  2. require dirname(__DIR__).'/vendor/autoload.php';
  3. \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
  4. \support\App::loadAllConfig(['route']);
  5. date_default_timezone_set('Asia/Shanghai');
  6. use plugin\saiadmin\app\model\water\{WaterAlertSetting,WaterAccount,WaterEnterprise,WaterSku,WaterLedger,WaterDelivery};
  7. use plugin\saiadmin\app\service\{WaterBusiness,WaterAlertDashboard,WaterEnterpriseDashboard};
  8. use plugin\saiadmin\app\service\alerts\AlertSettings;
  9. use plugin\saiadmin\app\controller\water\WaterController;
  10. use plugin\saiadmin\exception\ApiException;
  11. $checks=0;
  12. function check(bool $ok,string $label): void { global $checks; $checks++; if (!$ok) throw new RuntimeException($label); }
  13. function rejects(callable $fn,string $label): void { try {$fn();} catch (ApiException $e) {check(true,$label);return;} throw new RuntimeException($label); }
  14. function req(string $method,string $url,array $input=[]): \support\Request {
  15. $body=$method==='POST'?json_encode($input):'';
  16. return new \support\Request($method.' '.$url." HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: ".strlen($body)."\r\n\r\n".$body);
  17. }
  18. $before=[];
  19. foreach ([WaterAlertSetting::class,WaterEnterprise::class,WaterAccount::class,WaterSku::class,WaterLedger::class,WaterDelivery::class] as $model) $before[$model]=hash('sha256',serialize($model::query()->orderBy('id')->get()->toArray()));
  20. $db=(new WaterAccount())->getConnection(); $db->beginTransaction();
  21. try {
  22. $settings=new AlertSettings(); $business=new WaterBusiness(); $tag='POLICY-'.bin2hex(random_bytes(4));
  23. $original=$settings->current();
  24. $enterprise=$business->save('enterprise',['name'=>$tag,'contact'=>'TEST','phone'=>'TEST','address'=>'TEST','employee_count'=>3],1)['id'];
  25. $sku=$business->save('sku',['name'=>$tag,'liters'=>18.9],1)['id'];
  26. $account=WaterAccount::query()->insertGetId(['enterprise_id'=>$enterprise,'sku_id'=>$sku,'full_qty'=>5,'threshold_qty'=>999,'daily_liters'=>999,'entitlement'=>20,'updated_at'=>'2026-09-17 12:00:00']);
  27. $accountBefore=WaterAccount::query()->find($account)->toArray();
  28. $values=['alert_qty'=>8,'days_per_barrel'=>6,'urgent_qty'=>4,'revision'=>$original['revision']];
  29. $saved=$business->saveAlertPolicy($values,1);
  30. check($saved['revision']===$original['revision']+1 && $saved['alert_qty']===8,'saved platform parameters and version');
  31. check($settings->current()===$saved,'fresh read sees saved values');
  32. $now=new DateTimeImmutable('2026-09-17 12:00:00');
  33. $board=(new WaterAlertDashboard($now))->dashboard(['enterprise_id'=>$enterprise]);
  34. $row=$board['list']['data'][0];
  35. check($board['policy']===$saved && $board['list']['total']===1,'dashboard reads persisted settings');
  36. check($row['severity']==='normal' && $row['reasons']===['现场满桶少于8桶'],'configured quantity and severity in SQL and presentation');
  37. check($row['days_left']===10.0 && $row['estimated_daily_liters']===9.45 && $row['estimated_daily_barrels']===0.5,'six days per barrel changes all estimates');
  38. check($row['exhausts_at']==='2026-09-27 12:00:00','configured forecast timestamp');
  39. check((new WaterAlertDashboard($now))->listing(['enterprise_id'=>$enterprise,'exhaust_date'=>'2026-09-27'])['total']===1,'configured date SQL matches');
  40. $accounts=$business->listing('accounts',['enterprise_id'=>$enterprise]);
  41. check($accounts['data'][0]['days_left']===10.0 && $accounts['data'][0]['severity']==='normal','account view shares settings');
  42. check((new WaterEnterpriseDashboard())->detail($enterprise)['enterprise']['replenishment_status']==='pending','enterprise pending follows global threshold');
  43. check(WaterAccount::query()->find($account)->toArray()===$accountBefore,'settings do not modify inventory, rights or old account fields');
  44. $audit=WaterLedger::query()->where('reference','platform:alerts')->orderByDesc('id')->first();
  45. check($audit->kind==='alert_policy' && (int)$audit->created_by===1 && (int)$audit->enterprise_id===0,'platform change audited');
  46. $count=WaterLedger::query()->count();
  47. rejects(fn()=>$business->saveAlertPolicy($values,1),'stale revision rejected');
  48. $valid=['alert_qty'=>8,'days_per_barrel'=>6,'urgent_qty'=>4,'revision'=>$saved['revision']];
  49. foreach ([['alert_qty'=>0],['alert_qty'=>10000001],['alert_qty'=>1.5],['alert_qty'=>true],['alert_qty'=>[]],['days_per_barrel'=>0],['days_per_barrel'=>366],['days_per_barrel'=>null],['urgent_qty'=>-1],['urgent_qty'=>8],['revision'=>null],['id'=>1],['threshold_qty'=>9],['enterprise_id'=>$enterprise]] as $invalid) rejects(fn()=>$business->saveAlertPolicy(array_replace($valid,$invalid),1),'invalid or per-company override rejected');
  50. check($settings->current()===$saved && WaterLedger::query()->count()===$count,'failed writes preserve settings and audit');
  51. $reflection=new ReflectionClass(WaterController::class); $controller=$reflection->newInstanceWithoutConstructor();
  52. $reflection->getProperty('adminId')->setValue($controller,0);
  53. rejects(fn()=>$controller->alertPolicy(req('GET','/core/water/alerts/policy')),'policy read requires permission');
  54. rejects(fn()=>$controller->save(req('POST','/core/water/alerts',['action'=>'policy']+$valid),'alerts'),'policy write requires permission');
  55. $reflection->getProperty('adminId')->setValue($controller,1);
  56. $read=json_decode($controller->alertPolicy(req('GET','/core/water/alerts/policy'))->rawBody(),true);
  57. check($read['data']===$saved,'policy controller reads current global parameters');
  58. $response=json_decode($controller->save(req('POST','/core/water/alerts',['action'=>'policy']+array_replace($valid,['urgent_qty'=>5])),'alerts')->rawBody(),true);
  59. check($response['code']===200 && $response['data']['urgent_qty']===5,'JSON controller saves platform policy');
  60. check((new WaterAlertDashboard())->listing(['enterprise_id'=>$enterprise,'severity'=>'urgent'])['total']===1,'urgent SQL filter uses saved threshold');
  61. check((new WaterEnterpriseDashboard())->detail($enterprise)['enterprise']['replenishment_status']==='urgent','enterprise urgent follows edited global threshold');
  62. $updated=$settings->current();
  63. $business->saveAlertPolicy(['alert_qty'=>5,'days_per_barrel'=>3,'urgent_qty'=>0,'revision'=>$updated['revision']],1);
  64. check((new WaterAlertDashboard())->listing(['enterprise_id'=>$enterprise])['total']===0,'equality at edited threshold excludes alert');
  65. check((new WaterEnterpriseDashboard())->detail($enterprise)['enterprise']['replenishment_status']==='healthy','enterprise returns healthy after threshold edit');
  66. WaterAccount::query()->where('id',$account)->update(['full_qty'=>0]);
  67. check((new WaterAlertDashboard())->listing(['enterprise_id'=>$enterprise,'severity'=>'urgent'])['total']===1,'zero urgent threshold correctly alerts zero stock');
  68. } finally { $db->rollBack(); }
  69. foreach ($before as $model=>$hash) check(hash('sha256',serialize($model::query()->orderBy('id')->get()->toArray()))===$hash,'original model restored: '.$model);
  70. if (in_array('--http',$argv,true)) {
  71. $token=\Tinywan\Jwt\JwtToken::generateToken(['id'=>1,'username'=>'admin','plat'=>'saiadmin','type'=>'web','client'=>'policy-test','access_exp'=>120,'refresh_exp'=>120])['access_token'];
  72. $ch=curl_init('http://127.0.0.1:8787/core/water/alerts/policy');
  73. curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>15,CURLOPT_HTTPHEADER=>['Authorization: Bearer '.$token]]);
  74. $body=json_decode((string)curl_exec($ch),true); $status=curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch);
  75. check($status===200 && ($body['code']??0)===200 && $body['data']===(new AlertSettings())->current(),'HTTP current persisted policy');
  76. }
  77. echo "PASS: $checks platform policy checks; fixtures and settings rolled back.\n";