feat: 增强代理和玩家管理功能
- 在 SyncAdminAuthorizationCommand 中新增对代理线路和结算菜单操作的同步功能,确保缺失的菜单操作行能够被创建。 - 更新多个控制器中的权限检查逻辑,使用 hasPermissionCode 替代原有的权限验证方式,提升权限管理的灵活性。 - 在 AdminPlayerStoreController 中引入对玩家创建能力的验证,确保只有具备相应权限的管理员能够创建玩家。 - 更新请求验证逻辑,新增 credit_limit、rebate_rate 和 extra_rebate_rate 字段,以支持更细粒度的玩家管理。 - 在 AdminUser 和 AgentNode 模型中增强角色与用户的权限管理功能,支持更细粒度的权限控制。
This commit is contained in:
105
tests/Feature/AdminAgentLineApiTest.php
Normal file
105
tests/Feature/AdminAgentLineApiTest.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
||||
});
|
||||
|
||||
test('super admin can provision agent line with aligned root code', function (): void {
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'line_super',
|
||||
'name' => 'Line Super',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($admin);
|
||||
|
||||
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$response = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->postJson('/api/v1/admin/agent-lines', [
|
||||
'code' => 'line-alpha',
|
||||
'name' => 'Line Alpha',
|
||||
'username' => 'line_alpha_owner',
|
||||
'password' => 'secret-strong',
|
||||
'currency_code' => 'NPR',
|
||||
'status' => 1,
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.code', 'line-alpha')
|
||||
->assertJsonPath('data.agent_node.code', 'line-alpha')
|
||||
->assertJsonPath('data.line_root.site_code', 'line-alpha')
|
||||
->assertJsonPath('data.secrets.sso_jwt_secret', fn ($v) => is_string($v) && $v !== '')
|
||||
->assertJsonPath('data.secrets.wallet_api_key', fn ($v) => is_string($v) && $v !== '');
|
||||
|
||||
$siteId = (int) DB::table('admin_sites')->where('code', 'line-alpha')->value('id');
|
||||
expect($siteId)->toBeGreaterThan(0);
|
||||
|
||||
$root = DB::table('agent_nodes')
|
||||
->where('admin_site_id', $siteId)
|
||||
->where('depth', 0)
|
||||
->first();
|
||||
|
||||
expect($root)->not->toBeNull();
|
||||
expect((string) $root->code)->toBe('line-alpha');
|
||||
|
||||
expect(
|
||||
DB::table('admin_user_agents')->where('agent_node_id', (int) $root->id)->count()
|
||||
)->toBe(1);
|
||||
});
|
||||
|
||||
test('non super admin cannot create integration site directly', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'line_ops',
|
||||
'name' => 'Ops',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
$roleId = DB::table('admin_roles')->insertGetId([
|
||||
'slug' => 'integration_ops',
|
||||
'code' => 'integration_ops',
|
||||
'name' => 'Integration Ops',
|
||||
'description' => null,
|
||||
'status' => 1,
|
||||
'is_system' => false,
|
||||
'sort_order' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$actionId = DB::table('admin_menu_actions')
|
||||
->where('permission_code', 'integration.site.manage')
|
||||
->value('id');
|
||||
if ($actionId !== null) {
|
||||
DB::table('admin_role_menu_actions')->insert([
|
||||
'role_id' => $roleId,
|
||||
'menu_action_id' => (int) $actionId,
|
||||
]);
|
||||
}
|
||||
|
||||
DB::table('admin_user_site_roles')->insert([
|
||||
'admin_user_id' => $admin->id,
|
||||
'site_id' => $siteId,
|
||||
'role_id' => $roleId,
|
||||
'granted_at' => now(),
|
||||
]);
|
||||
|
||||
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->postJson('/api/v1/admin/integration-sites', [
|
||||
'code' => 'blocked-site',
|
||||
'name' => 'Blocked',
|
||||
])
|
||||
->assertForbidden();
|
||||
});
|
||||
@@ -168,16 +168,16 @@ test('agent operator can create child under own node but not under sibling', fun
|
||||
]);
|
||||
grantSuperAdminRole($super);
|
||||
|
||||
$nodeA = $service->createChild($super, [
|
||||
$nodeA = $service->createChild($super, agentChildPayload([
|
||||
'parent_id' => $rootId,
|
||||
'code' => 'branch-a2',
|
||||
'name' => 'Branch A',
|
||||
]);
|
||||
$nodeB = $service->createChild($super, [
|
||||
]));
|
||||
$nodeB = $service->createChild($super, agentChildPayload([
|
||||
'parent_id' => $rootId,
|
||||
'code' => 'branch-b2',
|
||||
'name' => 'Branch B',
|
||||
]);
|
||||
]));
|
||||
|
||||
$operator = AdminUser::query()->create([
|
||||
'username' => 'agent_a2_ops',
|
||||
@@ -194,6 +194,7 @@ test('agent operator can create child under own node but not under sibling', fun
|
||||
'parent_id' => $nodeA->id,
|
||||
'code' => 'a-child',
|
||||
'name' => 'A Child',
|
||||
'password' => agentNodeTestPassword(),
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.code', 'a-child');
|
||||
@@ -203,6 +204,7 @@ test('agent operator can create child under own node but not under sibling', fun
|
||||
'parent_id' => $nodeB->id,
|
||||
'code' => 'hack-child',
|
||||
'name' => 'Hack',
|
||||
'password' => agentNodeTestPassword(),
|
||||
])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
33
tests/Feature/AdminAgentSettlementBillApiTest.php
Normal file
33
tests/Feature/AdminAgentSettlementBillApiTest.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('settlement bills index api resource is configured after migrations', function (): void {
|
||||
expect(
|
||||
DB::table('admin_api_resources')
|
||||
->where('route_name', 'api.v1.admin.settlement-bills.index')
|
||||
->where('status', 1)
|
||||
->exists(),
|
||||
)->toBeTrue();
|
||||
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'bill_super',
|
||||
'name' => 'Bill Super',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($admin);
|
||||
|
||||
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->getJson('/api/v1/admin/settlement-bills')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.items', fn ($items) => is_array($items));
|
||||
});
|
||||
@@ -63,6 +63,47 @@ function playerPermissionRequest($test, string $token)
|
||||
return $test->withHeader('Authorization', 'Bearer '.$token);
|
||||
}
|
||||
|
||||
test('super admin can create player under site root agent', function (): void {
|
||||
$siteCode = DB::table('admin_sites')->where('is_default', true)->value('code');
|
||||
$siteCode = is_string($siteCode) && $siteCode !== '' ? $siteCode : 'default_site';
|
||||
|
||||
$token = playerManageAdminToken();
|
||||
|
||||
$sitePlayerId = 'manual-create-'.uniqid('', true);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->postJson('/api/v1/admin/players', [
|
||||
'site_code' => $siteCode,
|
||||
'site_player_id' => $sitePlayerId,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
])
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.site_code', $siteCode)
|
||||
->assertJsonPath('data.site_player_id', $sitePlayerId);
|
||||
|
||||
$this->assertDatabaseHas('players', [
|
||||
'site_code' => $siteCode,
|
||||
'site_player_id' => $sitePlayerId,
|
||||
]);
|
||||
});
|
||||
|
||||
test('platform user without agent binding cannot create player', function (): void {
|
||||
$siteCode = DB::table('admin_sites')->where('is_default', true)->value('code');
|
||||
$siteCode = is_string($siteCode) && $siteCode !== '' ? $siteCode : 'default_site';
|
||||
|
||||
$token = playerPermissionAdminToken('player_manager_no_agent', ['prd.users.manage']);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->postJson('/api/v1/admin/players', [
|
||||
'site_code' => $siteCode,
|
||||
'site_player_id' => 'blocked-create',
|
||||
'default_currency' => 'NPR',
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('msg', fn (string $msg): bool => str_contains($msg, '代理') || str_contains($msg, 'agent'));
|
||||
});
|
||||
|
||||
test('admin can freeze and unfreeze player with audit log', function (): void {
|
||||
$siteCode = DB::table('admin_sites')->where('is_default', true)->value('code');
|
||||
$siteCode = is_string($siteCode) && $siteCode !== '' ? $siteCode : 'default_site';
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
use App\Models\AdminRole;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\AgentNode;
|
||||
use App\Lottery\ErrorCode;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use App\Support\AdminPermissionBridge;
|
||||
use App\Services\Agent\AgentNodeService;
|
||||
use App\Services\Agent\AgentAdminUserService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
@@ -47,7 +50,7 @@ test('admin user permission apis require rbac permission', function (): void {
|
||||
->assertJsonPath('code', ErrorCode::AdminForbidden->value);
|
||||
});
|
||||
|
||||
test('admin can list users and sync direct permissions', function (): void {
|
||||
test('admin can list platform users and read effective permissions', function (): void {
|
||||
$token = makeAdminWithPermissions('rbac_manager', ['prd.admin_user.manage']);
|
||||
|
||||
$target = AdminUser::query()->create([
|
||||
@@ -93,23 +96,12 @@ test('admin can list users and sync direct permissions', function (): void {
|
||||
->assertJsonPath('data.items.0.username', 'target_user')
|
||||
->assertJsonPath('data.items.0.roles.0', 'target_role');
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->putJson('/api/v1/admin/admin-users/'.$target->id.'/permissions', [
|
||||
'permission_slugs' => ['prd.report.view'],
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('code', ErrorCode::Success->value)
|
||||
->assertJsonFragment(['prd.report.view']);
|
||||
|
||||
expect($target->fresh()->directLegacyPermissionSlugs())->toContain('prd.report.view');
|
||||
|
||||
$list = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->getJson('/api/v1/admin/admin-users?keyword=target')
|
||||
->assertOk()
|
||||
->json('data.items.0.effective_permissions');
|
||||
|
||||
expect($list)->toContain('prd.draw_result.view');
|
||||
expect($list)->toContain('prd.report.view');
|
||||
});
|
||||
|
||||
test('admin can sync user roles for default site', function (): void {
|
||||
@@ -138,6 +130,101 @@ test('admin can sync user roles for default site', function (): void {
|
||||
expect($slugs)->toBe(['role_sync_a', 'role_sync_b']);
|
||||
});
|
||||
|
||||
test('platform admin users list excludes agent accounts', function (): void {
|
||||
$token = makeAdminWithPermissions('platform_list_manager', ['prd.admin_user.manage']);
|
||||
|
||||
$platformUser = AdminUser::query()->create([
|
||||
'username' => 'platform_only_user',
|
||||
'name' => 'Platform Only',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
$siteId = AdminUser::defaultAdminSiteId();
|
||||
$root = AgentNode::query()->where('admin_site_id', $siteId)->where('depth', 0)->firstOrFail();
|
||||
|
||||
$super = AdminUser::query()->create([
|
||||
'username' => 'agent_list_super',
|
||||
'name' => 'Super',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($super);
|
||||
|
||||
$branch = app(AgentNodeService::class)->createChild($super, [
|
||||
'parent_id' => (int) $root->id,
|
||||
'code' => 'list-branch',
|
||||
'name' => 'List Branch',
|
||||
]);
|
||||
|
||||
app(AgentAdminUserService::class)->createUnderAgent($branch, [
|
||||
'username' => 'agent_hidden_user',
|
||||
'nickname' => 'Agent Hidden',
|
||||
'password' => 'secret-strong-2',
|
||||
'role_ids' => [],
|
||||
]);
|
||||
|
||||
$items = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->getJson('/api/v1/admin/admin-users')
|
||||
->assertOk()
|
||||
->json('data.items');
|
||||
|
||||
expect(collect($items)->pluck('username')->all())
|
||||
->toContain('platform_only_user')
|
||||
->not->toContain('agent_hidden_user');
|
||||
});
|
||||
|
||||
test('platform account apis reject agent accounts and agent roles', function (): void {
|
||||
$token = makeAdminWithPermissions('platform_scope_manager', ['prd.admin_user.manage', 'prd.admin_role.manage']);
|
||||
|
||||
$siteId = AdminUser::defaultAdminSiteId();
|
||||
$root = AgentNode::query()->where('admin_site_id', $siteId)->where('depth', 0)->firstOrFail();
|
||||
|
||||
$super = AdminUser::query()->create([
|
||||
'username' => 'platform_guard_super',
|
||||
'name' => 'Super',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($super);
|
||||
|
||||
$branch = app(AgentNodeService::class)->createChild($super, [
|
||||
'parent_id' => (int) $root->id,
|
||||
'code' => 'platform-guard-branch',
|
||||
'name' => 'Platform Guard Branch',
|
||||
]);
|
||||
|
||||
$agentRole = AdminRole::query()->create([
|
||||
'slug' => 'guard_agent_role',
|
||||
'name' => 'Guard Agent Role',
|
||||
'scope_type' => AdminRole::SCOPE_AGENT,
|
||||
'owner_agent_id' => $branch->id,
|
||||
]);
|
||||
$agentRole->syncLegacyPermissionSlugs(['prd.agent.role.view']);
|
||||
|
||||
$agentUser = app(AgentAdminUserService::class)->createUnderAgent($branch, [
|
||||
'username' => 'guard_agent_user',
|
||||
'nickname' => 'Guard Agent User',
|
||||
'password' => 'secret-strong-3',
|
||||
'role_ids' => [(int) $agentRole->id],
|
||||
]);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->putJson('/api/v1/admin/admin-users/'.$agentUser->id.'/roles', [
|
||||
'role_slugs' => ['guard_agent_role'],
|
||||
])
|
||||
->assertStatus(422);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->putJson('/api/v1/admin/admin-roles/'.$agentRole->id.'/permissions', [
|
||||
'permission_slugs' => ['prd.admin_role.manage'],
|
||||
])
|
||||
->assertStatus(422);
|
||||
});
|
||||
|
||||
test('permission catalog groups permissions by admin navigation order', function (): void {
|
||||
$token = makeAdminWithPermissions('nav_group_catalog', ['prd.admin_user.manage', 'prd.admin_role.manage']);
|
||||
|
||||
|
||||
44
tests/Feature/AgentCreditSettlementExampleTest.php
Normal file
44
tests/Feature/AgentCreditSettlementExampleTest.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use App\Services\AgentSettlement\ShareSettlementCalculator;
|
||||
use App\Support\Settlement\DesignDocExample12;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('design doc example 12 actual share rates sum to 100 percent', function (): void {
|
||||
$rates = DesignDocExample12::actualShareRates();
|
||||
expect($rates['total'])->toEqual(100.0);
|
||||
expect($rates['actual']['C'])->toEqual(25.0);
|
||||
expect($rates['actual']['B'])->toEqual(15.0);
|
||||
expect($rates['actual']['A'])->toEqual(20.0);
|
||||
expect($rates['actual']['platform'])->toEqual(40.0);
|
||||
});
|
||||
|
||||
test('design doc example 12 share settlement matches section 12.3 and 12.4', function (): void {
|
||||
$calculator = new ShareSettlementCalculator;
|
||||
$result = $calculator->calculate(
|
||||
sharedNetWinLoss: DesignDocExample12::SHARED_NET_WIN_LOSS,
|
||||
totalSharesByCode: [
|
||||
'A' => DesignDocExample12::TOTAL_SHARE_A,
|
||||
'B' => DesignDocExample12::TOTAL_SHARE_B,
|
||||
'C' => DesignDocExample12::TOTAL_SHARE_C,
|
||||
],
|
||||
extraRebateByCode: ['C' => DesignDocExample12::EXTRA_REBATE_BY_C],
|
||||
gameWinLoss: DesignDocExample12::GAME_WIN_LOSS,
|
||||
basicRebate: DesignDocExample12::BASIC_REBATE,
|
||||
chainFromPlayer: ['C', 'B', 'A'],
|
||||
);
|
||||
|
||||
expect($result->playerNetSettlement)->toEqual((float) DesignDocExample12::PLAYER_NET_SETTLEMENT);
|
||||
expect($result->sharedNetWinLoss)->toEqual((float) DesignDocExample12::SHARED_NET_WIN_LOSS);
|
||||
expect($result->shareProfits['C'])->toEqual(DesignDocExample12::SHARE_PROFIT_C);
|
||||
expect($result->shareProfits['B'])->toEqual(DesignDocExample12::SHARE_PROFIT_B);
|
||||
expect($result->shareProfits['A'])->toEqual(DesignDocExample12::SHARE_PROFIT_A);
|
||||
expect($result->shareProfits['platform'])->toEqual(DesignDocExample12::SHARE_PROFIT_PLATFORM);
|
||||
expect($result->finalProfits['C'])->toEqual(DesignDocExample12::FINAL_PROFIT_C);
|
||||
expect($result->tierSettlements['P_to_C'])->toEqual(DesignDocExample12::TIER_P_TO_C);
|
||||
expect($result->tierSettlements['C_to_B'])->toEqual(DesignDocExample12::TIER_C_TO_B);
|
||||
expect($result->tierSettlements['B_to_A'])->toEqual(DesignDocExample12::TIER_B_TO_A);
|
||||
expect($result->tierSettlements['A_to_platform'])->toEqual(DesignDocExample12::TIER_A_TO_PLATFORM);
|
||||
});
|
||||
Reference in New Issue
Block a user