Files
lotteryLaravel/app/Http/Controllers/Api/V1/Admin/Player/AdminPlayerStoreController.php
kang 3c57941895
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled
feat: add idempotency support for settlement adjustments and payments, fix rebate handling
- Added idempotency_key parameter to settlement bill adjustment and payment endpoints to prevent duplicate operations
- Fixed player rebate profile updates to preserve existing values when only one field is modified
- Moved wallet player validation earlier in AdminPlayerStoreController to prevent unnecessary processing
- Enhanced extra rebate calculation to include 'in_bill' and 'settled' statuses in AgentP
2026-06-23 16:49:57 +08:00

252 lines
9.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Api\V1\Admin\Player;
use App\Models\Player;
use App\Models\AdminSite;
use App\Models\AgentNode;
use App\Models\AdminUser;
use App\Lottery\ErrorCode;
use App\Support\ApiMessage;
use App\Support\ApiResponse;
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 Illuminate\Support\Facades\Hash;
use App\Services\Agent\AgentProfileService;
use App\Services\Agent\RebateLimitValidator;
use App\Services\Player\PlayerCreditService;
use Illuminate\Validation\ValidationException;
use App\Http\Requests\Admin\AdminPlayerStoreRequest;
/** POST /api/v1/admin/players */
final class AdminPlayerStoreController extends Controller
{
public function __invoke(
AdminPlayerStoreRequest $request,
PlayerCreditService $playerCreditService,
RebateLimitValidator $rebateLimitValidator,
AgentProfileService $agentProfileService,
): JsonResponse {
$admin = $request->lotteryAdmin();
abort_if($admin === null, 401);
try {
$agentProfileService->assertActorMayCreatePlayer($admin);
} catch (ValidationException $e) {
return ApiMessage::errorResponse(
$request,
'admin.player_create_capability_forbidden',
ErrorCode::AdminForbidden->value,
$e->errors(),
403,
);
}
$siteCode = (string) $request->validated('site_code');
if (! AdminSiteScope::siteCodeAllowed($admin, $siteCode)) {
return ApiMessage::errorResponse($request, 'admin.player_create_site_forbidden', ErrorCode::AdminForbidden->value, null, 403);
}
$isNative = $request->filled('password');
$sitePlayerId = (string) ($request->validated('site_player_id') ?? '');
if ($isNative) {
$sitePlayerId = $sitePlayerId !== ''
? $sitePlayerId
: $this->generateNativeSitePlayerId($siteCode);
}
if ($sitePlayerId === '') {
return ApiMessage::errorResponse($request, 'admin.player_site_player_id_required', ErrorCode::ValidationFailed->value, null, 422);
}
$exists = Player::query()
->where('site_code', $siteCode)
->where('site_player_id', $sitePlayerId)
->exists();
if ($exists) {
return ApiMessage::errorResponse($request, 'admin.player_already_registered', ErrorCode::ValidationFailed->value, null, 422);
}
if ($isNative) {
$username = trim((string) $request->validated('username', ''));
if ($username === '') {
return ApiMessage::errorResponse($request, 'admin.player_native_username_required', ErrorCode::ValidationFailed->value, null, 422);
}
$usernameTaken = Player::query()
->where('site_code', $siteCode)
->where('username', $username)
->where('auth_source', PlayerAuthSource::LOTTERY_NATIVE)
->exists();
if ($usernameTaken) {
return ApiMessage::errorResponse($request, 'admin.player_username_taken', ErrorCode::ValidationFailed->value, null, 422);
}
}
$agentNodeId = $admin->isSuperAdmin()
? $this->resolveAgentNodeIdForSuperAdmin($request->validated('agent_node_id'), $siteCode)
: $this->resolveAgentNodeIdForNonSuperAdmin($admin, $request->validated('agent_node_id'), $siteCode);
if ($agentNodeId === null) {
return ApiMessage::errorResponse(
$request,
'admin.player_create_agent_required',
ErrorCode::ValidationFailed->value,
null,
422,
);
}
$agent = AgentNode::query()->findOrFail($agentNodeId);
if (! $isNative && ($request->has('credit_limit') || $request->has('rebate_rate') || $request->has('extra_rebate_rate'))) {
throw ValidationException::withMessages([
'credit_limit' => ['wallet_player_prohibited'],
]);
}
$rebateRate = 0.0;
$extraRebateRate = 0.0;
if ($request->has('rebate_rate') || $request->has('extra_rebate_rate')) {
$rebateRate = (float) $request->input('rebate_rate', 0) / 100;
$extraRebateRate = (float) $request->input('extra_rebate_rate', 0) / 100;
$rebateLimitValidator->assertPlayerRebateWithinAgent(
$agent,
$rebateRate,
$extraRebateRate,
);
}
$creditLimit = $request->has('credit_limit')
? (int) $request->input('credit_limit', 0)
: ($isNative ? 0 : 0);
$player = DB::transaction(function () use (
$agent,
$agentProfileService,
$playerCreditService,
$request,
$isNative,
$siteCode,
$agentNodeId,
$sitePlayerId,
$creditLimit,
$rebateRate,
$extraRebateRate,
): Player {
$agentProfileService->assertMayIncreasePlayerCredit($agent, $creditLimit);
$player = Player::query()->create([
'site_code' => $siteCode,
'agent_node_id' => $agentNodeId,
'site_player_id' => $sitePlayerId,
'auth_source' => $isNative ? PlayerAuthSource::LOTTERY_NATIVE : PlayerAuthSource::MAIN_SITE_SSO,
'funding_mode' => $isNative ? PlayerFundingMode::CREDIT : PlayerFundingMode::WALLET,
'username' => $isNative ? trim((string) $request->validated('username')) : $request->validated('username'),
'password_hash' => $isNative ? Hash::make((string) $request->validated('password')) : null,
'nickname' => $request->validated('nickname'),
'default_currency' => $request->validated('default_currency', 'NPR'),
'status' => $request->validated('status', 0),
]);
if ($request->has('credit_limit') || $isNative) {
$playerCreditService->upsertAccount($player, [
'credit_limit' => $creditLimit,
]);
}
$agentProfileService->refreshAllocatedCredit($agent);
if ($request->has('rebate_rate') || $request->has('extra_rebate_rate')) {
DB::table('player_rebate_profiles')->insert([
'player_id' => $player->id,
'game_type' => '*',
'inherit_from_agent' => false,
'rebate_rate' => $rebateRate,
'extra_rebate_rate' => $extraRebateRate,
'created_at' => now(),
'updated_at' => now(),
]);
}
return $player;
});
return ApiResponse::success(PlayerApiPresenter::listItem($player))->setStatusCode(201);
}
private function resolveAgentNodeIdForSuperAdmin(mixed $requested, string $siteCode): ?int
{
if ($requested !== null && (int) $requested > 0) {
return (int) $requested;
}
$siteId = AdminSite::query()->where('code', $siteCode)->value('id');
if ($siteId === null) {
return null;
}
$rootId = AgentNode::query()
->where('admin_site_id', (int) $siteId)
->where('depth', 0)
->value('id');
return $rootId !== null ? (int) $rootId : null;
}
private function resolveAgentNodeIdForNonSuperAdmin(AdminUser $admin, mixed $requested, string $siteCode): ?int
{
$agent = AdminAgentScope::primaryAgentNode($admin);
if ($agent !== null) {
if ($requested !== null && (int) $requested > 0 && (int) $requested !== (int) $agent->id) {
return null;
}
return (int) $agent->id;
}
if (! AdminAgentScope::isSiteOnlyOperator($admin)) {
return null;
}
if ($requested === null || (int) $requested <= 0) {
return null;
}
$accessibleSiteIds = $admin->accessibleAdminSiteIds() ?? [];
$node = AgentNode::query()->find((int) $requested);
if ($node === null || ! in_array((int) $node->admin_site_id, $accessibleSiteIds, true)) {
return null;
}
return (int) $requested;
}
private function generateNativeSitePlayerId(string $siteCode): string
{
$prefix = strtoupper(substr(preg_replace('/[^A-Za-z]/', '', $siteCode) ?: 'LP', 0, 2));
$prefix = str_pad($prefix, 2, 'P');
// 使用 10 位数字100 亿组合)显著降低并发碰撞概率;即使极小概率撞上,
// 上层 Player::create 有 (site_code, site_player_id) 唯一索引兜底,
// 错误会在调用方被显式处理为 422/500而非静默双写。
do {
$candidate = sprintf('%s%010d', $prefix, random_int(0, 9999999999));
$exists = Player::query()
->where('site_code', $siteCode)
->where('site_player_id', $candidate)
->exists();
} while ($exists);
return $candidate;
}
}