From e14b7b456908fa8c545858690a0bf3f62f47678d Mon Sep 17 00:00:00 2001 From: kang Date: Thu, 11 Jun 2026 18:01:58 +0800 Subject: [PATCH] feat: add AgentNodeIndexController for node listing and remove settlement_cycle field from AgentProfile logic --- .../Admin/Agent/AgentNodeIndexController.php | 38 ++++++++ .../AdminIntegrationSiteStoreController.php | 6 ++ .../Player/AdminPlayerStoreController.php | 42 +++++---- .../Player/AdminPlayerUpdateController.php | 28 +++--- .../AdminIntegrationSiteStoreRequest.php | 5 ++ .../Admin/AdminPlayerStoreRequest.php | 4 +- .../Admin/AdminPlayerUpdateRequest.php | 8 +- .../Admin/Concerns/AgentProfileFieldRules.php | 11 +-- app/Models/AgentProfile.php | 1 - .../Admin/AgentDashboardOverviewBuilder.php | 1 - app/Services/Agent/AgentNodeService.php | 2 - app/Services/Agent/AgentProfileService.php | 5 -- .../Agent/AgentSiteProvisioningService.php | 1 - app/Services/Agent/RebateLimitValidator.php | 1 + .../BetSettlementSnapshotBuilder.php | 2 +- .../Integration/IntegrationSiteService.php | 89 ++++++++++++++++++- .../Player/PlayerRebateProfileService.php | 10 +-- app/Support/AdminAuthorizationRegistry.php | 15 ++-- app/Support/AgentNodePresenter.php | 27 +++++- app/Support/PlayerApiPresenter.php | 14 +-- ...000_agent_credit_and_settlement_tables.php | 8 +- ...000_seed_agent_node_index_api_resource.php | 89 +++++++++++++++++++ routes/api/v1/admin/agent.php | 3 + tests/Feature/AdminAgentProfileApiTest.php | 1 - ...inAgentProfileCapabilityPermissionTest.php | 2 - tests/Feature/AdminIntegrationSiteApiTest.php | 57 ++++++++++++ tests/Feature/AdminPlayerManageApiTest.php | 1 - tests/Feature/AgentCreditAllocationTest.php | 1 - .../AgentSettlementPeriodCloseP0FixesTest.php | 1 - .../BetShareSnapshotImmutabilityTest.php | 1 - 30 files changed, 383 insertions(+), 91 deletions(-) create mode 100644 app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeIndexController.php create mode 100644 database/migrations/2026_06_11_120000_seed_agent_node_index_api_resource.php diff --git a/app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeIndexController.php b/app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeIndexController.php new file mode 100644 index 0000000..4d1282a --- /dev/null +++ b/app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeIndexController.php @@ -0,0 +1,38 @@ +lotteryAdmin(); + abort_if($admin === null, 401); + + $query = AgentNode::query()->orderBy('admin_site_id')->orderBy('path'); + + if (! $admin->isSuperAdmin()) { + $actor = AdminAgentScope::primaryAgentNode($admin); + if ($actor === null) { + $query->whereRaw('0 = 1'); + } else { + $query + ->where('admin_site_id', (int) $actor->admin_site_id) + ->where('path', 'like', $actor->path.'%'); + } + } + + return ApiResponse::success([ + 'admin_site_id' => null, + 'items' => AgentNodePresenter::list($query->get()), + ]); + } +} diff --git a/app/Http/Controllers/Api/V1/Admin/Integration/AdminIntegrationSiteStoreController.php b/app/Http/Controllers/Api/V1/Admin/Integration/AdminIntegrationSiteStoreController.php index 37b1d2d..3186b8a 100644 --- a/app/Http/Controllers/Api/V1/Admin/Integration/AdminIntegrationSiteStoreController.php +++ b/app/Http/Controllers/Api/V1/Admin/Integration/AdminIntegrationSiteStoreController.php @@ -27,6 +27,12 @@ final class AdminIntegrationSiteStoreController extends Controller AdminIntegrationSitePresenter::detail($site), $result['secrets'], ); + $payload['admin_user'] = [ + 'id' => (int) $result['admin_user']->id, + 'username' => (string) $result['admin_user']->username, + 'nickname' => (string) $result['admin_user']->name, + 'email' => $result['admin_user']->email, + ]; AuditLogger::recordForAdmin( $admin, diff --git a/app/Http/Controllers/Api/V1/Admin/Player/AdminPlayerStoreController.php b/app/Http/Controllers/Api/V1/Admin/Player/AdminPlayerStoreController.php index 897d895..bcfa195 100644 --- a/app/Http/Controllers/Api/V1/Admin/Player/AdminPlayerStoreController.php +++ b/app/Http/Controllers/Api/V1/Admin/Player/AdminPlayerStoreController.php @@ -3,23 +3,25 @@ namespace App\Http\Controllers\Api\V1\Admin\Player; use App\Models\Player; +use App\Models\AdminSite; +use App\Models\AgentNode; use App\Lottery\ErrorCode; use App\Support\ApiMessage; use App\Support\ApiResponse; -use Illuminate\Http\JsonResponse; -use App\Support\AdminAgentScope; use App\Support\AdminSiteScope; +use App\Support\AdminAgentScope; +use App\Support\PlayerAuthSource; +use Illuminate\Http\JsonResponse; +use App\Support\PlayerFundingMode; +use Illuminate\Support\Facades\DB; use App\Support\PlayerApiPresenter; use App\Http\Controllers\Controller; -use App\Http\Requests\Admin\AdminPlayerStoreRequest; -use App\Models\AgentNode; +use Illuminate\Support\Facades\Hash; use App\Services\Agent\AgentProfileService; use App\Services\Agent\RebateLimitValidator; use App\Services\Player\PlayerCreditService; -use App\Support\PlayerAuthSource; -use App\Support\PlayerFundingMode; -use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Str; +use Illuminate\Validation\ValidationException; +use App\Http\Requests\Admin\AdminPlayerStoreRequest; /** POST /api/v1/admin/players */ final class AdminPlayerStoreController extends Controller @@ -35,7 +37,7 @@ final class AdminPlayerStoreController extends Controller try { $agentProfileService->assertActorMayCreatePlayer($admin); - } catch (\Illuminate\Validation\ValidationException $e) { + } catch (ValidationException $e) { return ApiMessage::errorResponse( $request, 'admin.player_create_capability_forbidden', @@ -110,11 +112,15 @@ final class AdminPlayerStoreController extends Controller } $agent = AgentNode::query()->findOrFail($agentNodeId); + $rebateRate = 0.0; + $extraRebateRate = 0.0; if ($request->has('rebate_rate')) { + $rebateRate = (float) $request->input('rebate_rate', 0) / 100; + $extraRebateRate = (float) $request->input('extra_rebate_rate', 0) / 100; $rebateLimitValidator->assertPlayerRebateWithinAgent( $agent, - (float) $request->input('rebate_rate', 0), - (float) $request->input('extra_rebate_rate', 0), + $rebateRate, + $extraRebateRate, ); } @@ -122,7 +128,7 @@ final class AdminPlayerStoreController extends Controller ? (int) $request->input('credit_limit', 0) : ($isNative ? 0 : 0); - $player = \Illuminate\Support\Facades\DB::transaction(function () use ( + $player = DB::transaction(function () use ( $agent, $agentProfileService, $playerCreditService, @@ -132,6 +138,8 @@ final class AdminPlayerStoreController extends Controller $agentNodeId, $sitePlayerId, $creditLimit, + $rebateRate, + $extraRebateRate, ): Player { $agentProfileService->assertMayIncreasePlayerCredit($agent, $creditLimit); @@ -157,12 +165,12 @@ final class AdminPlayerStoreController extends Controller $agentProfileService->refreshAllocatedCredit($agent); if ($request->has('rebate_rate')) { - \Illuminate\Support\Facades\DB::table('player_rebate_profiles')->insert([ + DB::table('player_rebate_profiles')->insert([ 'player_id' => $player->id, 'game_type' => '*', 'inherit_from_agent' => false, - 'rebate_rate' => (float) $request->input('rebate_rate', 0), - 'extra_rebate_rate' => (float) $request->input('extra_rebate_rate', 0), + 'rebate_rate' => $rebateRate, + 'extra_rebate_rate' => $extraRebateRate, 'created_at' => now(), 'updated_at' => now(), ]); @@ -180,12 +188,12 @@ final class AdminPlayerStoreController extends Controller return (int) $requested; } - $siteId = \App\Models\AdminSite::query()->where('code', $siteCode)->value('id'); + $siteId = AdminSite::query()->where('code', $siteCode)->value('id'); if ($siteId === null) { return null; } - $rootId = \App\Models\AgentNode::query() + $rootId = AgentNode::query() ->where('admin_site_id', (int) $siteId) ->where('depth', 0) ->value('id'); diff --git a/app/Http/Controllers/Api/V1/Admin/Player/AdminPlayerUpdateController.php b/app/Http/Controllers/Api/V1/Admin/Player/AdminPlayerUpdateController.php index 52a9cd9..f486c7a 100644 --- a/app/Http/Controllers/Api/V1/Admin/Player/AdminPlayerUpdateController.php +++ b/app/Http/Controllers/Api/V1/Admin/Player/AdminPlayerUpdateController.php @@ -2,19 +2,19 @@ namespace App\Http\Controllers\Api\V1\Admin\Player; -use App\Http\Controllers\Controller; -use App\Http\Requests\Admin\AdminPlayerUpdateRequest; -use App\Models\AgentNode; use App\Models\Player; +use App\Models\AgentNode; +use App\Support\ApiResponse; +use App\Support\AdminSiteScope; +use Illuminate\Http\JsonResponse; +use Illuminate\Support\Facades\DB; +use App\Support\PlayerApiPresenter; +use App\Http\Controllers\Controller; use App\Services\Agent\AgentProfileService; use App\Services\Agent\RebateLimitValidator; use App\Services\Player\PlayerCreditService; use App\Services\Player\PlayerRebateProfileService; -use App\Support\AdminSiteScope; -use App\Support\ApiResponse; -use App\Support\PlayerApiPresenter; -use Illuminate\Http\JsonResponse; -use Illuminate\Support\Facades\DB; +use App\Http\Requests\Admin\AdminPlayerUpdateRequest; /** PUT /api/v1/admin/players/{player} */ final class AdminPlayerUpdateController extends Controller @@ -44,11 +44,15 @@ final class AdminPlayerUpdateController extends Controller ? AgentNode::query()->find((int) $player->agent_node_id) : null; + $rebateRate = 0.0; + $extraRebateRate = 0.0; if ($agent !== null && $request->has('rebate_rate')) { + $rebateRate = (float) $request->input('rebate_rate', 0) / 100; + $extraRebateRate = (float) $request->input('extra_rebate_rate', 0) / 100; $rebateLimitValidator->assertPlayerRebateWithinAgent( $agent, - (float) $request->input('rebate_rate', 0), - (float) $request->input('extra_rebate_rate', 0), + $rebateRate, + $extraRebateRate, ); } @@ -68,8 +72,8 @@ final class AdminPlayerUpdateController extends Controller ['player_id' => $player->id, 'game_type' => '*'], [ 'inherit_from_agent' => false, - 'rebate_rate' => (float) $request->input('rebate_rate', 0), - 'extra_rebate_rate' => (float) $request->input('extra_rebate_rate', 0), + 'rebate_rate' => $rebateRate, + 'extra_rebate_rate' => $extraRebateRate, 'updated_at' => now(), 'created_at' => now(), ], diff --git a/app/Http/Requests/Admin/AdminIntegrationSiteStoreRequest.php b/app/Http/Requests/Admin/AdminIntegrationSiteStoreRequest.php index 6990116..6865a86 100644 --- a/app/Http/Requests/Admin/AdminIntegrationSiteStoreRequest.php +++ b/app/Http/Requests/Admin/AdminIntegrationSiteStoreRequest.php @@ -30,6 +30,11 @@ final class AdminIntegrationSiteStoreRequest extends ApiFormRequest 'iframe_allowed_origins.*' => ['string', 'max:512'], 'lottery_h5_base_url' => ['nullable', 'string', 'max:512'], 'notes' => ['nullable', 'string', 'max:5000'], + 'admin_account' => ['required', 'array'], + 'admin_account.username' => ['required', 'string', 'max:64', Rule::unique('admin_users', 'username')], + 'admin_account.nickname' => ['required', 'string', 'max:64'], + 'admin_account.password' => ['required', 'string', 'min:8', 'max:255'], + 'admin_account.email' => ['nullable', 'email', 'max:255'], ]; } } diff --git a/app/Http/Requests/Admin/AdminPlayerStoreRequest.php b/app/Http/Requests/Admin/AdminPlayerStoreRequest.php index 307d6a3..286a779 100644 --- a/app/Http/Requests/Admin/AdminPlayerStoreRequest.php +++ b/app/Http/Requests/Admin/AdminPlayerStoreRequest.php @@ -29,8 +29,8 @@ final class AdminPlayerStoreRequest extends ApiFormRequest 'status' => ['sometimes', 'integer', 'in:0,1,2'], 'agent_node_id' => ['sometimes', 'nullable', 'integer', 'min:1'], 'credit_limit' => ['sometimes', 'integer', 'min:0'], - 'rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:1'], - 'extra_rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:1'], + 'rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'], + 'extra_rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'], ]; } diff --git a/app/Http/Requests/Admin/AdminPlayerUpdateRequest.php b/app/Http/Requests/Admin/AdminPlayerUpdateRequest.php index eaad0ac..663142e 100644 --- a/app/Http/Requests/Admin/AdminPlayerUpdateRequest.php +++ b/app/Http/Requests/Admin/AdminPlayerUpdateRequest.php @@ -25,12 +25,12 @@ final class AdminPlayerUpdateRequest extends ApiFormRequest 'default_currency' => ['sometimes', 'string', 'max:16', Rule::exists('currencies', 'code')], 'status' => ['sometimes', 'integer', Rule::in([0, 1, 2])], 'credit_limit' => ['sometimes', 'integer', 'min:0'], - 'rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:1'], - 'extra_rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:1'], + 'rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'], + 'extra_rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'], 'rebate_profiles' => ['sometimes', 'array'], 'rebate_profiles.*.game_type' => ['required_with:rebate_profiles', 'string', 'max:32'], - 'rebate_profiles.*.rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:1'], - 'rebate_profiles.*.extra_rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:1'], + 'rebate_profiles.*.rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'], + 'rebate_profiles.*.extra_rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'], 'rebate_profiles.*.inherit_from_agent' => ['sometimes', 'boolean'], 'risk_tags' => ['sometimes', 'array'], 'risk_tags.*' => ['string', 'max:64'], diff --git a/app/Http/Requests/Admin/Concerns/AgentProfileFieldRules.php b/app/Http/Requests/Admin/Concerns/AgentProfileFieldRules.php index cfcb0d6..c31a710 100644 --- a/app/Http/Requests/Admin/Concerns/AgentProfileFieldRules.php +++ b/app/Http/Requests/Admin/Concerns/AgentProfileFieldRules.php @@ -2,8 +2,6 @@ namespace App\Http\Requests\Admin\Concerns; -use App\Support\AgentSettlementCycle; - trait AgentProfileFieldRules { /** @return array */ @@ -15,7 +13,6 @@ trait AgentProfileFieldRules 'credit_limit' => ['sometimes', 'integer', 'min:0'], 'rebate_limit' => ['sometimes', 'numeric', 'min:0', 'max:100'], 'default_player_rebate' => ['sometimes', 'numeric', 'min:0', 'max:100'], - 'settlement_cycle' => ['sometimes', 'string', 'in:'.implode(',', AgentSettlementCycle::VALUES)], 'can_grant_extra_rebate' => ['sometimes', 'boolean'], 'can_create_child_agent' => ['sometimes', 'boolean'], 'can_create_player' => ['sometimes', 'boolean'], @@ -26,12 +23,6 @@ trait AgentProfileFieldRules protected function prepareAgentProfileFieldsForValidation(): void { - if (! $this->has('settlement_cycle')) { - return; - } - - $this->merge([ - 'settlement_cycle' => AgentSettlementCycle::normalize($this->input('settlement_cycle')), - ]); + // 预处理字段(如需要) } } diff --git a/app/Models/AgentProfile.php b/app/Models/AgentProfile.php index 5bccddb..f691fdf 100644 --- a/app/Models/AgentProfile.php +++ b/app/Models/AgentProfile.php @@ -21,7 +21,6 @@ final class AgentProfile extends Model 'used_credit', 'rebate_limit', 'default_player_rebate', - 'settlement_cycle', 'can_grant_extra_rebate', 'can_create_child_agent', 'can_create_player', diff --git a/app/Services/Admin/AgentDashboardOverviewBuilder.php b/app/Services/Admin/AgentDashboardOverviewBuilder.php index cdc0984..861ae51 100644 --- a/app/Services/Admin/AgentDashboardOverviewBuilder.php +++ b/app/Services/Admin/AgentDashboardOverviewBuilder.php @@ -77,7 +77,6 @@ final class AgentDashboardOverviewBuilder (int) ($profile?->credit_limit ?? 0) - (int) ($profile?->allocated_credit ?? 0), ), 'total_share_rate' => (float) ($profile?->total_share_rate ?? 0), - 'settlement_cycle' => (string) ($profile?->settlement_cycle ?? 'weekly'), 'can_create_child_agent' => (bool) ($profile?->can_create_child_agent ?? false), 'can_create_player' => (bool) ($profile?->can_create_player ?? false), 'direct_child_count' => $directChildCount, diff --git a/app/Services/Agent/AgentNodeService.php b/app/Services/Agent/AgentNodeService.php index dc5d5f1..a63fe4e 100644 --- a/app/Services/Agent/AgentNodeService.php +++ b/app/Services/Agent/AgentNodeService.php @@ -30,7 +30,6 @@ final class AgentNodeService * credit_limit?: int, * rebate_limit?: float|int, * default_player_rebate?: float|int, - * settlement_cycle?: string, * can_grant_extra_rebate?: bool, * can_create_child_agent?: bool, * can_create_player?: bool @@ -125,7 +124,6 @@ final class AgentNodeService 'credit_limit' => (int) ($payload['credit_limit'] ?? 0), 'rebate_limit' => (float) ($payload['rebate_limit'] ?? 0), 'default_player_rebate' => (float) ($payload['default_player_rebate'] ?? 0), - 'settlement_cycle' => (string) ($payload['settlement_cycle'] ?? 'weekly'), 'can_grant_extra_rebate' => (bool) ($payload['can_grant_extra_rebate'] ?? false), 'can_create_child_agent' => (bool) ($payload['can_create_child_agent'] ?? false), 'can_create_player' => (bool) ($payload['can_create_player'] ?? true), diff --git a/app/Services/Agent/AgentProfileService.php b/app/Services/Agent/AgentProfileService.php index 89a0c94..5e97843 100644 --- a/app/Services/Agent/AgentProfileService.php +++ b/app/Services/Agent/AgentProfileService.php @@ -7,7 +7,6 @@ use App\Models\AgentNode; use App\Models\AgentProfile; use App\Support\AdminAgentScope; use App\Support\AgentOverdueGuard; -use App\Support\AgentSettlementCycle; use Illuminate\Support\Facades\DB; use Illuminate\Validation\ValidationException; @@ -86,9 +85,6 @@ final class AgentProfileService 'credit_limit' => $creditLimit, 'rebate_limit' => $rebateLimit, 'default_player_rebate' => $defaultRebate, - 'settlement_cycle' => AgentSettlementCycle::normalize( - $payload['settlement_cycle'] ?? $profile->settlement_cycle ?? 'weekly', - ), 'can_grant_extra_rebate' => (bool) ($payload['can_grant_extra_rebate'] ?? $profile->can_grant_extra_rebate ?? false), 'can_create_child_agent' => (bool) ($payload['can_create_child_agent'] ?? ($isNew ? false : $profile->can_create_child_agent)), 'can_create_player' => (bool) ($payload['can_create_player'] ?? ($isNew ? true : $profile->can_create_player ?? true)), @@ -126,7 +122,6 @@ final class AgentProfileService 'available_credit' => $available, 'rebate_limit' => round((float) $profile->rebate_limit * 100, 4), 'default_player_rebate' => round((float) $profile->default_player_rebate * 100, 4), - 'settlement_cycle' => AgentSettlementCycle::normalize($profile->settlement_cycle), 'can_grant_extra_rebate' => (bool) $profile->can_grant_extra_rebate, 'can_create_child_agent' => (bool) $profile->can_create_child_agent, 'can_create_player' => (bool) $profile->can_create_player, diff --git a/app/Services/Agent/AgentSiteProvisioningService.php b/app/Services/Agent/AgentSiteProvisioningService.php index 40ac2d8..44e99e9 100644 --- a/app/Services/Agent/AgentSiteProvisioningService.php +++ b/app/Services/Agent/AgentSiteProvisioningService.php @@ -105,7 +105,6 @@ final class AgentSiteProvisioningService 'credit_limit' => (int) ($payload['credit_limit'] ?? $defaults['credit_limit'] ?? 0), 'rebate_limit' => (float) ($payload['rebate_limit'] ?? $defaults['rebate_limit'] ?? 0), 'default_player_rebate' => (float) ($payload['default_player_rebate'] ?? $defaults['default_player_rebate'] ?? 0), - 'settlement_cycle' => (string) ($payload['settlement_cycle'] ?? $defaults['settlement_cycle'] ?? 'weekly'), 'can_grant_extra_rebate' => (bool) ($payload['can_grant_extra_rebate'] ?? true), 'can_create_child_agent' => (bool) ($payload['can_create_child_agent'] ?? true), 'can_create_player' => (bool) ($payload['can_create_player'] ?? true), diff --git a/app/Services/Agent/RebateLimitValidator.php b/app/Services/Agent/RebateLimitValidator.php index d3d732f..39a15b8 100644 --- a/app/Services/Agent/RebateLimitValidator.php +++ b/app/Services/Agent/RebateLimitValidator.php @@ -15,6 +15,7 @@ final class RebateLimitValidator return; } + // Both $rebateRate and $profile->rebate_limit are ratios (0-1) $limit = (float) $profile->rebate_limit; if ($rebateRate > $limit) { throw ValidationException::withMessages([ diff --git a/app/Services/AgentSettlement/BetSettlementSnapshotBuilder.php b/app/Services/AgentSettlement/BetSettlementSnapshotBuilder.php index 66b1e96..7499bb0 100644 --- a/app/Services/AgentSettlement/BetSettlementSnapshotBuilder.php +++ b/app/Services/AgentSettlement/BetSettlementSnapshotBuilder.php @@ -2,9 +2,9 @@ namespace App\Services\AgentSettlement; +use App\Models\Player; use App\Models\AgentNode; use App\Models\AgentProfile; -use App\Models\Player; use Illuminate\Support\Facades\DB; final class BetSettlementSnapshotBuilder diff --git a/app/Services/Integration/IntegrationSiteService.php b/app/Services/Integration/IntegrationSiteService.php index 3243810..2e8f43c 100644 --- a/app/Services/Integration/IntegrationSiteService.php +++ b/app/Services/Integration/IntegrationSiteService.php @@ -3,25 +3,45 @@ namespace App\Services\Integration; use App\Models\AdminSite; +use App\Models\AdminRole; +use App\Models\AdminUser; +use App\Support\AdminPermissionInheritance; use Illuminate\Support\Str; +use Illuminate\Validation\ValidationException; use Illuminate\Support\Facades\DB; final class IntegrationSiteService { + /** @var list */ + private const SITE_ADMIN_PERMISSION_SLUGS = [ + 'prd.agent.manage', + 'prd.agent.profile.manage', + 'prd.agent.user.manage', + 'prd.agent.role.manage', + 'prd.users.manage', + 'prd.tickets.view', + 'prd.report.view', + 'prd.settlement.agent.view', + 'prd.settlement.agent.manage', + ]; + public function __construct( private readonly PartnerSiteConfigResolver $configResolver, ) {} /** * @param array $data - * @return array{site: AdminSite, secrets: array{sso_jwt_secret: string, wallet_api_key: string}} + * @return array{site: AdminSite, secrets: array{sso_jwt_secret: string, wallet_api_key: string}, admin_user: AdminUser} */ public function create(array $data): array { $secrets = $this->generateSecrets(); - $site = DB::transaction(function () use ($data, $secrets): AdminSite { - return AdminSite::query()->create([ + ['site' => $site, 'admin_user' => $adminUser] = DB::transaction(function () use ($data, $secrets): array { + /** @var array{username: string, nickname: string, password: string, email?: string|null} $adminAccount */ + $adminAccount = $data['admin_account']; + + $site = AdminSite::query()->create([ 'code' => (string) $data['code'], 'name' => (string) $data['name'], 'currency_code' => (string) ($data['currency_code'] ?? 'NPR'), @@ -38,11 +58,23 @@ final class IntegrationSiteService 'sso_jwt_secret_encrypted' => encrypt($secrets['sso_jwt_secret']), 'wallet_api_key_encrypted' => encrypt($secrets['wallet_api_key']), ]); + + $role = $this->createSiteAdminRole($site); + $adminUser = $this->createSiteAdminUser($site, $role, $adminAccount); + + return [ + 'site' => $site, + 'admin_user' => $adminUser, + ]; }); $this->configResolver->forgetCache((string) $site->code); - return ['site' => $site->fresh(), 'secrets' => $secrets]; + return [ + 'site' => $site->fresh(), + 'secrets' => $secrets, + 'admin_user' => $adminUser->fresh(), + ]; } /** @@ -114,4 +146,53 @@ final class IntegrationSiteService return $trimmed === '' ? null : $trimmed; } + + private function createSiteAdminRole(AdminSite $site): AdminRole + { + $slug = sprintf('site_admin_%s', (string) $site->code); + $role = AdminRole::query()->create([ + 'slug' => $slug, + 'name' => sprintf('%s 站点后台管理员', (string) $site->name), + 'description' => sprintf('自动创建:站点 %s (%s) 后台管理账号专用角色', (string) $site->name, (string) $site->code), + 'status' => 1, + 'is_system' => true, + 'sort_order' => 900, + 'scope_type' => AdminRole::SCOPE_SYSTEM, + ]); + + $role->syncLegacyPermissionSlugs( + AdminPermissionInheritance::expand(self::SITE_ADMIN_PERMISSION_SLUGS), + ); + + return $role; + } + + /** + * @param array{username: string, nickname: string, password: string, email?: string|null} $adminAccount + */ + private function createSiteAdminUser(AdminSite $site, AdminRole $role, array $adminAccount): AdminUser + { + $username = trim((string) ($adminAccount['username'] ?? '')); + $nickname = trim((string) ($adminAccount['nickname'] ?? '')); + $password = (string) ($adminAccount['password'] ?? ''); + $email = $this->nullableTrim($adminAccount['email'] ?? null); + + if ($username === '' || $nickname === '' || $password === '') { + throw ValidationException::withMessages([ + 'admin_account' => ['站点后台管理账号信息不完整。'], + ]); + } + + $user = AdminUser::query()->create([ + 'username' => $username, + 'name' => $nickname, + 'email' => $email, + 'password' => $password, + 'status' => 0, + ]); + + $user->syncSystemRoleSlugsForSite((int) $site->id, [(string) $role->slug]); + + return $user; + } } diff --git a/app/Services/Player/PlayerRebateProfileService.php b/app/Services/Player/PlayerRebateProfileService.php index 11f55ac..d74e278 100644 --- a/app/Services/Player/PlayerRebateProfileService.php +++ b/app/Services/Player/PlayerRebateProfileService.php @@ -3,8 +3,8 @@ namespace App\Services\Player; use App\Models\AgentNode; -use App\Services\Agent\RebateLimitValidator; use Illuminate\Support\Facades\DB; +use App\Services\Agent\RebateLimitValidator; final class PlayerRebateProfileService { @@ -25,8 +25,8 @@ final class PlayerRebateProfileService foreach ($profiles as $row) { $gameType = trim((string) ($row['game_type'] ?? '*')) ?: '*'; $inherit = (bool) ($row['inherit_from_agent'] ?? false); - $rebateRate = (float) ($row['rebate_rate'] ?? 0); - $extraRate = (float) ($row['extra_rebate_rate'] ?? 0); + $rebateRate = (float) ($row['rebate_rate'] ?? 0) / 100; + $extraRate = (float) ($row['extra_rebate_rate'] ?? 0) / 100; if (! $inherit) { $this->rebateLimitValidator->assertPlayerRebateWithinAgent($agent, $rebateRate, $extraRate); @@ -56,8 +56,8 @@ final class PlayerRebateProfileService ->get() ->map(static fn (object $row): array => [ 'game_type' => (string) $row->game_type, - 'rebate_rate' => (float) $row->rebate_rate, - 'extra_rebate_rate' => (float) $row->extra_rebate_rate, + 'rebate_rate' => round((float) $row->rebate_rate * 100, 4), + 'extra_rebate_rate' => round((float) $row->extra_rebate_rate * 100, 4), 'inherit_from_agent' => (bool) $row->inherit_from_agent, ]) ->all(); diff --git a/app/Support/AdminAuthorizationRegistry.php b/app/Support/AdminAuthorizationRegistry.php index b1ee823..5916f96 100644 --- a/app/Support/AdminAuthorizationRegistry.php +++ b/app/Support/AdminAuthorizationRegistry.php @@ -139,6 +139,7 @@ final class AdminAuthorizationRegistry * platform_only?: bool, * agent_hidden?: bool, * activeMatchPrefix?: string, + * activeExact?: bool, * requiredAny?: list * }> */ @@ -146,7 +147,8 @@ final class AdminAuthorizationRegistry { return [ ['segment' => 'dashboard', 'label' => 'Dashboard', 'href' => '/admin', 'nav_group' => 'overview', 'requiredAny' => ['prd.dashboard.view']], - ['segment' => 'agents', 'label' => 'Agent lines', 'href' => '/admin/agents', 'nav_group' => 'agent', 'activeMatchPrefix' => '/admin/agents', 'requiredAny' => array_values(array_unique(array_merge(['prd.agent.view', 'prd.agent.manage', 'prd.agent.role.view', 'prd.agent.role.manage', 'prd.agent.user.view', 'prd.agent.user.manage', 'prd.agent-line.provision', 'prd.agent.profile.manage'], AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites'))))], + ['segment' => 'agents', 'label' => 'Agent lines', 'href' => '/admin/agents', 'nav_group' => 'agent', 'activeExact' => true, 'requiredAny' => array_values(array_unique(array_merge(['prd.agent.view', 'prd.agent.manage', 'prd.agent.role.view', 'prd.agent.role.manage', 'prd.agent.user.view', 'prd.agent.user.manage', 'prd.agent-line.provision', 'prd.agent.profile.manage'], AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites'))))], + ['segment' => 'agent_list', 'label' => 'Agent list', 'href' => '/admin/agents/list', 'nav_group' => 'agent', 'activeMatchPrefix' => '/admin/agents/list', 'requiredAny' => ['prd.agent.view', 'prd.agent.manage', 'prd.agent.role.view', 'prd.agent.role.manage', 'prd.agent.user.view', 'prd.agent.user.manage', 'prd.agent.profile.manage']], ['segment' => 'settlement_center', 'label' => 'Credit settlement', 'href' => '/admin/settlement-center', 'nav_group' => 'agent', 'activeMatchPrefix' => '/admin/settlement-center', 'requiredAny' => ['prd.settlement.agent.view', 'prd.settlement.agent.manage']], ['segment' => 'draws', 'label' => 'Draws', 'href' => '/admin/draws', 'nav_group' => 'operations', 'requiredAny' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.draw_reopen.manage']], ['segment' => 'tickets', 'label' => 'Tickets', 'href' => '/admin/tickets', 'nav_group' => 'operations', 'requiredAny' => ['prd.tickets.view']], @@ -155,10 +157,10 @@ final class AdminAuthorizationRegistry ['segment' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'nav_group' => 'finance', 'activeMatchPrefix' => '/admin/wallet', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage'], 'agent_hidden' => true], ['segment' => 'reconcile', 'label' => 'Reconcile', 'href' => '/admin/reconcile', 'nav_group' => 'finance', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs'], 'agent_hidden' => true], ['segment' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'nav_group' => 'finance', 'requiredAny' => ['prd.report.view']], - ['segment' => 'rules_plays', 'label' => 'Play rules', 'href' => '/admin/rules/plays', 'nav_group' => 'rules', 'platform_only' => true, 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']], - ['segment' => 'rules_odds', 'label' => 'Odds & rebate', 'href' => '/admin/rules/odds', 'nav_group' => 'rules', 'platform_only' => true, 'requiredAny' => ['prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view']], - ['segment' => 'jackpot', 'label' => 'Jackpot', 'href' => '/admin/jackpot', 'nav_group' => 'rules', 'platform_only' => true, 'activeMatchPrefix' => '/admin/jackpot', 'requiredAny' => ['prd.jackpot.manage', 'prd.jackpot.view']], - ['segment' => 'risk_cap', 'label' => 'Risk cap rules', 'href' => '/admin/risk/cap', 'nav_group' => 'rules', 'platform_only' => true, 'activeMatchPrefix' => '/admin/risk/cap', 'requiredAny' => ['prd.risk_cap.manage', 'prd.risk_cap.view']], + ['segment' => 'rules_plays', 'label' => 'Play rules', 'href' => '/admin/rules/plays', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']], + ['segment' => 'rules_odds', 'label' => 'Odds & rebate', 'href' => '/admin/rules/odds', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view']], + ['segment' => 'jackpot', 'label' => 'Jackpot', 'href' => '/admin/jackpot', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/jackpot', 'requiredAny' => ['prd.jackpot.manage', 'prd.jackpot.view']], + ['segment' => 'risk_cap', 'label' => 'Risk cap rules', 'href' => '/admin/risk/cap', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/risk/cap', 'requiredAny' => ['prd.risk_cap.manage', 'prd.risk_cap.view']], ['segment' => 'currencies', 'label' => 'Currencies', 'href' => '/admin/currencies', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.currency.manage']], ['segment' => 'integration', 'label' => 'Integration sites', 'href' => '/admin/config/integration-sites', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/config/integration-sites', 'requiredAny' => AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites')], ['segment' => 'admin_users', 'label' => 'Admin Users', 'href' => '/admin/admin-users', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.admin_user.manage']], @@ -279,6 +281,7 @@ final class AdminAuthorizationRegistry * platform_only?: bool, * agent_hidden?: bool, * activeMatchPrefix?: string, + * activeExact?: bool, * requiredAny?: list * }> */ @@ -297,6 +300,7 @@ final class AdminAuthorizationRegistry * platform_only?: bool, * agent_hidden?: bool, * activeMatchPrefix?: string, + * activeExact?: bool, * requiredAny?: list * }> */ @@ -420,6 +424,7 @@ final class AdminAuthorizationRegistry ['code' => 'admin.agent-lines.store', 'module_code' => 'agent', 'name' => '开通代理线路', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-lines', 'route_name' => 'api.v1.admin.agent-lines.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.line.provision'], 'legacy_permission_slugs' => ['prd.agent-line.provision', 'prd.agent.manage']], ['code' => 'admin.agent-lines.show', 'module_code' => 'agent', 'name' => '代理线路详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-lines/{admin_site}', 'route_name' => 'api.v1.admin.agent-lines.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.line.provision', 'agent.node.view', 'agent.node.manage'], 'legacy_permission_slugs' => ['prd.agent-line.provision', 'prd.agent.view', 'prd.agent.manage']], ['code' => 'admin.agent-nodes.tree', 'module_code' => 'agent', 'name' => '代理树', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/tree', 'route_name' => 'api.v1.admin.agent-nodes.tree', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage', 'agent.role.view', 'agent.role.manage', 'agent.user.view', 'agent.user.manage']], + ['code' => 'admin.agent-nodes.index', 'module_code' => 'agent', 'name' => '代理列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes', 'route_name' => 'api.v1.admin.agent-nodes.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage', 'agent.role.view', 'agent.role.manage', 'agent.user.view', 'agent.user.manage']], ['code' => 'admin.agent-nodes.store', 'module_code' => 'agent', 'name' => '创建下级代理', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-nodes', 'route_name' => 'api.v1.admin.agent-nodes.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']], ['code' => 'admin.agent-nodes.show', 'module_code' => 'agent', 'name' => '代理节点详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}', 'route_name' => 'api.v1.admin.agent-nodes.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']], ['code' => 'admin.agent-nodes.update', 'module_code' => 'agent', 'name' => '更新代理节点', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}', 'route_name' => 'api.v1.admin.agent-nodes.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']], diff --git a/app/Support/AgentNodePresenter.php b/app/Support/AgentNodePresenter.php index 05d0c5a..d7521b6 100644 --- a/app/Support/AgentNodePresenter.php +++ b/app/Support/AgentNodePresenter.php @@ -36,9 +36,8 @@ final class AgentNodePresenter 'allocated_credit' => (int) $profile->allocated_credit, 'used_credit' => (int) $profile->used_credit, 'available_credit' => max(0, (int) $profile->credit_limit - (int) $profile->allocated_credit), - 'rebate_limit' => (float) $profile->rebate_limit, - 'default_player_rebate' => (float) $profile->default_player_rebate, - 'settlement_cycle' => AgentSettlementCycle::normalize($profile->settlement_cycle), + 'rebate_limit' => round((float) $profile->rebate_limit * 100, 4), + 'default_player_rebate' => round((float) $profile->default_player_rebate * 100, 4), ]; } @@ -126,4 +125,26 @@ final class AgentNodePresenter return $roots; } + + /** + * @param iterable $nodes + * @return list> + */ + public static function list(iterable $nodes): array + { + $nodeList = $nodes instanceof Collection ? $nodes : collect($nodes); + $profiles = AgentProfile::query() + ->whereIn('agent_node_id', $nodeList->pluck('id')) + ->get() + ->keyBy('agent_node_id'); + + return $nodeList + ->map(function (AgentNode $node) use ($profiles): array { + $profile = $profiles->get($node->id); + + return self::item($node, $profile instanceof AgentProfile ? $profile : null); + }) + ->values() + ->all(); + } } diff --git a/app/Support/PlayerApiPresenter.php b/app/Support/PlayerApiPresenter.php index 1ccd48e..d2690cc 100644 --- a/app/Support/PlayerApiPresenter.php +++ b/app/Support/PlayerApiPresenter.php @@ -2,10 +2,10 @@ namespace App\Support; -use App\Models\AgentProfile; use App\Models\Player; +use App\Models\AgentNode; +use App\Models\AgentProfile; use App\Models\PlayerWallet; -use App\Support\PlayerFundingMode; use Illuminate\Support\Facades\DB; /** 玩家 API 统一 JSON 形状(列表行 / 详情)。 */ @@ -68,8 +68,8 @@ final class PlayerApiPresenter ->get() ->map(static fn (object $row): array => [ 'game_type' => (string) $row->game_type, - 'rebate_rate' => (float) $row->rebate_rate, - 'extra_rebate_rate' => (float) $row->extra_rebate_rate, + 'rebate_rate' => round((float) $row->rebate_rate * 100, 4), + 'extra_rebate_rate' => round((float) $row->extra_rebate_rate * 100, 4), 'inherit_from_agent' => (bool) $row->inherit_from_agent, ]) ->all(), @@ -79,7 +79,7 @@ final class PlayerApiPresenter /** * @return array{0: ?float, 1: bool} rebate rate (ratio) and whether inherited from agent */ - private static function resolveListRebate(Player $player, ?\App\Models\AgentNode $agent): array + private static function resolveListRebate(Player $player, ?AgentNode $agent): array { $row = DB::table('player_rebate_profiles') ->where('player_id', $player->id) @@ -87,13 +87,13 @@ final class PlayerApiPresenter ->first(); if ($row !== null && ! (bool) $row->inherit_from_agent) { - return [(float) $row->rebate_rate, false]; + return [round((float) $row->rebate_rate * 100, 4), false]; } if ($agent !== null) { $profile = AgentProfile::query()->where('agent_node_id', $agent->id)->first(); - return [(float) ($profile?->default_player_rebate ?? 0), true]; + return [round((float) ($profile?->default_player_rebate ?? 0) * 100, 4), true]; } return [null, false]; diff --git a/database/migrations/2026_06_03_160000_agent_credit_and_settlement_tables.php b/database/migrations/2026_06_03_160000_agent_credit_and_settlement_tables.php index cc01290..ba42ecd 100644 --- a/database/migrations/2026_06_03_160000_agent_credit_and_settlement_tables.php +++ b/database/migrations/2026_06_03_160000_agent_credit_and_settlement_tables.php @@ -1,8 +1,8 @@ foreignId('player_id')->constrained('players')->cascadeOnDelete(); $table->string('game_type', 32)->default('*'); $table->boolean('inherit_from_agent')->default(true); - $table->decimal('rebate_rate', 8, 4)->default(0); - $table->decimal('extra_rebate_rate', 8, 4)->default(0); + $table->decimal('rebate_rate', 8, 4)->default(0)->comment('回水比例 0-100'); + $table->decimal('extra_rebate_rate', 8, 4)->default(0)->comment('额外回水比例 0-100'); $table->timestamps(); $table->unique(['player_id', 'game_type']); }); diff --git a/database/migrations/2026_06_11_120000_seed_agent_node_index_api_resource.php b/database/migrations/2026_06_11_120000_seed_agent_node_index_api_resource.php new file mode 100644 index 0000000..30da611 --- /dev/null +++ b/database/migrations/2026_06_11_120000_seed_agent_node_index_api_resource.php @@ -0,0 +1,89 @@ +firstWhere('code', self::RESOURCE_CODE); + + if (! is_array($resource)) { + return; + } + + $now = Carbon::now(); + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + + $resourceId = DB::table('admin_api_resources') + ->where('code', self::RESOURCE_CODE) + ->value('id'); + + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => self::RESOURCE_CODE, + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + public function down(): void + { + $resourceId = DB::table('admin_api_resources') + ->where('code', self::RESOURCE_CODE) + ->value('id'); + + if ($resourceId === null) { + return; + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->delete(); + } +}; diff --git a/routes/api/v1/admin/agent.php b/routes/api/v1/admin/agent.php index 2376759..8bfab1a 100644 --- a/routes/api/v1/admin/agent.php +++ b/routes/api/v1/admin/agent.php @@ -2,6 +2,7 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeTreeController; +use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeIndexController; use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeShowController; use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeStoreController; use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeUpdateController; @@ -30,6 +31,8 @@ Route::middleware('admin.api-resource') ->name('api.v1.admin.agent-lines.show'); Route::get('agent-nodes/tree', AgentNodeTreeController::class) ->name('api.v1.admin.agent-nodes.tree'); + Route::get('agent-nodes', AgentNodeIndexController::class) + ->name('api.v1.admin.agent-nodes.index'); Route::post('agent-nodes', AgentNodeStoreController::class) ->name('api.v1.admin.agent-nodes.store'); Route::get('agent-nodes/{agent_node}/roles', AgentNodeRoleIndexController::class) diff --git a/tests/Feature/AdminAgentProfileApiTest.php b/tests/Feature/AdminAgentProfileApiTest.php index 15b90c1..c8c2e26 100644 --- a/tests/Feature/AdminAgentProfileApiTest.php +++ b/tests/Feature/AdminAgentProfileApiTest.php @@ -44,7 +44,6 @@ test('super admin can update agent profile with capability flags', function (): 'credit_limit' => 1200, 'rebate_limit' => 1, 'default_player_rebate' => 0.5, - 'settlement_cycle' => 'weekly', 'can_grant_extra_rebate' => false, 'can_create_child_agent' => true, 'can_create_player' => true, diff --git a/tests/Feature/AdminAgentProfileCapabilityPermissionTest.php b/tests/Feature/AdminAgentProfileCapabilityPermissionTest.php index 3c9f035..298319f 100644 --- a/tests/Feature/AdminAgentProfileCapabilityPermissionTest.php +++ b/tests/Feature/AdminAgentProfileCapabilityPermissionTest.php @@ -38,7 +38,6 @@ test('agent profile switches strip create player and child manage from effective 'used_credit' => 0, 'rebate_limit' => 0, 'default_player_rebate' => 0, - 'settlement_cycle' => 'weekly', 'can_grant_extra_rebate' => false, 'can_create_child_agent' => false, 'can_create_player' => false, @@ -98,7 +97,6 @@ test('agent profile switches on grant create capabilities even when platform age 'used_credit' => 0, 'rebate_limit' => 0, 'default_player_rebate' => 0, - 'settlement_cycle' => 'weekly', 'can_grant_extra_rebate' => false, 'can_create_child_agent' => true, 'can_create_player' => true, diff --git a/tests/Feature/AdminIntegrationSiteApiTest.php b/tests/Feature/AdminIntegrationSiteApiTest.php index efff07f..794a800 100644 --- a/tests/Feature/AdminIntegrationSiteApiTest.php +++ b/tests/Feature/AdminIntegrationSiteApiTest.php @@ -40,15 +40,22 @@ test('super admin can create integration site and receive secrets once', functio 'name' => 'Partner A', 'wallet_api_url' => 'https://wallet.partner-a.test', 'status' => 1, + 'admin_account' => [ + 'username' => 'partner_a_admin', + 'nickname' => 'Partner A Admin', + 'password' => 'secret-strong', + ], ]); $response->assertCreated() ->assertJsonPath('code', 0) ->assertJsonPath('data.code', 'partner-a') ->assertJsonPath('data.secrets_display_once', true) + ->assertJsonPath('data.admin_user.username', 'partner_a_admin') ->assertJsonStructure([ 'data' => [ 'secrets' => ['sso_jwt_secret', 'wallet_api_key'], + 'admin_user' => ['id', 'username', 'nickname', 'email'], ], ]); @@ -66,6 +73,11 @@ test('super admin can reveal integration site secrets for copy', function (): vo ->postJson('/api/v1/admin/integration-sites', [ 'code' => 'partner-secrets', 'name' => 'Partner Secrets', + 'admin_account' => [ + 'username' => 'partner_secrets_admin', + 'nickname' => 'Partner Secrets Admin', + 'password' => 'secret-strong', + ], ]) ->assertCreated(); @@ -94,6 +106,11 @@ test('integration site code cannot be changed on update', function (): void { ->postJson('/api/v1/admin/integration-sites', [ 'code' => 'partner-b', 'name' => 'Partner B', + 'admin_account' => [ + 'username' => 'partner_b_admin', + 'nickname' => 'Partner B Admin', + 'password' => 'secret-strong', + ], ]); $create->assertCreated(); $id = (int) $create->json('data.id'); @@ -154,6 +171,11 @@ test('rotate secrets returns new plaintext once', function (): void { ->postJson('/api/v1/admin/integration-sites', [ 'code' => 'partner-rotate', 'name' => 'Rotate', + 'admin_account' => [ + 'username' => 'partner_rotate_admin', + 'nickname' => 'Partner Rotate Admin', + 'password' => 'secret-strong', + ], ]); $id = (int) $create->json('data.id'); $oldSecret = (string) $create->json('data.secrets.sso_jwt_secret'); @@ -183,6 +205,11 @@ test('connectivity test probes partner balance api', function (): void { 'code' => 'probe-site', 'name' => 'Probe', 'wallet_api_url' => 'https://wallet.probe.test', + 'admin_account' => [ + 'username' => 'probe_site_admin', + 'nickname' => 'Probe Site Admin', + 'password' => 'secret-strong', + ], ]); $id = (int) $create->json('data.id'); @@ -205,6 +232,11 @@ test('export parameter sheet excludes plaintext secrets', function (): void { 'code' => 'export-site', 'name' => 'Export', 'wallet_api_url' => 'https://wallet.export.test', + 'admin_account' => [ + 'username' => 'export_site_admin', + 'nickname' => 'Export Site Admin', + 'password' => 'secret-strong', + ], ]); $id = (int) $create->json('data.id'); @@ -225,6 +257,11 @@ test('site scoped admin only sees bound integration sites', function (): void { ->postJson('/api/v1/admin/integration-sites', [ 'code' => 'site-a', 'name' => 'Site A', + 'admin_account' => [ + 'username' => 'site_a_admin', + 'nickname' => 'Site A Admin', + 'password' => 'secret-strong', + ], ]) ->assertCreated(); @@ -232,6 +269,11 @@ test('site scoped admin only sees bound integration sites', function (): void { ->postJson('/api/v1/admin/integration-sites', [ 'code' => 'site-b', 'name' => 'Site B', + 'admin_account' => [ + 'username' => 'site_b_admin', + 'nickname' => 'Site B Admin', + 'password' => 'secret-strong', + ], ]) ->assertCreated(); @@ -360,6 +402,11 @@ test('wallet_api_url rejects non-https', function (): void { 'code' => 'bad-https-1', 'name' => 'Bad HTTPS 1', 'wallet_api_url' => 'http://wallet.bad.test', + 'admin_account' => [ + 'username' => 'bad_https_admin_1', + 'nickname' => 'Bad HTTPS Admin 1', + 'password' => 'secret-strong', + ], ]) ->assertStatus(422) ->assertJsonPath('data.errors.wallet_api_url.0', 'wallet_api_url 必须是 https 的公开域名根地址,并拒绝 localhost/内网 IP 与带路径/查询的地址。'); @@ -373,6 +420,11 @@ test('wallet_api_url rejects localhost', function (): void { 'code' => 'bad-https-2', 'name' => 'Bad HTTPS 2', 'wallet_api_url' => 'https://localhost:8080', + 'admin_account' => [ + 'username' => 'bad_https_admin_2', + 'nickname' => 'Bad HTTPS Admin 2', + 'password' => 'secret-strong', + ], ]) ->assertStatus(422) ->assertJsonPath('data.errors.wallet_api_url.0', 'wallet_api_url 必须是 https 的公开域名根地址,并拒绝 localhost/内网 IP 与带路径/查询的地址。'); @@ -386,6 +438,11 @@ test('wallet_api_url rejects private ip with path', function (): void { 'code' => 'bad-https-3', 'name' => 'Bad HTTPS 3', 'wallet_api_url' => 'https://127.0.0.1/wallet', + 'admin_account' => [ + 'username' => 'bad_https_admin_3', + 'nickname' => 'Bad HTTPS Admin 3', + 'password' => 'secret-strong', + ], ]) ->assertStatus(422) ->assertJsonPath('data.errors.wallet_api_url.0', 'wallet_api_url 必须是 https 的公开域名根地址,并拒绝 localhost/内网 IP 与带路径/查询的地址。'); diff --git a/tests/Feature/AdminPlayerManageApiTest.php b/tests/Feature/AdminPlayerManageApiTest.php index 49545a8..570a3d3 100644 --- a/tests/Feature/AdminPlayerManageApiTest.php +++ b/tests/Feature/AdminPlayerManageApiTest.php @@ -281,7 +281,6 @@ test('admin can set player credit limit without clobbering used credit', functio 'used_credit' => 0, 'rebate_limit' => 0.01, 'default_player_rebate' => 0.005, - 'settlement_cycle' => 'weekly', ], ); diff --git a/tests/Feature/AgentCreditAllocationTest.php b/tests/Feature/AgentCreditAllocationTest.php index 9ac8e6a..3c26346 100644 --- a/tests/Feature/AgentCreditAllocationTest.php +++ b/tests/Feature/AgentCreditAllocationTest.php @@ -38,7 +38,6 @@ function createAgentLineForAllocation(string $code, int $creditLimit): AgentNode 'used_credit' => 0, 'rebate_limit' => 0.01, 'default_player_rebate' => 0.005, - 'settlement_cycle' => 'weekly', ]); return AgentNode::query()->findOrFail($rootId); diff --git a/tests/Feature/AgentSettlementPeriodCloseP0FixesTest.php b/tests/Feature/AgentSettlementPeriodCloseP0FixesTest.php index 610b3b9..65167b6 100644 --- a/tests/Feature/AgentSettlementPeriodCloseP0FixesTest.php +++ b/tests/Feature/AgentSettlementPeriodCloseP0FixesTest.php @@ -49,7 +49,6 @@ function createSiteWithRoot(string $code): array 'used_credit' => 0, 'rebate_limit' => 0.01, 'default_player_rebate' => 0.005, - 'settlement_cycle' => 'weekly', ]); return ['site_id' => $siteId, 'site_code' => $code, 'root_id' => $rootId]; diff --git a/tests/Feature/BetShareSnapshotImmutabilityTest.php b/tests/Feature/BetShareSnapshotImmutabilityTest.php index e753be5..564f92d 100644 --- a/tests/Feature/BetShareSnapshotImmutabilityTest.php +++ b/tests/Feature/BetShareSnapshotImmutabilityTest.php @@ -37,7 +37,6 @@ test('share snapshot uses profile at build time not after change', function (): 'used_credit' => 0, 'rebate_limit' => 0.01, 'default_player_rebate' => 0.005, - 'settlement_cycle' => 'weekly', ]); $playerId = (int) DB::table('players')->insertGetId([