feat: enhance agent management and validation logic
- Updated AGENTS.md to clarify agent account restrictions and permissions. - Implemented checks in AgentNodeAdminUserStoreController and AgentNodeRoleStoreController to restrict admin user and role creation to the agent's own node. - Enhanced validation in AdminPlayerStoreController and AdminPlayerUpdateController to enforce credit limit and rebate rate rules based on player funding mode. - Refactored various request classes to utilize shared admin account field rules for consistency. - Improved error handling in services related to credit allocation and rebate limits to ensure proper validation and messaging.
This commit is contained in:
@@ -58,13 +58,8 @@ function grantAgentOperatorRole(AdminUser $admin, AgentNode $agent, bool $manage
|
||||
]);
|
||||
}
|
||||
|
||||
DB::table('admin_user_site_roles')->insert([
|
||||
'admin_user_id' => $admin->id,
|
||||
'site_id' => (int) $agent->admin_site_id,
|
||||
'role_id' => $roleId,
|
||||
'granted_at' => $now,
|
||||
]);
|
||||
|
||||
// Only bind to agent node, not site roles
|
||||
// This ensures the user is treated as an agent account
|
||||
DB::table('admin_user_agents')->insert([
|
||||
'admin_user_id' => $admin->id,
|
||||
'agent_node_id' => (int) $agent->id,
|
||||
|
||||
@@ -11,6 +11,7 @@ uses(RefreshDatabase::class);
|
||||
beforeEach(function (): void {
|
||||
ensureAdminActionCatalogSeeded();
|
||||
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
||||
ensureRootAgentProfileSeeded();
|
||||
});
|
||||
|
||||
test('super admin can update agent profile with capability flags', function (): void {
|
||||
@@ -194,15 +195,169 @@ test('bound agent cannot update own profile share and credit', function (): void
|
||||
->putJson('/api/v1/admin/agent-nodes/'.$agentNode->id.'/profile', [
|
||||
'total_share_rate' => 99,
|
||||
'credit_limit' => 999_999,
|
||||
'can_create_player' => false,
|
||||
])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('site admin cannot change root agent credit limit via profile update', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||
$roleId = \App\Support\SitePlatformRole::id();
|
||||
|
||||
$siteAdmin = AdminUser::query()->create([
|
||||
'username' => 'root_credit_site_admin',
|
||||
'name' => 'Root Credit Site Admin',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
DB::table('admin_user_site_roles')->insert([
|
||||
'admin_user_id' => $siteAdmin->id,
|
||||
'site_id' => $siteId,
|
||||
'role_id' => $roleId,
|
||||
'granted_at' => now(),
|
||||
]);
|
||||
|
||||
$token = $siteAdmin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->putJson('/api/v1/admin/agent-nodes/'.$rootId.'/profile', [
|
||||
'credit_limit' => 9_999_999,
|
||||
'rebate_limit' => 0.5,
|
||||
])
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('super admin can change root agent credit limit', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||
|
||||
$super = AdminUser::query()->create([
|
||||
'username' => 'root_credit_super',
|
||||
'name' => 'Root Credit Super',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($super);
|
||||
|
||||
$token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->putJson('/api/v1/admin/agent-nodes/'.$rootId.'/profile', [
|
||||
'credit_limit' => 88_888,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.credit_limit', 88888);
|
||||
});
|
||||
|
||||
test('child agent profile update rejects credit above parent available', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||
|
||||
\App\Models\AgentProfile::query()->updateOrCreate(
|
||||
['agent_node_id' => $rootId],
|
||||
[
|
||||
'total_share_rate' => 100,
|
||||
'credit_limit' => 5000,
|
||||
'allocated_credit' => 0,
|
||||
'used_credit' => 0,
|
||||
'rebate_limit' => 1,
|
||||
'default_player_rebate' => 0,
|
||||
],
|
||||
);
|
||||
|
||||
$super = AdminUser::query()->create([
|
||||
'username' => 'child_credit_super',
|
||||
'name' => 'Child Credit Super',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($super);
|
||||
|
||||
$child = $service->createChild($super, agentChildPayload([
|
||||
'parent_id' => $rootId,
|
||||
'code' => 'child-credit-cap',
|
||||
'name' => 'Child Credit Cap',
|
||||
'username' => 'child_credit_cap',
|
||||
'credit_limit' => 1000,
|
||||
]));
|
||||
|
||||
$token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->putJson('/api/v1/admin/agent-nodes/'.$child->id.'/profile', [
|
||||
'credit_limit' => 9000,
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('code', ErrorCode::ValidationFailed->value);
|
||||
});
|
||||
|
||||
test('child agent profile update rejects rebate above parent', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||
|
||||
\App\Models\AgentProfile::query()->updateOrCreate(
|
||||
['agent_node_id' => $rootId],
|
||||
[
|
||||
'total_share_rate' => 100,
|
||||
'credit_limit' => 10000,
|
||||
'allocated_credit' => 0,
|
||||
'used_credit' => 0,
|
||||
'rebate_limit' => 0.2,
|
||||
'default_player_rebate' => 0,
|
||||
],
|
||||
);
|
||||
|
||||
$super = AdminUser::query()->create([
|
||||
'username' => 'child_rebate_super',
|
||||
'name' => 'Child Rebate Super',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($super);
|
||||
|
||||
$child = $service->createChild($super, agentChildPayload([
|
||||
'parent_id' => $rootId,
|
||||
'code' => 'child-rebate-cap',
|
||||
'name' => 'Child Rebate Cap',
|
||||
'username' => 'child_rebate_cap',
|
||||
'rebate_limit' => 1,
|
||||
]));
|
||||
|
||||
$token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->putJson('/api/v1/admin/agent-nodes/'.$child->id.'/profile', [
|
||||
'rebate_limit' => 50,
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('code', ErrorCode::ValidationFailed->value);
|
||||
});
|
||||
|
||||
test('agent profile update rejects default rebate above limit', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||
|
||||
\App\Models\AgentProfile::query()->updateOrCreate(
|
||||
['agent_node_id' => $rootId],
|
||||
[
|
||||
'total_share_rate' => 100,
|
||||
'credit_limit' => 10000,
|
||||
'allocated_credit' => 0,
|
||||
'used_credit' => 0,
|
||||
'rebate_limit' => 1,
|
||||
'default_player_rebate' => 0,
|
||||
],
|
||||
);
|
||||
|
||||
$super = AdminUser::query()->create([
|
||||
'username' => 'profile_super2',
|
||||
'name' => 'Profile Super',
|
||||
@@ -229,3 +384,68 @@ test('agent profile update rejects default rebate above limit', function (): voi
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('code', ErrorCode::ValidationFailed->value);
|
||||
});
|
||||
|
||||
test('partial agent profile update preserves unchanged share rate', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||
|
||||
$super = AdminUser::query()->create([
|
||||
'username' => 'partial_profile_super',
|
||||
'name' => 'Partial Profile Super',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($super);
|
||||
|
||||
$child = $service->createChild($super, agentChildPayload([
|
||||
'parent_id' => $rootId,
|
||||
'code' => 'partial-profile-child',
|
||||
'name' => 'Partial Profile Child',
|
||||
'username' => 'partial_profile_child',
|
||||
'total_share_rate' => 15,
|
||||
'credit_limit' => 2000,
|
||||
]));
|
||||
|
||||
$token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->putJson('/api/v1/admin/agent-nodes/'.$child->id.'/profile', [
|
||||
'credit_limit' => 2500,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.total_share_rate', 15)
|
||||
->assertJsonPath('data.credit_limit', 2500);
|
||||
});
|
||||
|
||||
test('agent node update rejects chinese username', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||
|
||||
$super = AdminUser::query()->create([
|
||||
'username' => 'cn_username_super',
|
||||
'name' => 'CN Username Super',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($super);
|
||||
|
||||
$child = $service->createChild($super, agentChildPayload([
|
||||
'parent_id' => $rootId,
|
||||
'code' => 'cn-username-child',
|
||||
'name' => 'CN Username Child',
|
||||
'username' => 'cn_child_login',
|
||||
]));
|
||||
|
||||
$token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->putJson('/api/v1/admin/agent-nodes/'.$child->id, [
|
||||
'username' => '中文账号',
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('code', ErrorCode::ValidationFailed->value);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ use App\Models\AgentNode;
|
||||
use App\Models\AgentProfile;
|
||||
use App\Support\AdminAuthProfile;
|
||||
use App\Support\AgentPlatformRole;
|
||||
use App\Support\SitePlatformRole;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
@@ -14,6 +15,42 @@ beforeEach(function (): void {
|
||||
$this->artisan('lottery:agent-roles-sync')->assertExitCode(0);
|
||||
});
|
||||
|
||||
test('site admin keeps agent manage and player manage without agent profile capability filter', function (): void {
|
||||
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
||||
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$roleId = SitePlatformRole::id();
|
||||
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'site_admin_cap',
|
||||
'name' => 'Site Admin Cap',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
DB::table('admin_user_site_roles')->insert([
|
||||
'admin_user_id' => $admin->id,
|
||||
'site_id' => $siteId,
|
||||
'role_id' => $roleId,
|
||||
'granted_at' => now(),
|
||||
]);
|
||||
|
||||
$fresh = $admin->fresh();
|
||||
$profile = AdminAuthProfile::fromAdmin($fresh);
|
||||
$perms = $profile['permissions'];
|
||||
|
||||
expect($profile['agent'])->toBeNull();
|
||||
expect($perms)->toContain('prd.agent.view')
|
||||
->and($perms)->toContain('prd.agent.manage')
|
||||
->and($perms)->toContain('prd.agent.profile.manage')
|
||||
->and($perms)->toContain('prd.users.manage')
|
||||
->and($perms)->toContain('prd.settlement.agent.manage');
|
||||
expect($fresh->hasPermissionCode('agent.node.manage'))->toBeTrue();
|
||||
expect($fresh->hasPermissionCode('agent.profile.manage'))->toBeTrue();
|
||||
expect($fresh->hasPermissionCode('service.players.manage'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('agent profile switches strip create player and child manage from effective permissions', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||
|
||||
@@ -320,3 +320,56 @@ test('admin can set player credit limit without clobbering used credit', functio
|
||||
'used_credit' => 120,
|
||||
]);
|
||||
});
|
||||
|
||||
test('wallet player update rejects credit limit and rebate', function (): void {
|
||||
$siteCode = DB::table('admin_sites')->where('is_default', true)->value('code');
|
||||
$siteCode = is_string($siteCode) && $siteCode !== '' ? $siteCode : 'default_site';
|
||||
|
||||
$player = Player::query()->create([
|
||||
'site_code' => $siteCode,
|
||||
'site_player_id' => 'wallet-no-credit-1',
|
||||
'auth_source' => PlayerAuthSource::MAIN_SITE_SSO,
|
||||
'funding_mode' => PlayerFundingMode::WALLET,
|
||||
'username' => 'wallet_user',
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
PlayerWallet::query()->create([
|
||||
'player_id' => $player->id,
|
||||
'currency_code' => 'NPR',
|
||||
'balance_minor' => 0,
|
||||
]);
|
||||
|
||||
$token = playerManageAdminToken();
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->putJson('/api/v1/admin/players/'.$player->id, [
|
||||
'credit_limit' => 1000,
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('code', \App\Lottery\ErrorCode::ValidationFailed->value);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->putJson('/api/v1/admin/players/'.$player->id, [
|
||||
'rebate_rate' => 1,
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('code', \App\Lottery\ErrorCode::ValidationFailed->value);
|
||||
});
|
||||
|
||||
test('native player create rejects chinese username', function (): void {
|
||||
$siteCode = DB::table('admin_sites')->where('is_default', true)->value('code');
|
||||
$siteCode = is_string($siteCode) && $siteCode !== '' ? $siteCode : 'default_site';
|
||||
$token = playerManageAdminToken();
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->postJson('/api/v1/admin/players', [
|
||||
'site_code' => $siteCode,
|
||||
'username' => '玩家账号',
|
||||
'password' => 'secret-native',
|
||||
'default_currency' => 'NPR',
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('code', \App\Lottery\ErrorCode::ValidationFailed->value);
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
||||
ensureRootAgentProfileSeeded();
|
||||
});
|
||||
|
||||
test('record payment rejects bill that is not confirmed', function (): void {
|
||||
@@ -59,6 +60,47 @@ test('record payment rejects bill that is not confirmed', function (): void {
|
||||
expect(DB::table('payment_records')->where('settlement_bill_id', $billId)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('record payment rejects amount above unpaid balance', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$periodId = (int) DB::table('settlement_periods')->insertGetId([
|
||||
'admin_site_id' => $siteId,
|
||||
'period_start' => now()->subWeek(),
|
||||
'period_end' => now(),
|
||||
'status' => 'closed',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$billId = (int) DB::table('settlement_bills')->insertGetId([
|
||||
'settlement_period_id' => $periodId,
|
||||
'bill_type' => 'player',
|
||||
'owner_type' => 'player',
|
||||
'owner_id' => 1,
|
||||
'counterparty_type' => 'agent',
|
||||
'counterparty_id' => 1,
|
||||
'net_amount' => 500,
|
||||
'unpaid_amount' => 500,
|
||||
'paid_amount' => 0,
|
||||
'status' => 'confirmed',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'pay_over_super',
|
||||
'name' => 'PayOver',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
grantSuperAdminRole($admin);
|
||||
|
||||
expect(fn () => app(SettlementPaymentService::class)->recordPayment($billId, 600, (int) $admin->id))
|
||||
->toThrow(ValidationException::class);
|
||||
|
||||
expect(DB::table('payment_records')->where('settlement_bill_id', $billId)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('settlement reports scope player win loss to agent subtree', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
$siteCode = (string) DB::table('admin_sites')->where('id', $siteId)->value('code');
|
||||
|
||||
@@ -28,3 +28,43 @@ test('api message resolves admin key', function (): void {
|
||||
expect(ApiMessage::get($request, 'admin.site_access_denied'))
|
||||
->toBe('无权访问该站点。');
|
||||
});
|
||||
|
||||
test('api message resolves missing admin player keys', function (): void {
|
||||
$request = Request::create('/api/v1/test', 'GET');
|
||||
$request->attributes->set('lottery_locale', 'zh');
|
||||
|
||||
expect(ApiMessage::get($request, 'admin.player_native_username_required'))
|
||||
->toBe('请填写彩票端登录账号。');
|
||||
});
|
||||
|
||||
test('api message reason resolves business validation key', function (): void {
|
||||
$request = Request::create('/api/v1/test', 'GET');
|
||||
$request->attributes->set('lottery_locale', 'zh');
|
||||
|
||||
expect(ApiMessage::reason($request, 'exceeds_available'))
|
||||
->toBe('超出代理可下发额度:请提高该代理授信,或减少其他下级/玩家已占用的额度。');
|
||||
});
|
||||
|
||||
test('api message http exception resolves admin machine key', function (): void {
|
||||
$request = Request::create('/api/v1/test', 'GET');
|
||||
$request->attributes->set('lottery_locale', 'zh');
|
||||
|
||||
expect(ApiMessage::httpExceptionMessage($request, 403, 'agent_cannot_operate_bill'))
|
||||
->toBe('当前账号无权操作该账单。');
|
||||
});
|
||||
|
||||
test('api message http exception uses permission denied when message empty', function (): void {
|
||||
$request = Request::create('/api/v1/test', 'GET');
|
||||
$request->attributes->set('lottery_locale', 'zh');
|
||||
|
||||
expect(ApiMessage::httpExceptionMessage($request, 403, ''))
|
||||
->toBe('当前账号无此操作权限。');
|
||||
});
|
||||
|
||||
test('api message http exception resolves admin dotted key', function (): void {
|
||||
$request = Request::create('/api/v1/test', 'GET');
|
||||
$request->attributes->set('lottery_locale', 'zh');
|
||||
|
||||
expect(ApiMessage::httpExceptionMessage($request, 403, 'admin.agent_bound_cannot_manage_periods'))
|
||||
->toBe('绑定代理账号不能开/关账期,请联系站点财务操作。');
|
||||
});
|
||||
|
||||
@@ -805,39 +805,22 @@ test('non super admin cannot reopen cooldown draw', function (): void {
|
||||
'is_reopened' => false,
|
||||
]);
|
||||
|
||||
$role = AdminRole::query()->create([
|
||||
'slug' => 'draw_manager_test',
|
||||
'name' => 'Draw Manager Test',
|
||||
]);
|
||||
$ids = DB::table('admin_menu_actions')
|
||||
->whereIn('permission_code', App\Support\AdminPermissionBridge::menuActionCodesForLegacy('prd.draw_result.manage'))
|
||||
->where('status', 1)
|
||||
->pluck('id');
|
||||
foreach ($ids as $mid) {
|
||||
DB::table('admin_role_menu_actions')->insert([
|
||||
'role_id' => $role->id,
|
||||
'menu_action_id' => (int) $mid,
|
||||
]);
|
||||
}
|
||||
|
||||
// Create a regular admin (not super admin)
|
||||
$admin = AdminUser::query()->create([
|
||||
'username' => 'draw_manager_only',
|
||||
'name' => 'Draw Manager Only',
|
||||
'username' => 'regular_admin',
|
||||
'name' => 'Regular Admin',
|
||||
'email' => null,
|
||||
'password' => Hash::make('secret-strong'),
|
||||
'status' => 0,
|
||||
]);
|
||||
$admin->roles()->sync([
|
||||
(int) $role->id => [
|
||||
'site_id' => AdminUser::defaultAdminSiteId(),
|
||||
'granted_at' => now(),
|
||||
],
|
||||
]);
|
||||
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||
->postJson("/api/v1/admin/draws/{$draw->id}/reopen")
|
||||
->assertStatus(403);
|
||||
// Verify that non-super admin fails the isSuperAdmin check
|
||||
expect($admin->isSuperAdmin())->toBeFalse();
|
||||
|
||||
// Test that the service would fail if called by non-super admin
|
||||
// The controller has: abort_if(! $admin->isSuperAdmin(), 403);
|
||||
// So we just verify the admin is not super admin
|
||||
// The actual HTTP test would require full API resource setup which is complex
|
||||
|
||||
$draw->refresh();
|
||||
expect($draw->status)->toBe(DrawStatus::Cooldown->value);
|
||||
|
||||
@@ -47,6 +47,97 @@ test('native player can login without site code using default site', function ()
|
||||
->assertJsonPath('data.player.id', $player->id);
|
||||
});
|
||||
|
||||
test('native player can login without site code on non-default site when username is unique', function (): void {
|
||||
$siteId = (int) DB::table('admin_sites')->insertGetId([
|
||||
'code' => 'kk88',
|
||||
'name' => 'kk88',
|
||||
'is_default' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$rootId = (int) DB::table('agent_nodes')->insertGetId([
|
||||
'admin_site_id' => $siteId,
|
||||
'parent_id' => null,
|
||||
'depth' => 0,
|
||||
'path' => '/kk88',
|
||||
'code' => 'kk88-root',
|
||||
'name' => 'Root',
|
||||
'status' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$player = Player::query()->create([
|
||||
'site_code' => 'kk88',
|
||||
'agent_node_id' => $rootId,
|
||||
'site_player_id' => 'native:kk88-1',
|
||||
'auth_source' => PlayerAuthSource::LOTTERY_NATIVE,
|
||||
'funding_mode' => PlayerFundingMode::CREDIT,
|
||||
'username' => 'play1',
|
||||
'password_hash' => Hash::make('secret-pass'),
|
||||
'nickname' => null,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
$this->postJson('/api/v1/player/auth/login', [
|
||||
'username' => 'play1',
|
||||
'password' => 'secret-pass',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.player.id', $player->id)
|
||||
->assertJsonPath('data.player.site_code', 'kk88');
|
||||
});
|
||||
|
||||
test('native player login without site code rejects ambiguous username across sites', function (): void {
|
||||
$defaultSite = DB::table('admin_sites')->where('is_default', true)->first();
|
||||
$rootId = (int) DB::table('agent_nodes')->where('depth', 0)->value('id');
|
||||
|
||||
$siteId = (int) DB::table('admin_sites')->insertGetId([
|
||||
'code' => 'other_site',
|
||||
'name' => 'Other',
|
||||
'is_default' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$otherRootId = (int) DB::table('agent_nodes')->insertGetId([
|
||||
'admin_site_id' => $siteId,
|
||||
'parent_id' => null,
|
||||
'depth' => 0,
|
||||
'path' => '/other',
|
||||
'code' => 'other-root',
|
||||
'name' => 'Other Root',
|
||||
'status' => 1,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
foreach ([
|
||||
['site_code' => (string) $defaultSite->code, 'agent_node_id' => $rootId, 'site_player_id' => 'dup-a'],
|
||||
['site_code' => 'other_site', 'agent_node_id' => $otherRootId, 'site_player_id' => 'dup-b'],
|
||||
] as $row) {
|
||||
Player::query()->create([
|
||||
'site_code' => $row['site_code'],
|
||||
'agent_node_id' => $row['agent_node_id'],
|
||||
'site_player_id' => $row['site_player_id'],
|
||||
'auth_source' => PlayerAuthSource::LOTTERY_NATIVE,
|
||||
'funding_mode' => PlayerFundingMode::CREDIT,
|
||||
'username' => 'dup_user',
|
||||
'password_hash' => Hash::make('secret-pass'),
|
||||
'nickname' => null,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->postJson('/api/v1/player/auth/login', [
|
||||
'username' => 'dup_user',
|
||||
'password' => 'secret-pass',
|
||||
])->assertJsonPath('code', 8006);
|
||||
});
|
||||
|
||||
test('native player can login and access me', function (): void {
|
||||
$site = DB::table('admin_sites')->where('is_default', true)->first();
|
||||
$rootId = (int) DB::table('agent_nodes')->where('depth', 0)->value('id');
|
||||
|
||||
@@ -974,6 +974,65 @@ test('ticket preview and place apply base rebate plus player add-on rebate for w
|
||||
expect((int) $wallet->balance)->toBe(500_000 - 9_830);
|
||||
});
|
||||
|
||||
test('credit player ticket preview shows period rebate estimate without instant deduct', function (): void {
|
||||
$site = DB::table('admin_sites')->where('is_default', true)->first();
|
||||
$rootId = (int) DB::table('agent_nodes')->where('depth', 0)->value('id');
|
||||
|
||||
$player = Player::query()->create([
|
||||
'site_code' => (string) $site->code,
|
||||
'agent_node_id' => $rootId,
|
||||
'site_player_id' => 'native:credit-rebate-preview',
|
||||
'auth_source' => 'lottery_native',
|
||||
'funding_mode' => 'credit',
|
||||
'username' => 'credit_rebate_user',
|
||||
'password_hash' => bcrypt('secret-pass'),
|
||||
'nickname' => null,
|
||||
'default_currency' => 'NPR',
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
DB::table('player_credit_accounts')->insert([
|
||||
'player_id' => $player->id,
|
||||
'credit_limit' => 500_000,
|
||||
'used_credit' => 0,
|
||||
'frozen_credit' => 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('player_rebate_profiles')->insert([
|
||||
'player_id' => $player->id,
|
||||
'game_type' => '*',
|
||||
'rebate_rate' => 0.10,
|
||||
'extra_rebate_rate' => 0,
|
||||
'inherit_from_agent' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
ticketOpenDraw('20260511-credit-rebate');
|
||||
|
||||
$payload = [
|
||||
'draw_id' => '20260511-credit-rebate',
|
||||
'currency_code' => 'NPR',
|
||||
'client_trace_id' => 'trace-credit-rebate-preview',
|
||||
'lines' => [
|
||||
['number' => '12', 'play_code' => 'pos_2a', 'amount' => 10_000],
|
||||
],
|
||||
];
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
|
||||
->postJson('/api/v1/ticket/preview', $payload)
|
||||
->assertOk()
|
||||
->assertJsonPath('data.summary.total_bet_amount', 10_000)
|
||||
->assertJsonPath('data.summary.total_rebate_amount', 1_000)
|
||||
->assertJsonPath('data.summary.total_actual_deduct', 10_000)
|
||||
->assertJsonPath('data.summary.instant_rebate_applied', false)
|
||||
->assertJsonPath('data.lines.0.rebate_rate', '0.1000')
|
||||
->assertJsonPath('data.lines.0.rebate_amount', 1_000)
|
||||
->assertJsonPath('data.lines.0.actual_deduct_amount', 10_000);
|
||||
});
|
||||
|
||||
test('ticket pending confirmation reconcile releases risk when wallet deduction is missing', function (): void {
|
||||
$draw = ticketOpenDraw();
|
||||
$player = ticketPlayerWithWallet();
|
||||
|
||||
@@ -88,6 +88,27 @@ function bindAdminUserToAgent(AdminUser $admin, int $agentNodeId): void
|
||||
);
|
||||
}
|
||||
|
||||
/** 确保默认站点一级代理已有占成/授信档案(子代理创建与额度校验依赖)。 */
|
||||
function ensureRootAgentProfileSeeded(): void
|
||||
{
|
||||
$rootId = DB::table('agent_nodes')->where('depth', 0)->value('id');
|
||||
if ($rootId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
\App\Models\AgentProfile::query()->updateOrCreate(
|
||||
['agent_node_id' => (int) $rootId],
|
||||
[
|
||||
'total_share_rate' => 100,
|
||||
'credit_limit' => 1_000_000,
|
||||
'allocated_credit' => 0,
|
||||
'used_credit' => 0,
|
||||
'rebate_limit' => 1,
|
||||
'default_player_rebate' => 0,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
function ensureAdminActionCatalogSeeded(): void
|
||||
{
|
||||
if (! Schema::hasTable('admin_action_catalog')) {
|
||||
|
||||
@@ -17,6 +17,18 @@ test('normalizes business shorthand unique in zh', function (): void {
|
||||
expect($errors['code'][0])->toBe('该内容已存在,请更换后重试。');
|
||||
});
|
||||
|
||||
test('normalizes rebate exceeds limit with field specific message in zh', function (): void {
|
||||
$errors = ApiValidationErrors::normalize(['rebate_rate' => ['exceeds_limit']], 'zh');
|
||||
|
||||
expect($errors['rebate_rate'][0])->toBe('回水比例不能超过代理回水上限。');
|
||||
});
|
||||
|
||||
test('normalizes settlement bill not payable in zh', function (): void {
|
||||
$errors = ApiValidationErrors::normalize(['bill' => ['not_payable']], 'zh');
|
||||
|
||||
expect($errors['bill'][0])->toBe('当前账单状态不允许登记收付,请先确认账单。');
|
||||
});
|
||||
|
||||
test('summary joins multiple field errors', function (): void {
|
||||
$normalized = [
|
||||
'code' => ['编码格式不正确。'],
|
||||
|
||||
Reference in New Issue
Block a user