load(); \support\App::loadAllConfig(['route']); date_default_timezone_set('Asia/Shanghai'); use plugin\saiadmin\app\model\water\{WaterAlertSetting,WaterAccount,WaterEnterprise,WaterSku,WaterLedger,WaterDelivery}; use plugin\saiadmin\app\service\{WaterBusiness,WaterAlertDashboard,WaterEnterpriseDashboard}; use plugin\saiadmin\app\service\alerts\AlertSettings; use plugin\saiadmin\app\controller\water\WaterController; use plugin\saiadmin\exception\ApiException; $checks=0; function check(bool $ok,string $label): void { global $checks; $checks++; if (!$ok) throw new RuntimeException($label); } function rejects(callable $fn,string $label): void { try {$fn();} catch (ApiException $e) {check(true,$label);return;} throw new RuntimeException($label); } function req(string $method,string $url,array $input=[]): \support\Request { $body=$method==='POST'?json_encode($input):''; 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); } $before=[]; 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())); $db=(new WaterAccount())->getConnection(); $db->beginTransaction(); try { $settings=new AlertSettings(); $business=new WaterBusiness(); $tag='POLICY-'.bin2hex(random_bytes(4)); $original=$settings->current(); $enterprise=$business->save('enterprise',['name'=>$tag,'contact'=>'TEST','phone'=>'TEST','address'=>'TEST','employee_count'=>3],1)['id']; $sku=$business->save('sku',['name'=>$tag,'liters'=>18.9],1)['id']; $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']); $accountBefore=WaterAccount::query()->find($account)->toArray(); $values=['alert_qty'=>8,'days_per_barrel'=>6,'urgent_qty'=>4,'revision'=>$original['revision']]; $saved=$business->saveAlertPolicy($values,1); check($saved['revision']===$original['revision']+1 && $saved['alert_qty']===8,'saved platform parameters and version'); check($settings->current()===$saved,'fresh read sees saved values'); $now=new DateTimeImmutable('2026-09-17 12:00:00'); $board=(new WaterAlertDashboard($now))->dashboard(['enterprise_id'=>$enterprise]); $row=$board['list']['data'][0]; check($board['policy']===$saved && $board['list']['total']===1,'dashboard reads persisted settings'); check($row['severity']==='normal' && $row['reasons']===['现场满桶少于8桶'],'configured quantity and severity in SQL and presentation'); 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'); check($row['exhausts_at']==='2026-09-27 12:00:00','configured forecast timestamp'); check((new WaterAlertDashboard($now))->listing(['enterprise_id'=>$enterprise,'exhaust_date'=>'2026-09-27'])['total']===1,'configured date SQL matches'); $accounts=$business->listing('accounts',['enterprise_id'=>$enterprise]); check($accounts['data'][0]['days_left']===10.0 && $accounts['data'][0]['severity']==='normal','account view shares settings'); check((new WaterEnterpriseDashboard())->detail($enterprise)['enterprise']['replenishment_status']==='pending','enterprise pending follows global threshold'); check(WaterAccount::query()->find($account)->toArray()===$accountBefore,'settings do not modify inventory, rights or old account fields'); $audit=WaterLedger::query()->where('reference','platform:alerts')->orderByDesc('id')->first(); check($audit->kind==='alert_policy' && (int)$audit->created_by===1 && (int)$audit->enterprise_id===0,'platform change audited'); $count=WaterLedger::query()->count(); rejects(fn()=>$business->saveAlertPolicy($values,1),'stale revision rejected'); $valid=['alert_qty'=>8,'days_per_barrel'=>6,'urgent_qty'=>4,'revision'=>$saved['revision']]; 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'); check($settings->current()===$saved && WaterLedger::query()->count()===$count,'failed writes preserve settings and audit'); $reflection=new ReflectionClass(WaterController::class); $controller=$reflection->newInstanceWithoutConstructor(); $reflection->getProperty('adminId')->setValue($controller,0); rejects(fn()=>$controller->alertPolicy(req('GET','/core/water/alerts/policy')),'policy read requires permission'); rejects(fn()=>$controller->save(req('POST','/core/water/alerts',['action'=>'policy']+$valid),'alerts'),'policy write requires permission'); $reflection->getProperty('adminId')->setValue($controller,1); $read=json_decode($controller->alertPolicy(req('GET','/core/water/alerts/policy'))->rawBody(),true); check($read['data']===$saved,'policy controller reads current global parameters'); $response=json_decode($controller->save(req('POST','/core/water/alerts',['action'=>'policy']+array_replace($valid,['urgent_qty'=>5])),'alerts')->rawBody(),true); check($response['code']===200 && $response['data']['urgent_qty']===5,'JSON controller saves platform policy'); check((new WaterAlertDashboard())->listing(['enterprise_id'=>$enterprise,'severity'=>'urgent'])['total']===1,'urgent SQL filter uses saved threshold'); check((new WaterEnterpriseDashboard())->detail($enterprise)['enterprise']['replenishment_status']==='urgent','enterprise urgent follows edited global threshold'); $updated=$settings->current(); $business->saveAlertPolicy(['alert_qty'=>5,'days_per_barrel'=>3,'urgent_qty'=>0,'revision'=>$updated['revision']],1); check((new WaterAlertDashboard())->listing(['enterprise_id'=>$enterprise])['total']===0,'equality at edited threshold excludes alert'); check((new WaterEnterpriseDashboard())->detail($enterprise)['enterprise']['replenishment_status']==='healthy','enterprise returns healthy after threshold edit'); WaterAccount::query()->where('id',$account)->update(['full_qty'=>0]); check((new WaterAlertDashboard())->listing(['enterprise_id'=>$enterprise,'severity'=>'urgent'])['total']===1,'zero urgent threshold correctly alerts zero stock'); } finally { $db->rollBack(); } foreach ($before as $model=>$hash) check(hash('sha256',serialize($model::query()->orderBy('id')->get()->toArray()))===$hash,'original model restored: '.$model); if (in_array('--http',$argv,true)) { $token=\Tinywan\Jwt\JwtToken::generateToken(['id'=>1,'username'=>'admin','plat'=>'saiadmin','type'=>'web','client'=>'policy-test','access_exp'=>120,'refresh_exp'=>120])['access_token']; $ch=curl_init('http://127.0.0.1:8787/core/water/alerts/policy'); curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>15,CURLOPT_HTTPHEADER=>['Authorization: Bearer '.$token]]); $body=json_decode((string)curl_exec($ch),true); $status=curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch); check($status===200 && ($body['code']??0)===200 && $body['data']===(new AlertSettings())->current(),'HTTP current persisted policy'); } echo "PASS: $checks platform policy checks; fixtures and settings rolled back.\n";