| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126 |
- <?php
- /** Agent hierarchy, real sales, account opening and live authorization regression. Fixtures 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\{WaterModel,WaterAgent,WaterEnterprise,WaterOrder,WaterOperator,WaterOperatorRole,WaterLedger};
- use plugin\saiadmin\app\service\{WaterBusiness,WaterAgentDashboard,WaterAgentManagement};
- 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 tableModel(string $table) { return (new class extends WaterModel {})->setTable($table)->newQuery(); }
- $now=new DateTimeImmutable('2026-09-16 12:00:00',new DateTimeZone('Asia/Shanghai'));
- $dashboard=new WaterAgentDashboard($now); $management=new WaterAgentManagement(); $business=new WaterBusiness();
- $tag='AGT-'.bin2hex(random_bytes(4)); $db=(new WaterAgent())->getConnection(); $db->beginTransaction();
- try {
- $before=$dashboard->dashboard([]); $ids=[];
- foreach (['root','child','leaf','unset','disabled','other'] as $i=>$name) {
- $parent=match($name) {'root','disabled','other'=>0,'child'=>$ids['root'],'leaf'=>$ids['child'],default=>null};
- $ids[$name]=$management->save(['name'=>$tag.'-'.$name,'contact'=>'Contact','phone'=>'TEST-'.$i,'region'=>'测试辖区',
- 'parent_id'=>$parent,'status'=>$name==='disabled'?2:1],1)['id'];
- WaterAgent::query()->where('id',$ids[$name])->update(['created_at'=>'2026-09-01 00:00:00']);
- }
- $first=$dashboard->dashboard(['keyword'=>' '.$tag.' ','limit'=>5]); $second=$dashboard->dashboard(['keyword'=>$tag,'limit'=>5,'page'=>2]);
- check($first['list']['total']===6 && count($first['list']['data'])===5 && count($second['list']['data'])===1,'pagination');
- check(array_column(array_merge($first['list']['data'],$second['list']['data']),'id')===array_reverse(array_values($ids)),'stable order');
- check($first['summary']===$second['summary'],'global cards independent of pages');
- check($first['summary']['total']-$before['summary']['total']===6,'agent total delta');
- check($first['summary']['new_month']-$before['summary']['new_month']===6,'month new delta');
- check($first['summary']['members']-$before['summary']['members']===2,'team members counted once');
- foreach (['1'=>3,'2'=>1,'3'=>1,'unset'=>1] as $level=>$count) check($dashboard->dashboard(['keyword'=>$tag,'level'=>(string)$level])['list']['total']===$count,'level filter '.$level);
- check($dashboard->dashboard(['keyword'=>$tag,'authorization'=>'pending'])['list']['total']===5,'enabled archive is pending, not authorized');
- check($dashboard->dashboard(['keyword'=>$tag,'authorization'=>'disabled'])['list']['total']===1,'disabled filter');
- check($dashboard->dashboard(['keyword'=>'TEST-5'])['list']['total']>=1,'phone search');
- $save=function(string $name,$parent) use ($ids,$management) {
- return $management->save(array_replace(WaterAgent::query()->find($ids[$name])->toArray(),['parent_id'=>$parent]),1);
- };
- rejects(fn()=>$save('root',$ids['leaf']),'cycle rejected');
- rejects(fn()=>$save('root',$ids['other']),'subtree exceeding three levels rejected');
- rejects(fn()=>$save('child',$ids['child']),'self parent rejected');
- rejects(fn()=>$save('child',null),'parent with children cannot become unknown');
- rejects(fn()=>$save('leaf',$ids['unset']),'unknown parent level rejected');
- rejects(fn()=>$save('leaf',$ids['disabled']),'disabled parent rejected');
- rejects(fn()=>$save('leaf',99999999),'missing parent rejected');
- check($dashboard->detail($ids['leaf'])['agent']['level']===3,'invalid reparent left hierarchy intact');
- $save('leaf',$ids['root']); check($dashboard->detail($ids['leaf'])['agent']['level']===2,'reparent recalculates level');
- $save('leaf',$ids['child']);
- $enterprises=[];
- foreach (['root','child','other'] as $name) $enterprises[$name]=$business->save('enterprise',['name'=>$tag.'-company-'.$name,'contact'=>'TEST','phone'=>'TEST','address'=>'TEST','agent_id'=>$ids[$name]],1)['id'];
- $orders=[['root','2026-08-31 23:59:59',1000],['root','2026-09-01 00:00:00',2000],['root','2026-09-16 12:00:00',3000],['child','2026-09-10 12:00:00',7000],['other','2026-09-17 00:00:00',99000]];
- foreach ($orders as $i=>[$name,$at,$amount]) WaterOrder::query()->insert(['enterprise_id'=>$enterprises[$name],'package_id'=>0,'sku_id'=>0,'package_name'=>'TEST','buckets'=>1,'amount_cents'=>$amount,'reference'=>$tag.'-order-'.$i,'created_by'=>1,'created_at'=>$at]);
- $data=$dashboard->dashboard(['keyword'=>$tag,'limit'=>100]); $rows=array_column($data['list']['data'],null,'id');
- check($rows[$ids['root']]['sales_cents']===6000 && $rows[$ids['root']]['month_cents']===5000,'month boundary and lifetime sales');
- check($rows[$ids['child']]['month_cents']===7000 && $rows[$ids['other']]['month_cents']===0,'future sales excluded');
- check($rows[$ids['root']]['enterprise_count']===1,'orders do not multiply enterprise count');
- check($data['summary']['sales_cents']-$before['summary']['sales_cents']===13000,'global sales delta');
- check($data['summary']['active_members']-$before['summary']['active_members']===1,'active member has real monthly sales');
- $rankIds=array_column($data['ranking'],'id');
- check(array_search($ids['child'],$rankIds,true)<array_search($ids['root'],$rankIds,true),'monthly sales ranking');
- check($data['summary']['commission_cents']===$before['summary']['commission_cents'] && $rows[$ids['root']]['commission_cents']===0 && $data['commission']['configured']===true,'only recorded commissions are shown');
- $detail=$dashboard->detail($ids['root']);
- check(count($detail['children'])===1 && count($detail['enterprises'])===1,'detail direct team and enterprises');
- $password='Test-'.bin2hex(random_bytes(12));
- $input=['id'=>$ids['root'],'username'=>$tag.'-user','password'=>$password];
- rejects(fn()=>$management->openAccount($input,101),'non-superuser cannot open account');
- rejects(fn()=>$management->openAccount(array_replace($input,['password'=>'123']),1),'short password rejected');
- $account=$management->openAccount($input,1); $uid=$account['user_id'];
- $user=WaterOperator::query()->find($uid);
- check(password_verify($password,$user->getRawOriginal('password')),'password hashed');
- check(!array_key_exists('password',$user->toArray()),'model password hidden');
- check(WaterOperatorRole::query()->where('user_id',$uid)->count()===0 && (int)$user->is_super===0 && $user->dept_id===null,'no automatic roles or scope');
- check($dashboard->detail($ids['root'])['agent']['authorization']==='pending','account without roles is pending');
- rejects(fn()=>$management->openAccount(array_replace($input,['username'=>$tag.'-new']),1),'agent account duplicate rejected');
- rejects(fn()=>$management->openAccount(array_replace($input,['id'=>$ids['child']]),1),'duplicate username rejected');
- // A valid water permission is required before an active linked account can be shown authorized.
- $role=tableModel('sa_system_role')->insertGetId(['name'=>$tag,'code'=>$tag,'status'=>1,'data_scope'=>4]);
- $menu=tableModel('sa_system_menu')->where('slug','water:agents:read')->whereNull('delete_time')->value('id');
- check((bool)$menu,'existing water permission');
- tableModel('sa_system_role_menu')->insert(['role_id'=>$role,'menu_id'=>$menu]);
- WaterOperatorRole::query()->insert(['user_id'=>$uid,'role_id'=>$role]);
- check($dashboard->detail($ids['root'])['agent']['authorization']==='authorized','live permission authorizes display');
- $reflection=new ReflectionClass(WaterController::class); $controller=$reflection->newInstanceWithoutConstructor();
- $reflection->getProperty('adminId')->setValue($controller,$uid);
- $request=new \support\Request("GET /core/water/agents/dashboard HTTP/1.1\r\nHost: localhost\r\n\r\n");
- check(json_decode($controller->agentDashboard($request)->rawBody(),true)['code']===200,'authorized read');
- WaterAgent::query()->where('id',$ids['root'])->update(['status'=>2]);
- rejects(fn()=>$controller->agentDashboard($request),'disabled agent blocked despite role');
- check($dashboard->detail($ids['root'])['agent']['authorization']==='disabled','disabled agent display');
- WaterAgent::query()->where('id',$ids['root'])->update(['status'=>1]);
- tableModel('sa_system_role')->where('id',$role)->update(['status'=>0]);
- rejects(fn()=>$controller->agentDashboard($request),'disabled role immediately blocked');
- check($dashboard->detail($ids['root'])['agent']['authorization']==='pending','disabled role removes display authorization');
- WaterOperator::query()->where('id',$uid)->update(['status'=>2]);
- check($dashboard->detail($ids['root'])['agent']['authorization']==='disabled','disabled linked account');
- $reflection->getProperty('adminId')->setValue($controller,99999999);
- rejects(fn()=>$controller->agentDashboard($request),'dashboard unauthorized');
- rejects(fn()=>$controller->agentDetail($request),'detail unauthorized');
- $body=json_encode($input);
- $post=new \support\Request("POST /core/water/agents/open-account HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: ".strlen($body)."\r\n\r\n".$body);
- $response=$controller->agentOpenAccount($post)->rawBody();
- check(!str_contains($response,$password) && (json_decode($response,true)['code']??0)!==200,'opening error does not echo credentials');
- check(!str_contains(serialize(WaterLedger::query()->where('reference','agent:'.$ids['root'])->get()->toArray()),$password),'audit excludes password');
- foreach ([['keyword'=>[]],['keyword'=>str_repeat('字',121)],['level'=>'9'],['authorization'=>'bad']] as $input) rejects(fn()=>$dashboard->dashboard($input),'invalid filters');
- rejects(fn()=>$dashboard->detail(99999999),'missing detail');
- $roleless=['name'=>$tag.'-new','contact'=>'TEST','phone'=>'TEST','region'=>'TEST','parent_id'=>0,'username'=>$tag.'-new-user','password'=>$password];
- $created=$management->openAccount($roleless,1);
- check((int)WaterAgent::query()->find($created['id'])->user_id===$created['user_id'],'new archive and account created atomically');
- $agentCount=WaterAgent::query()->count();
- rejects(fn()=>$management->openAccount(array_replace($roleless,['username'=>$tag.'-rollback','phone'=>str_repeat('x',21)]),1),'account validation rollback');
- check(WaterAgent::query()->count()===$agentCount && !WaterOperator::query()->where('username',$tag.'-rollback')->exists(),'no orphan archive on opening failure');
- } finally { $db->rollBack(); }
- check(!WaterAgent::query()->where('name','like',$tag.'%')->exists() && !WaterOperator::query()->where('username','like',$tag.'%')->exists(),'all 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'];
- $id=WaterAgent::query()->orderBy('id')->value('id');
- foreach (['dashboard?limit=1','detail?id='.$id] as $path) {
- $ch=curl_init('http://127.0.0.1:8787/core/water/agents/'.$path);
- curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>15,CURLOPT_HTTPHEADER=>['Authorization: Bearer '.$token]]);
- $raw=curl_exec($ch); $status=curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch); $body=json_decode((string)$raw,true);
- check($status===200 && ($body['code']??0)===200,'HTTP '.$path);
- check(!str_contains((string)$raw,'"password"'),'HTTP excludes password');
- }
- }
- echo "PASS: $checks agent checks; all fixtures rolled back.\n";
|