| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- <?php
- /** Enterprise dashboard integration regression; all fixtures and batch writes roll back. */
- require dirname(__DIR__).'/vendor/autoload.php';
- \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
- \support\App::loadAllConfig(['route']);
- date_default_timezone_set('Asia/Shanghai');
- use plugin\saiadmin\app\model\water\{WaterEnterprise,WaterAgent,WaterSku,WaterAccount,WaterDevice,WaterDeviceUsage,WaterOrder,WaterDelivery,WaterLedger};
- use plugin\saiadmin\app\service\{WaterBusiness,WaterEnterpriseDashboard};
- 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 workbook(string $content): array {
- $path=tempnam(sys_get_temp_dir(),'enterprise-test-'); $reader=new \OpenSpout\Reader\XLSX\Reader();
- try { file_put_contents($path,$content); $reader->open($path); $rows=[]; foreach ($reader->getSheetIterator() as $sheet) foreach ($sheet->getRowIterator() as $row) $rows[]=$row->toArray(); return $rows; }
- finally { $reader->close(); unlink($path); }
- }
- $now=new DateTimeImmutable('2026-09-16 12:00:00',new DateTimeZone('Asia/Shanghai'));
- $service=new WaterEnterpriseDashboard($now); $business=new WaterBusiness();
- $tag='ENT-'.bin2hex(random_bytes(5)); $db=(new WaterEnterprise())->getConnection();
- $db->beginTransaction();
- try {
- \plugin\saiadmin\app\model\water\WaterAlertSetting::query()->where('id',1)->update(['alert_qty'=>5,'days_per_barrel'=>3,'urgent_qty'=>2]);
- $before=$service->dashboard([]);
- $agent=$business->save('agent',['name'=>$tag.'-agent','contact'=>'test','phone'=>'TEST'],1)['id'];
- $disabledAgent=$business->save('agent',['name'=>$tag.'-disabled','contact'=>'test','phone'=>'TEST','status'=>2],1)['id'];
- $sku=$business->save('sku',['name'=>$tag.'-sku','liters'=>10],1)['id'];
- $sku2=$business->save('sku',['name'=>$tag.'-sku2','liters'=>20],1)['id'];
- $ids=[];
- foreach (['healthy','urgent','pending','ordered','unconfigured','disabled','mixed'] as $i=>$state) {
- $ids[$state]=$business->save('enterprise',['name'=>($i===0?'=':'').$tag.'-'.$state,'contact'=>'contact-'.$tag,'phone'=>'phone-'.$tag,'address'=>'TEST ONLY','agent_id'=>$i===0?$agent:0,'status'=>$state==='disabled'?2:1],1)['id'];
- WaterEnterprise::query()->where('id',$ids[$state])->update(['created_at'=>'2026-09-01 00:00:00']);
- if ($state==='unconfigured') continue;
- WaterAccount::query()->insert(['enterprise_id'=>$ids[$state],'sku_id'=>$sku,'entitlement'=>$i===0?20:5,'frozen'=>$i===0?10:0,
- 'full_qty'=>$state==='healthy'?20:($state==='pending'?4:1),'empty_qty'=>2,'threshold_qty'=>5,'updated_at'=>'2026-09-16 12:00:00']);
- }
- // Two SKUs, multiple devices and orders must not multiply stock or rights.
- WaterAccount::query()->insert(['enterprise_id'=>$ids['healthy'],'sku_id'=>$sku2,'entitlement'=>30,'frozen'=>10,'full_qty'=>9,'empty_qty'=>3,'updated_at'=>'2026-09-16 12:00:00']);
- WaterAccount::query()->insert(['enterprise_id'=>$ids['mixed'],'sku_id'=>$sku2,'entitlement'=>2,'full_qty'=>0,'updated_at'=>'2026-09-16 12:00:00']);
- foreach (['ordered','mixed'] as $state) {
- WaterDelivery::query()->insert(['enterprise_id'=>$ids[$state],'sku_id'=>$sku,'quantity'=>1,'address'=>'TEST','created_by'=>1,
- 'active_key'=>$ids[$state].':'.$sku,'created_at'=>'2026-09-16 12:00:00','updated_at'=>'2026-09-16 12:00:00']);
- }
- $devices=[];
- foreach ([null,'2026-09-16 11:45:00','2026-09-16 11:44:59'] as $i=>$seen) {
- $devices[]=$business->save('device',['sn'=>$tag.'-'.$i,'name'=>'device','enterprise_id'=>$ids['healthy'],'sku_id'=>$sku],1)['id'];
- WaterDevice::query()->where('id',$devices[$i])->update(['last_seen'=>$seen]);
- }
- WaterDeviceUsage::query()->insert(['device_id'=>$devices[0],'day'=>'2026-08-18','liters'=>3]);
- WaterLedger::query()->insert(['enterprise_id'=>$ids['urgent'],'sku_id'=>$sku,'kind'=>'exchange','quantity'=>-1,'reference'=>$tag,'created_by'=>1,'created_at'=>'2026-09-16 11:00:00']);
- // A previous-month first purchase plus a current-month purchase is a repeat buyer.
- $orderRows=[['healthy','2026-09-02 10:00:00',100],['healthy','2026-09-16 10:00:00',200],['urgent','2026-09-03 10:00:00',300],['pending','2026-08-31 23:59:59',400],['pending','2026-09-01 00:00:00',500]];
- foreach ($orderRows as $i=>[$state,$time,$amount]) WaterOrder::query()->insert(['enterprise_id'=>$ids[$state],'sku_id'=>$sku,'package_id'=>0,'package_name'=>'TEST','reference'=>$tag.'-order-'.$i,'buckets'=>1,'amount_cents'=>$amount,'created_by'=>1,'created_at'=>$time]);
- // Pending was already a customer before this month, so exclude it from the new-customer funnel.
- WaterEnterprise::query()->where('id',$ids['pending'])->update(['created_at'=>'2026-08-17 23:59:59']);
- $first=$service->dashboard(['keyword'=>' '.$tag.' ','limit'=>3]);
- $second=$service->dashboard(['keyword'=>$tag,'page'=>2,'limit'=>3]);
- $third=$service->dashboard(['keyword'=>$tag,'page'=>3,'limit'=>3]);
- $rows=array_merge($first['list']['data'],$second['list']['data'],$third['list']['data']);
- check($first['list']['total']===7 && count($rows)===7,'paginated search');
- check(array_column($rows,'id')===array_reverse(array_values($ids)),'stable pagination');
- $byId=array_column($rows,null,'id');
- foreach ($ids as $state=>$id) check($byId[$id]['replenishment_status']===($state==='mixed'?'urgent':$state),'replenishment '.$state);
- $healthy=$byId[$ids['healthy']];
- foreach (['entitlement'=>50,'frozen'=>20,'available_entitlement'=>30,'full_qty'=>29,'empty_qty'=>5,'device_count'=>3,'online_count'=>1,'unconnected_count'=>1,'latest_order_amount'=>200] as $key=>$expected) check($healthy[$key]===$expected,'aggregate '.$key);
- check($healthy['latest_order_at']==='2026-09-16 10:00:00','latest order deterministic');
- check($first['summary']===$second['summary'] && $first['summary']===$service->dashboard(['keyword'=>'missing-'.$tag])['summary'],'global summary independent of filters and pages');
- check($first['summary']['total']-$before['summary']['total']===7,'total delta');
- check($first['summary']['new_month']-$before['summary']['new_month']===6,'new customer month boundary');
- check($first['summary']['active']-$before['summary']['active']===2,'actual usage and exchange count distinct enterprises');
- check($first['summary']['entitlement']-$before['summary']['entitlement']===77,'rights include frozen once');
- check($first['summary']['repurchase_buyers']-$before['summary']['repurchase_buyers']===3,'monthly buyer denominator');
- check($first['summary']['repeat_buyers']-$before['summary']['repeat_buyers']===2,'repeat buyer includes previous first purchase');
- foreach (['registered'=>6,'purchased'=>2,'repeated'=>1] as $key=>$delta) check($first['funnel'][$key]-$before['funnel'][$key]===$delta,'cohort funnel '.$key);
- check(count($first['trend'])===30 && $first['trend'][0]['day']==='2026-08-18' && $first['trend'][29]['day']==='2026-09-16','30-day boundaries');
- check($first['trend'][0]['total']-$before['trend'][0]['total']===1 && $first['trend'][29]['total']-$before['trend'][29]['total']===7,'cumulative growth');
- check($service->dashboard(['keyword'=>$tag,'status'=>'2'])['list']['total']===1,'disabled filter');
- check($service->dashboard(['keyword'=>$tag,'agent_id'=>$agent])['list']['total']===1,'agent filter');
- check($service->dashboard(['keyword'=>$tag,'agent_id'=>0])['list']['total']===6,'direct filter');
- foreach (['empty'=>1,'low'=>5,'sufficient'=>1] as $balance=>$total) check($service->dashboard(['keyword'=>$tag,'balance'=>$balance])['list']['total']===$total,'balance '.$balance);
- check($service->dashboard(['keyword'=>'phone-'.$tag])['list']['total']===7,'phone search');
- $detail=$service->detail($ids['healthy']);
- check(count($detail['accounts'])===2 && count($detail['orders'])===2 && $detail['accounts'][0]['sku']['name']===$tag.'-sku','detail model relations');
- $edit=WaterEnterprise::query()->find($ids['healthy'])->toArray();
- $business->save('enterprise',$edit+['longitude'=>113.1,'latitude'=>22.2],1);
- check((int)WaterEnterprise::query()->find($ids['healthy'])->agent_id===$agent,'editor agent preserved');
- rejects(fn()=>$business->save('enterprise',array_replace($edit,['agent_id'=>$disabledAgent]),1),'disabled agent assignment rejected');
- rejects(fn()=>$business->save('enterprise',array_replace($edit,['agent_id'=>99999999]),1),'missing agent rejected');
- $batch=$service->batchStatus(['ids'=>[$ids['healthy'],$ids['urgent']],'status'=>2],1);
- check($batch['changed']===2,'batch changed count');
- check($service->batchStatus(['ids'=>[$ids['healthy'],$ids['urgent']],'status'=>2],1)['changed']===0,'batch idempotent');
- check(WaterLedger::query()->whereIn('enterprise_id',[$ids['healthy'],$ids['urgent']])->where('note','批量停用企业')->count()===2,'batch audit');
- check($service->detail($ids['healthy'])['enterprise']['entitlement']===50,'batch preserves rights');
- rejects(fn()=>$service->batchStatus(['ids'=>[$ids['healthy'],99999999],'status'=>1],1),'all-or-nothing missing id');
- check((int)WaterEnterprise::query()->find($ids['healthy'])->status===2,'failed batch rolled back');
- $service->batchStatus(['ids'=>[$ids['healthy'],$ids['urgent']],'status'=>1],1);
- $book=workbook($service->export(['keyword'=>$tag,'page'=>2,'limit'=>1]));
- check(count($book)===8 && count($book[0])===17,'export all filtered rows');
- check($book[7][0]==="'=".$tag.'-healthy','export formula escaping');
- check(count(workbook($service->export(['keyword'=>$tag,'agent_id'=>$agent])))===2,'export filter matches list');
- foreach ([['keyword'=>['bad']],['keyword'=>str_repeat('字',121)],['status'=>'bad'],['balance'=>'bad'],['agent_id'=>'bad']] as $input) rejects(fn()=>$service->dashboard($input),'invalid filter');
- rejects(fn()=>$service->batchStatus(['ids'=>[],'status'=>1],1),'empty batch');
- rejects(fn()=>$service->detail(99999999),'missing detail');
- $reflection=new ReflectionClass(WaterController::class); $controller=$reflection->newInstanceWithoutConstructor();
- $reflection->getProperty('adminId')->setValue($controller,99999999);
- $request=new \support\Request("GET /core/water/enterprises/dashboard HTTP/1.1\r\nHost: localhost\r\n\r\n");
- foreach (['enterpriseDashboard','enterpriseDetail','enterpriseExport','enterpriseBatchStatus'] as $method) rejects(fn()=>$controller->$method($request),'authorization '.$method);
- $reflection->getProperty('adminId')->setValue($controller,1);
- $body=json_encode(['keyword'=>$tag]);
- $request=new \support\Request("POST /core/water/enterprises/export HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: ".strlen($body)."\r\n\r\n".$body);
- check(count(workbook($controller->enterpriseExport($request)->rawBody()))===8,'JSON export controller');
- } finally { $db->rollBack(); }
- check(WaterEnterprise::query()->where('name','like','%'.$tag.'%')->count()===0,'fixtures rolled back');
- if (in_array('--http',$argv,true)) {
- $token=\Tinywan\Jwt\JwtToken::generateToken(['id'=>1,'username'=>'admin','plat'=>'saiadmin','type'=>'web','client'=>$tag,'access_exp'=>120,'refresh_exp'=>120])['access_token'];
- foreach (['dashboard?limit=1','export'] as $path) {
- $ch=curl_init('http://127.0.0.1:8787/core/water/enterprises/'.$path);
- $options=[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>20,CURLOPT_HTTPHEADER=>['Content-Type: application/json','Authorization: Bearer '.$token]];
- if ($path==='export') $options += [CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>'{}'];
- curl_setopt_array($ch,$options); $raw=curl_exec($ch); $status=curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch);
- check($status===200,'HTTP status '.$path);
- if ($path==='export') check(substr($raw,0,2)==='PK','HTTP workbook');
- else { $json=json_decode($raw,true); check(($json['code']??0)===200 && count($json['data']['trend']??[])===30,'HTTP dashboard contract'); }
- }
- unset($token);
- }
- echo "PASS: $checks enterprise checks; fixtures rolled back.\n";
|