feat: enhance player authentication and agent management features
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
- Updated AGENTS.md to clarify player interface bindings and agent account restrictions. - Improved PlayerAuthLoginController to include captcha verification for player login. - Enhanced AdminPlayerIndexController with permission checks for admin users. - Refactored AdminPlayerStoreController to enforce agent node restrictions for non-super admins. - Introduced new error codes for player authentication failures and updated related services. - Enhanced validation rules for agent profiles to include settlement cycle options. - Improved AdminCaptchaService to support separate scopes for admin and player captcha handling. - Updated various services to ensure proper credit management and settlement processes.
This commit is contained in:
@@ -40,18 +40,7 @@ final class AgentNodeRoleStoreController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
if (! AdminAgentScope::nodeManageableBy($admin, $agent_node)) {
|
||||
return AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $agent_node)
|
||||
?? ApiMessage::errorResponse(
|
||||
$request,
|
||||
'admin.agent_role_manage_denied',
|
||||
ErrorCode::AdminForbidden->value,
|
||||
null,
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
// Agent accounts can only create roles on their own node, not descendants
|
||||
// Agent accounts can only create roles on their own node, not descendants.
|
||||
$primaryNode = AdminAgentScope::primaryAgentNode($admin);
|
||||
if ($primaryNode !== null && (int) $primaryNode->id !== (int) $agent_node->id) {
|
||||
return ApiMessage::errorResponse(
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace App\Http\Controllers\Api\V1\Admin\Player;
|
||||
|
||||
use App\Models\Player;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Support\ApiMessage;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
@@ -19,6 +21,20 @@ final class AdminPlayerIndexController extends Controller
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
if (
|
||||
! $admin->isSuperAdmin()
|
||||
&& ! $admin->hasPermissionCode('service.players.view')
|
||||
&& ! $admin->hasPermissionCode('service.players.manage')
|
||||
) {
|
||||
return ApiMessage::errorResponse(
|
||||
$request,
|
||||
'admin.permission_denied',
|
||||
ErrorCode::AdminForbidden->value,
|
||||
null,
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
$p = AdminApiList::readPaging($request);
|
||||
$keyword = trim((string) $request->query('keyword', ''));
|
||||
$status = $request->query('status');
|
||||
|
||||
@@ -203,39 +203,30 @@ final class AdminPlayerStoreController extends Controller
|
||||
|
||||
private function resolveAgentNodeIdForNonSuperAdmin(AdminUser $admin, mixed $requested, string $siteCode): ?int
|
||||
{
|
||||
// Check if admin is a platform account (bound via admin_user_site_roles)
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($accessibleSiteIds !== null) {
|
||||
// Platform account (site admin) can specify agent_node_id
|
||||
if ($requested !== null && (int) $requested > 0) {
|
||||
$agent = AgentNode::query()->find((int) $requested);
|
||||
if ($agent !== null && in_array((int) $agent->admin_site_id, $accessibleSiteIds, true)) {
|
||||
return (int) $requested;
|
||||
}
|
||||
}
|
||||
// Default to root node of the site
|
||||
$siteId = AdminSite::query()->where('code', $siteCode)->value('id');
|
||||
if ($siteId !== null && in_array((int) $siteId, $accessibleSiteIds, true)) {
|
||||
$rootId = AgentNode::query()
|
||||
->where('admin_site_id', (int) $siteId)
|
||||
->where('depth', 0)
|
||||
->value('id');
|
||||
return $rootId !== null ? (int) $rootId : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Agent account (bound via agent node) - can only create under own node
|
||||
$agent = AdminAgentScope::primaryAgentNode($admin);
|
||||
if ($agent === null) {
|
||||
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 && (int) $requested !== (int) $agent->id) {
|
||||
return null; // Agent account cannot create under other nodes
|
||||
if ($requested === null || (int) $requested <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $agent->id;
|
||||
$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
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Player;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\AdminCaptchaService;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/** GET /api/v1/player/auth/captcha — 玩家账号密码登录图形验证码 */
|
||||
final class PlayerAuthCaptchaController extends Controller
|
||||
{
|
||||
public function __invoke(AdminCaptchaService $captcha): JsonResponse
|
||||
{
|
||||
$payload = $captcha->create(AdminCaptchaService::SCOPE_PLAYER);
|
||||
|
||||
return ApiResponse::success([
|
||||
'captcha_key' => $payload['captcha_key'],
|
||||
'image_base64' => $payload['image_base64'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,35 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Player;
|
||||
|
||||
use App\Exceptions\PlayerAuthenticationException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Player\PlayerAuthLoginRequest;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Services\AdminCaptchaService;
|
||||
use App\Services\Player\PlayerNativeAuthService;
|
||||
use App\Support\ApiResponse;
|
||||
use App\Support\LotteryMessage;
|
||||
use App\Exceptions\PlayerAuthenticationException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/** POST /api/v1/player/auth/login — 代理线下玩家账号密码登录 */
|
||||
final class PlayerAuthLoginController extends Controller
|
||||
{
|
||||
public function __invoke(PlayerAuthLoginRequest $request, PlayerNativeAuthService $auth): JsonResponse
|
||||
{
|
||||
public function __invoke(
|
||||
PlayerAuthLoginRequest $request,
|
||||
PlayerNativeAuthService $auth,
|
||||
AdminCaptchaService $captcha,
|
||||
): JsonResponse {
|
||||
$data = $request->validated();
|
||||
|
||||
if (! $captcha->verify($data['captcha_key'], $data['captcha_code'], AdminCaptchaService::SCOPE_PLAYER)) {
|
||||
return ApiResponse::error(
|
||||
LotteryMessage::sso($request, ErrorCode::PlayerCaptchaInvalid->value),
|
||||
ErrorCode::PlayerCaptchaInvalid->value,
|
||||
null,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $auth->login(
|
||||
(string) $request->validated('site_code', ''),
|
||||
|
||||
@@ -16,6 +16,7 @@ trait AgentProfileFieldRules
|
||||
'can_grant_extra_rebate' => ['sometimes', 'boolean'],
|
||||
'can_create_child_agent' => ['sometimes', 'boolean'],
|
||||
'can_create_player' => ['sometimes', 'boolean'],
|
||||
'settlement_cycle' => ['sometimes', 'nullable', 'string', 'in:weekly,monthly'],
|
||||
'risk_tags' => ['sometimes', 'array'],
|
||||
'risk_tags.*' => ['string', 'max:64'],
|
||||
];
|
||||
@@ -23,6 +24,13 @@ trait AgentProfileFieldRules
|
||||
|
||||
protected function prepareAgentProfileFieldsForValidation(): void
|
||||
{
|
||||
// 预处理字段(如需要)
|
||||
if (! $this->has('settlement_cycle')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cycle = trim((string) $this->input('settlement_cycle'));
|
||||
$this->merge([
|
||||
'settlement_cycle' => $cycle === '' ? 'weekly' : $cycle,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ final class PlayerAuthLoginRequest extends ApiFormRequest
|
||||
'site_code' => ['sometimes', 'nullable', 'string', 'max:64'],
|
||||
'username' => $this->nativePlayerUsernameRules(required: true),
|
||||
'password' => $this->nativePlayerPasswordRules(required: true),
|
||||
'captcha_key' => ['required', 'string', 'uuid'],
|
||||
'captcha_code' => ['required', 'string', 'max:32'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,9 @@ enum ErrorCode: int
|
||||
/** 原生登录:非彩票账号密码登录类型 */
|
||||
case PlayerNativeLoginRequired = 8008;
|
||||
|
||||
/** 原生登录:验证码错误或过期 */
|
||||
case PlayerCaptchaInvalid = 8009;
|
||||
|
||||
/* ========== 8100–8199 管理端 API ========== */
|
||||
|
||||
/** 未登录或 Token 无效 */
|
||||
|
||||
@@ -24,6 +24,7 @@ final class AgentProfile extends Model
|
||||
'can_grant_extra_rebate',
|
||||
'can_create_child_agent',
|
||||
'can_create_player',
|
||||
'settlement_cycle',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
@@ -39,6 +40,7 @@ final class AgentProfile extends Model
|
||||
'can_grant_extra_rebate' => 'boolean',
|
||||
'can_create_child_agent' => 'boolean',
|
||||
'can_create_player' => 'boolean',
|
||||
'settlement_cycle' => 'string',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -57,5 +57,13 @@ final class AppServiceProvider extends ServiceProvider
|
||||
RateLimiter::for('admin-auth-login', function (Request $request) {
|
||||
return Limit::perMinute(15)->by($request->ip());
|
||||
});
|
||||
|
||||
RateLimiter::for('player-auth-captcha', function (Request $request) {
|
||||
return Limit::perMinute(45)->by($request->ip());
|
||||
});
|
||||
|
||||
RateLimiter::for('player-auth-login', function (Request $request) {
|
||||
return Limit::perMinute(15)->by($request->ip());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -787,10 +787,6 @@ final class AdminReportQueryService
|
||||
];
|
||||
}
|
||||
|
||||
if ($limited['truncated']) {
|
||||
array_unshift($rows, ['警告', '审计日志已截断至 5000 条,请缩小日期范围后重试']);
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,19 @@ use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* 后台登录图形验证码:SVG 产出 + Cache 短时保存答案摘要(单行文本,便于前台用 img[src=data:...] 展示)。
|
||||
* 图形验证码:SVG 产出 + Cache 短时保存答案摘要(admin / player 登录共用渲染逻辑)。
|
||||
*/
|
||||
final class AdminCaptchaService
|
||||
{
|
||||
private const PREFIX = 'admin_captcha:';
|
||||
public const SCOPE_ADMIN = 'admin';
|
||||
|
||||
public const SCOPE_PLAYER = 'player';
|
||||
|
||||
/** @var array<string, string> */
|
||||
private const PREFIXES = [
|
||||
self::SCOPE_ADMIN => 'admin_captcha:',
|
||||
self::SCOPE_PLAYER => 'player_captcha:',
|
||||
];
|
||||
|
||||
private const TTL_SECONDS = 120;
|
||||
|
||||
@@ -20,13 +28,13 @@ final class AdminCaptchaService
|
||||
/**
|
||||
* @return array{captcha_key: string, image_svg: string, image_base64: string}
|
||||
*/
|
||||
public function create(): array
|
||||
public function create(string $scope = self::SCOPE_ADMIN): array
|
||||
{
|
||||
$code = $this->randomCode();
|
||||
$key = (string) Str::uuid();
|
||||
|
||||
Cache::put(
|
||||
self::PREFIX.$key,
|
||||
$this->prefix($scope).$key,
|
||||
$this->digest($code),
|
||||
now()->addSeconds(self::TTL_SECONDS),
|
||||
);
|
||||
@@ -40,14 +48,14 @@ final class AdminCaptchaService
|
||||
];
|
||||
}
|
||||
|
||||
public function verify(?string $captchaKey, ?string $captchaInput): bool
|
||||
public function verify(?string $captchaKey, ?string $captchaInput, string $scope = self::SCOPE_ADMIN): bool
|
||||
{
|
||||
if ($captchaKey === null || $captchaKey === ''
|
||||
|| $captchaInput === null || trim($captchaInput) === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$digest = Cache::pull(self::PREFIX.$captchaKey);
|
||||
$digest = Cache::pull($this->prefix($scope).$captchaKey);
|
||||
if ($digest === null) {
|
||||
return false;
|
||||
}
|
||||
@@ -57,6 +65,16 @@ final class AdminCaptchaService
|
||||
return hash_equals($digest, $this->digest($guess));
|
||||
}
|
||||
|
||||
private function prefix(string $scope): string
|
||||
{
|
||||
$prefix = self::PREFIXES[$scope] ?? null;
|
||||
if ($prefix === null) {
|
||||
throw new \InvalidArgumentException('invalid_captcha_scope');
|
||||
}
|
||||
|
||||
return $prefix;
|
||||
}
|
||||
|
||||
private function digest(string $normalizedCode): string
|
||||
{
|
||||
return hash_hmac(
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Services\Agent;
|
||||
|
||||
use App\Models\AgentNode;
|
||||
use App\Models\AgentProfile;
|
||||
use App\Support\PlayerFundingMode;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
@@ -43,6 +44,7 @@ final class AgentCreditAllocatedSyncService
|
||||
$playerTotal = (int) DB::table('player_credit_accounts as pca')
|
||||
->join('players as p', 'p.id', '=', 'pca.player_id')
|
||||
->where('p.agent_node_id', $agent->id)
|
||||
->where('p.funding_mode', PlayerFundingMode::CREDIT)
|
||||
->sum('pca.credit_limit');
|
||||
|
||||
$childIds = AgentNode::query()->where('parent_id', $agent->id)->pluck('id');
|
||||
|
||||
@@ -40,6 +40,12 @@ final class AgentProfileService
|
||||
$defaultRebate = array_key_exists('default_player_rebate', $payload)
|
||||
? (float) $payload['default_player_rebate'] / 100
|
||||
: (float) ($existingProfile->default_player_rebate ?? 0);
|
||||
$settlementCycle = array_key_exists('settlement_cycle', $payload)
|
||||
? (string) $payload['settlement_cycle']
|
||||
: (string) ($existingProfile->settlement_cycle ?? config('agent_line_defaults.settlement_cycle', 'weekly'));
|
||||
if ($settlementCycle === '') {
|
||||
$settlementCycle = 'weekly';
|
||||
}
|
||||
|
||||
$useRelative = $parent !== null && array_key_exists('relative_share_rate', $payload);
|
||||
|
||||
@@ -59,7 +65,7 @@ final class AgentProfileService
|
||||
);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($node, $payload, $parent, $totalShare, $creditLimit, $rebateLimit, $defaultRebate): AgentProfile {
|
||||
return DB::transaction(function () use ($node, $payload, $parent, $totalShare, $creditLimit, $rebateLimit, $defaultRebate, $settlementCycle): AgentProfile {
|
||||
$profile = AgentProfile::query()->firstOrNew(['agent_node_id' => $node->id]);
|
||||
$previousCredit = (int) $profile->credit_limit;
|
||||
$isNew = ! $profile->exists;
|
||||
@@ -104,6 +110,7 @@ final class AgentProfileService
|
||||
'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)),
|
||||
'settlement_cycle' => $settlementCycle,
|
||||
]);
|
||||
if (! $profile->exists) {
|
||||
$profile->allocated_credit = 0;
|
||||
@@ -141,6 +148,7 @@ final class AgentProfileService
|
||||
'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,
|
||||
'settlement_cycle' => (string) ($profile->settlement_cycle ?? 'weekly'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
namespace App\Services\AgentSettlement;
|
||||
|
||||
use App\Models\Player;
|
||||
use App\Support\CreditAmountScale;
|
||||
use App\Models\TicketItem;
|
||||
use App\Services\Player\PlayerCreditService;
|
||||
use App\Support\PlayerFundingMode;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class GameSettlementReversalService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PlayerCreditService $playerCreditService,
|
||||
) {}
|
||||
|
||||
public function reverseTicketItem(TicketItem $item): void
|
||||
{
|
||||
$ledger = DB::table('share_ledger')->where('ticket_item_id', $item->id)->whereNull('reversal_of_id')->first();
|
||||
@@ -60,21 +64,10 @@ final class GameSettlementReversalService
|
||||
}
|
||||
|
||||
$player = Player::query()->find((int) $ledger->player_id);
|
||||
if ($player !== null && PlayerFundingMode::usesCredit($player) && (int) $ledger->game_win_loss > 0) {
|
||||
$playerId = (int) $ledger->player_id;
|
||||
$row = DB::table('player_credit_accounts')->where('player_id', $playerId)->first();
|
||||
if ($row !== null) {
|
||||
$deltaMinor = (int) $ledger->game_win_loss;
|
||||
$deltaMajor = CreditAmountScale::minorToMajor(
|
||||
$deltaMinor,
|
||||
(string) $player->default_currency,
|
||||
);
|
||||
DB::table('player_credit_accounts')
|
||||
->where('player_id', $playerId)
|
||||
->update([
|
||||
'used_credit' => max(0, (int) $row->used_credit - $deltaMajor),
|
||||
'updated_at' => $settledAt,
|
||||
]);
|
||||
if ($player !== null && PlayerFundingMode::usesCredit($player)) {
|
||||
$gameWinLoss = (int) $ledger->game_win_loss;
|
||||
if ($gameWinLoss !== 0) {
|
||||
$this->playerCreditService->reverseGameSettlement($player, $gameWinLoss, $item->id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ final class SettlementCenterLedgerService
|
||||
'bet_hold_release',
|
||||
'game_settlement_loss',
|
||||
'game_settlement_win',
|
||||
'game_settlement_reversal',
|
||||
'settlement_confirm',
|
||||
'settlement_payout',
|
||||
];
|
||||
@@ -572,10 +573,9 @@ final class SettlementCenterLedgerService
|
||||
$id = (int) $stub->entry_id;
|
||||
if ($kind === 'credit' && isset($creditById[$id])) {
|
||||
$row = $creditById[$id];
|
||||
$pid = (int) $row->player_id;
|
||||
$bill = (string) ($row->ref_type ?? '') === 'settlement_bill'
|
||||
? ($creditBillRefs[(int) ($row->ref_id ?? 0)] ?? null)
|
||||
: ($playerBills[$pid] ?? null);
|
||||
: null;
|
||||
$items[] = $this->formatCreditEntry($row, $bill, $ticketRefs);
|
||||
} elseif ($kind === 'payment' && isset($paymentById[$id])) {
|
||||
$items[] = $this->formatPaymentEntry($paymentById[$id]);
|
||||
|
||||
@@ -18,11 +18,18 @@ final class PlayerCreditService
|
||||
{
|
||||
$limit = max(0, (int) ($payload['credit_limit'] ?? 0));
|
||||
$now = now();
|
||||
$exists = DB::table('player_credit_accounts')
|
||||
$row = DB::table('player_credit_accounts')
|
||||
->where('player_id', $player->id)
|
||||
->exists();
|
||||
->first();
|
||||
|
||||
if ($row !== null) {
|
||||
$usedTotal = (int) $row->used_credit + (int) $row->frozen_credit;
|
||||
if ($limit < $usedTotal) {
|
||||
throw ValidationException::withMessages([
|
||||
'credit_limit' => ['below_player_used'],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($exists) {
|
||||
DB::table('player_credit_accounts')
|
||||
->where('player_id', $player->id)
|
||||
->update([
|
||||
@@ -72,7 +79,8 @@ final class PlayerCreditService
|
||||
$this->assertCreditGuards($player);
|
||||
|
||||
$currency = (string) $player->default_currency;
|
||||
if ($amountMinor > $this->availableCreditMinor($player, $currency)) {
|
||||
$majorNeeded = CreditAmountScale::minorToMajor($amountMinor, $currency);
|
||||
if ($majorNeeded > $this->availableCredit($player)) {
|
||||
throw ValidationException::withMessages([
|
||||
'credit' => ['insufficient'],
|
||||
]);
|
||||
@@ -274,9 +282,58 @@ final class PlayerCreditService
|
||||
]);
|
||||
}
|
||||
|
||||
public function reverseGameSettlement(Player $player, int $gameWinLossSigned, int $ticketItemId): void
|
||||
{
|
||||
if ($gameWinLossSigned === 0 || ! PlayerFundingMode::usesCredit($player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = now();
|
||||
|
||||
if ($gameWinLossSigned > 0) {
|
||||
$amountMinor = $gameWinLossSigned;
|
||||
$this->decreaseUsedCredit($player, $amountMinor);
|
||||
|
||||
DB::table('credit_ledger')->insert([
|
||||
'owner_type' => 'player',
|
||||
'owner_id' => $player->id,
|
||||
'amount' => $amountMinor,
|
||||
'reason' => 'game_settlement_reversal',
|
||||
'ref_type' => 'ticket_item',
|
||||
'ref_id' => $ticketItemId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$amountMinor = abs($gameWinLossSigned);
|
||||
$currency = (string) $player->default_currency;
|
||||
$majorDelta = CreditAmountScale::minorToMajor($amountMinor, $currency);
|
||||
|
||||
DB::table('player_credit_accounts')
|
||||
->where('player_id', $player->id)
|
||||
->update([
|
||||
'used_credit' => DB::raw('used_credit + '.$majorDelta),
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
DB::table('credit_ledger')->insert([
|
||||
'owner_type' => 'player',
|
||||
'owner_id' => $player->id,
|
||||
'amount' => -$amountMinor,
|
||||
'reason' => 'game_settlement_reversal',
|
||||
'ref_type' => 'ticket_item',
|
||||
'ref_id' => $ticketItemId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
public function releaseFromSettlement(Player $player, int $amountMinor, int $billId): void
|
||||
{
|
||||
if ($amountMinor <= 0) {
|
||||
if ($amountMinor <= 0 || ! PlayerFundingMode::usesCredit($player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ final class PlayerLedgerLogsService
|
||||
/** PRD 对外类型 → credit_ledger.reason(信用盘不用钱包「派彩」口径) */
|
||||
private const CREDIT_TYPE_TO_REASON = [
|
||||
'bet' => ['bet_hold', 'game_settlement_loss'],
|
||||
'reversal' => ['bet_hold_release'],
|
||||
'reversal' => ['bet_hold_release', 'game_settlement_reversal'],
|
||||
'refund' => ['settlement_confirm'],
|
||||
'win_credit' => ['game_settlement_win'],
|
||||
'credit_release' => ['game_settlement_win', 'settlement_confirm', 'bet_hold_release'],
|
||||
@@ -106,6 +106,7 @@ final class PlayerLedgerLogsService
|
||||
'bet_hold_release',
|
||||
'game_settlement_loss',
|
||||
'game_settlement_win',
|
||||
'game_settlement_reversal',
|
||||
'settlement_confirm',
|
||||
'settlement_payout',
|
||||
]), 5000);
|
||||
@@ -180,9 +181,26 @@ final class PlayerLedgerLogsService
|
||||
$pageRows = array_slice($simplified, $offset, $perPage);
|
||||
|
||||
$runningMinor = $this->playerCreditService->availableCreditMinor($player, $currency);
|
||||
foreach (array_slice($simplified, 0, $offset) as $priorRow) {
|
||||
if (! $this->adminCreditRowAffectsAvailableBalance($priorRow)) {
|
||||
continue;
|
||||
}
|
||||
$runningMinor -= $this->adminCreditRowSignedDelta($priorRow);
|
||||
}
|
||||
$items = [];
|
||||
foreach ($pageRows as $formatted) {
|
||||
$signed = (int) ($formatted['direction'] === 1 ? $formatted['amount'] : -$formatted['amount']);
|
||||
if (! $this->adminCreditRowAffectsAvailableBalance($formatted)) {
|
||||
$items[] = array_merge($formatted, [
|
||||
'balance_after' => null,
|
||||
'balance_after_formatted' => null,
|
||||
'balance_before' => null,
|
||||
'balance_before_formatted' => null,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$signed = $this->adminCreditRowSignedDelta($formatted);
|
||||
$items[] = array_merge($formatted, [
|
||||
'balance_after' => $runningMinor,
|
||||
'balance_after_formatted' => CurrencyFormatter::fromMinor($runningMinor),
|
||||
@@ -414,7 +432,13 @@ final class PlayerLedgerLogsService
|
||||
->paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
$currency = (string) $player->default_currency;
|
||||
$runningMinor = $this->playerCreditService->availableCreditMinor($player, $currency);
|
||||
$skipRows = max(0, ($paginator->currentPage() - 1) * $paginator->perPage());
|
||||
$runningMinor = $this->advanceCreditLedgerRunningMinor(
|
||||
(int) $player->id,
|
||||
$reasonFilter,
|
||||
$this->playerCreditService->availableCreditMinor($player, $currency),
|
||||
$skipRows,
|
||||
);
|
||||
$items = $paginator->getCollection()
|
||||
->map(function (object $row) use (&$runningMinor, $player, $currency): array {
|
||||
$amount = (int) $row->amount;
|
||||
@@ -587,6 +611,49 @@ final class PlayerLedgerLogsService
|
||||
return $reason !== 'settlement_payout';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string>|null $reasonFilter
|
||||
*/
|
||||
private function advanceCreditLedgerRunningMinor(
|
||||
int $playerId,
|
||||
?array $reasonFilter,
|
||||
int $runningMinor,
|
||||
int $skipRows,
|
||||
): int {
|
||||
if ($skipRows <= 0) {
|
||||
return $runningMinor;
|
||||
}
|
||||
|
||||
$priorRows = $this->creditLedgerQuery($playerId, $reasonFilter)
|
||||
->limit($skipRows)
|
||||
->get();
|
||||
|
||||
foreach ($priorRows as $row) {
|
||||
if (! $this->creditReasonAffectsAvailableBalance((string) $row->reason)) {
|
||||
continue;
|
||||
}
|
||||
$runningMinor -= (int) $row->amount;
|
||||
}
|
||||
|
||||
return $runningMinor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $formatted
|
||||
*/
|
||||
private function adminCreditRowAffectsAvailableBalance(array $formatted): bool
|
||||
{
|
||||
return ($formatted['biz_type'] ?? '') !== 'settlement_payout';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $formatted
|
||||
*/
|
||||
private function adminCreditRowSignedDelta(array $formatted): int
|
||||
{
|
||||
return (int) ($formatted['direction'] === 1 ? $formatted['amount'] : -$formatted['amount']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
@@ -636,7 +703,7 @@ final class PlayerLedgerLogsService
|
||||
{
|
||||
return match ($reason) {
|
||||
'bet_hold', 'game_settlement_loss' => 'bet',
|
||||
'bet_hold_release' => 'reversal',
|
||||
'bet_hold_release', 'game_settlement_reversal' => 'reversal',
|
||||
'settlement_confirm' => 'refund',
|
||||
'game_settlement_win' => 'win_credit',
|
||||
'settlement_payout' => 'bill_settlement',
|
||||
|
||||
@@ -23,32 +23,30 @@ final class AdminAgentNodeAccess
|
||||
?? AdminSite::query()->orderBy('id')->value('id'));
|
||||
}
|
||||
|
||||
// Check if admin is a platform account (bound via admin_user_site_roles)
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($accessibleSiteIds !== null) {
|
||||
// Platform account (site admin)
|
||||
if ($requestedSiteId !== null && $requestedSiteId > 0) {
|
||||
if (in_array($requestedSiteId, $accessibleSiteIds, true)) {
|
||||
return $requestedSiteId;
|
||||
}
|
||||
// Agent account (bound via agent node) - check first
|
||||
$actor = AdminAgentScope::primaryAgentNode($admin);
|
||||
if ($actor !== null) {
|
||||
if ($requestedSiteId !== null && $requestedSiteId > 0 && $requestedSiteId !== (int) $actor->admin_site_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return first accessible site if no specific site requested
|
||||
return $accessibleSiteIds[0] ?? null;
|
||||
return (int) $actor->admin_site_id;
|
||||
}
|
||||
|
||||
// Agent account (bound via agent node)
|
||||
$actor = AdminAgentScope::primaryAgentNode($admin);
|
||||
if ($actor === null) {
|
||||
if (! AdminAgentScope::isSiteOnlyOperator($admin)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($requestedSiteId !== null && $requestedSiteId > 0 && $requestedSiteId !== (int) $actor->admin_site_id) {
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds() ?? [];
|
||||
if ($requestedSiteId !== null && $requestedSiteId > 0) {
|
||||
if (in_array($requestedSiteId, $accessibleSiteIds, true)) {
|
||||
return $requestedSiteId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $actor->admin_site_id;
|
||||
return $accessibleSiteIds[0] ?? null;
|
||||
}
|
||||
|
||||
public static function denyUnlessSiteResolved(AdminUser $admin, ?int $siteId): ?JsonResponse
|
||||
|
||||
@@ -26,6 +26,18 @@ final class AdminAgentScope
|
||||
return AgentNode::query()->find($agentId);
|
||||
}
|
||||
|
||||
/** 仅站点运营(admin_user_site_roles),未绑定代理节点。 */
|
||||
public static function isSiteOnlyOperator(AdminUser $admin): bool
|
||||
{
|
||||
if ($admin->isSuperAdmin() || self::primaryAgentNode($admin) !== null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$siteIds = $admin->accessibleAdminSiteIds();
|
||||
|
||||
return $siteIds !== null && $siteIds !== [];
|
||||
}
|
||||
|
||||
public static function nodeVisibleTo(AdminUser $admin, AgentNode $node): bool
|
||||
{
|
||||
if ($admin->isSuperAdmin()) {
|
||||
@@ -55,30 +67,21 @@ final class AdminAgentScope
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if admin is a platform account (bound via admin_user_site_roles)
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($accessibleSiteIds !== null) {
|
||||
// Platform account (site admin) can access all players in the site
|
||||
// Site check is done by AdminSiteScope::playerAccessible before calling this
|
||||
return true;
|
||||
}
|
||||
|
||||
// Agent account (bound via agent node)
|
||||
$actor = self::primaryAgentNode($admin);
|
||||
if ($actor === null) {
|
||||
return false;
|
||||
if ($actor !== null) {
|
||||
if ($player->agent_node_id === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$playerAgent = AgentNode::query()->find((int) $player->agent_node_id);
|
||||
if ($playerAgent === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $playerAgent->isSameOrDescendantOf($actor);
|
||||
}
|
||||
|
||||
if ($player->agent_node_id === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$playerAgent = AgentNode::query()->find((int) $player->agent_node_id);
|
||||
if ($playerAgent === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $playerAgent->isSameOrDescendantOf($actor);
|
||||
return self::isSiteOnlyOperator($admin);
|
||||
}
|
||||
|
||||
public static function nodeManageableBy(AdminUser $admin, AgentNode $node): bool
|
||||
@@ -113,32 +116,22 @@ final class AdminAgentScope
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if admin is a platform account (bound via admin_user_site_roles)
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($accessibleSiteIds !== null) {
|
||||
// Platform account (site admin) can edit all nodes in the site
|
||||
// EXCEPT their own bound agent node
|
||||
if (in_array((int) $node->admin_site_id, $accessibleSiteIds, true)) {
|
||||
$actor = self::primaryAgentNode($admin);
|
||||
if ($actor !== null && (int) $actor->id === (int) $node->id) {
|
||||
return false; // Cannot edit own bound node
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Agent account (bound via agent node)
|
||||
$actor = self::primaryAgentNode($admin);
|
||||
if ($actor === null) {
|
||||
if ($actor !== null) {
|
||||
if ((int) $actor->id === (int) $node->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $node->isDescendantOf($actor);
|
||||
}
|
||||
|
||||
if (! self::isSiteOnlyOperator($admin)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((int) $actor->id === (int) $node->id) {
|
||||
return false;
|
||||
}
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
|
||||
return $node->isDescendantOf($actor);
|
||||
return in_array((int) $node->admin_site_id, $accessibleSiteIds ?? [], true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,18 +181,11 @@ final class AdminAgentScope
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if admin is a platform account (bound via admin_user_site_roles)
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($accessibleSiteIds !== null) {
|
||||
// Platform account (site admin) - site filtering is handled by AdminSiteScope
|
||||
// No agent node filtering needed
|
||||
return;
|
||||
}
|
||||
|
||||
// Agent account (bound via agent node)
|
||||
$actor = self::primaryAgentNode($admin);
|
||||
if ($actor === null) {
|
||||
$query->whereRaw('0 = 1');
|
||||
if (! self::isSiteOnlyOperator($admin)) {
|
||||
$query->whereRaw('0 = 1');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -541,13 +541,13 @@ final class AdminAuthorizationRegistry
|
||||
['code' => 'admin.jackpot.pools.manual-burst', 'module_code' => 'jackpot', 'name' => '手动爆池', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/jackpot/pools/{pool}/manual-burst', 'route_name' => 'api.v1.admin.jackpot.pools.manual-burst', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.jackpot.manual_burst']],
|
||||
|
||||
['code' => 'admin.players.index', 'module_code' => 'player_service', 'name' => '玩家列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/players', 'route_name' => 'api.v1.admin.players.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.players.manage', 'service.players.view']],
|
||||
['code' => 'admin.players.store', 'module_code' => 'player_service', 'name' => '创建玩家', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/players', 'route_name' => 'api.v1.admin.players.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.users.manage']],
|
||||
['code' => 'admin.players.store', 'module_code' => 'player_service', 'name' => '创建玩家', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/players', 'route_name' => 'api.v1.admin.players.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['service.players.manage']],
|
||||
['code' => 'admin.players.show', 'module_code' => 'player_service', 'name' => '玩家详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/players/{player}', 'route_name' => 'api.v1.admin.players.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.players.manage', 'service.players.view']],
|
||||
['code' => 'admin.players.update', 'module_code' => 'player_service', 'name' => '更新玩家', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/players/{player}', 'route_name' => 'api.v1.admin.players.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.users.manage']],
|
||||
['code' => 'admin.players.destroy', 'module_code' => 'player_service', 'name' => '删除玩家', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/players/{player}', 'route_name' => 'api.v1.admin.players.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.users.manage']],
|
||||
['code' => 'admin.players.update', 'module_code' => 'player_service', 'name' => '更新玩家', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/players/{player}', 'route_name' => 'api.v1.admin.players.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['service.players.manage']],
|
||||
['code' => 'admin.players.destroy', 'module_code' => 'player_service', 'name' => '删除玩家', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/players/{player}', 'route_name' => 'api.v1.admin.players.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['service.players.manage']],
|
||||
['code' => 'admin.players.freeze', 'module_code' => 'player_service', 'name' => '冻结玩家', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/players/{player}/freeze', 'route_name' => 'api.v1.admin.players.freeze', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['service.players.freeze']],
|
||||
['code' => 'admin.players.unfreeze', 'module_code' => 'player_service', 'name' => '解冻玩家', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/players/{player}/unfreeze', 'route_name' => 'api.v1.admin.players.unfreeze', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['service.players.freeze']],
|
||||
['code' => 'admin.players.wallets', 'module_code' => 'player_service', 'name' => '玩家钱包查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/players/{player}/wallets', 'route_name' => 'api.v1.admin.players.wallets', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.wallet.view']],
|
||||
['code' => 'admin.players.wallets', 'module_code' => 'player_service', 'name' => '玩家钱包查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/players/{player}/wallets', 'route_name' => 'api.v1.admin.players.wallets', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.players.manage', 'service.wallet.view']],
|
||||
['code' => 'admin.players.ticket-items', 'module_code' => 'player_service', 'name' => '玩家注单查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/players/{player}/ticket-items', 'route_name' => 'api.v1.admin.players.ticket-items.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.players.manage', 'service.tickets.view']],
|
||||
['code' => 'admin.tickets.index', 'module_code' => 'ticket', 'name' => '后台注单列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/tickets', 'route_name' => 'api.v1.admin.tickets.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.tickets.view']],
|
||||
['code' => 'admin.tickets.show', 'module_code' => 'ticket', 'name' => '后台注单详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/tickets/{ticket_no}', 'route_name' => 'api.v1.admin.tickets.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.tickets.view', 'prd.draw_result.manage', 'prd.draw_result.view', 'prd.risk.view', 'prd.risk.manage']],
|
||||
|
||||
@@ -43,20 +43,13 @@ final class AdminDataScope
|
||||
$query->whereIn($alias.'.site_code', $codes);
|
||||
}
|
||||
|
||||
// Check if admin is a platform account (bound via admin_user_site_roles)
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($accessibleSiteIds !== null) {
|
||||
// Platform account (site admin) - no agent node filtering needed
|
||||
if ($requestedAgentNodeId !== null && $requestedAgentNodeId > 0) {
|
||||
self::applyAgentNodeIdOnAlias($query, $admin, $alias, $requestedAgentNodeId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Agent account (bound via agent node)
|
||||
$actor = AdminAgentScope::primaryAgentNode($admin);
|
||||
if ($actor === null) {
|
||||
$query->whereRaw('0 = 1');
|
||||
if (! AdminAgentScope::isSiteOnlyOperator($admin)) {
|
||||
$query->whereRaw('0 = 1');
|
||||
} elseif ($requestedAgentNodeId !== null && $requestedAgentNodeId > 0) {
|
||||
self::applyAgentNodeIdOnAlias($query, $admin, $alias, $requestedAgentNodeId);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,13 +91,7 @@ final class AdminSiteScope
|
||||
|
||||
$query->whereIn('site_code', $codes);
|
||||
|
||||
// Apply agent node filtering only for agent accounts, not platform accounts
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($accessibleSiteIds === null) {
|
||||
// Agent account - apply agent node filtering
|
||||
AdminAgentScope::applyToPlayerQuery($query, $admin);
|
||||
}
|
||||
// Platform account - no additional agent node filtering needed
|
||||
AdminAgentScope::applyToPlayerQuery($query, $admin);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,7 +35,9 @@ final class CreditAmountScale
|
||||
return $major * self::minorUnitFactor($currencyCode);
|
||||
}
|
||||
|
||||
/** 最小单位 → 主货币整数(四舍五入)。 */
|
||||
/**
|
||||
* 最小单位 → 主货币整数(占用授信时向上取整,与 preflight 按 major 校验一致)。
|
||||
*/
|
||||
public static function minorToMajor(int $minor, string $currencyCode): int
|
||||
{
|
||||
$factor = self::minorUnitFactor($currencyCode);
|
||||
@@ -43,10 +45,10 @@ final class CreditAmountScale
|
||||
return $minor;
|
||||
}
|
||||
|
||||
if ($minor >= 0) {
|
||||
return intdiv($minor + intdiv($factor, 2), $factor);
|
||||
if ($minor <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -intdiv(-$minor + intdiv($factor, 2), $factor);
|
||||
return intdiv($minor + $factor - 1, $factor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,12 @@
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminSite;
|
||||
use App\Services\LotterySettings;
|
||||
|
||||
/** 站点级信用占成盘开关(extra_json.credit_line_mode);玩家判读以 funding_mode 为准。 */
|
||||
final class CreditLineMode
|
||||
{
|
||||
public static function isEnabledForSiteCode(string $siteCode): bool
|
||||
{
|
||||
if ((bool) LotterySettings::get('settlement.credit_line_disable_instant_rebate', false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$site = AdminSite::query()->where('code', $siteCode)->first(['extra_json']);
|
||||
if ($site === null) {
|
||||
return false;
|
||||
|
||||
@@ -13,18 +13,7 @@ final class PlayerFundingMode
|
||||
|
||||
public static function usesCredit(Player $player): bool
|
||||
{
|
||||
$mode = (string) ($player->funding_mode ?? '');
|
||||
|
||||
if ($mode === self::CREDIT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($mode === self::WALLET) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (string) ($player->auth_source ?? '') === PlayerAuthSource::LOTTERY_NATIVE
|
||||
&& CreditLineMode::isEnabledForSiteCode((string) $player->site_code);
|
||||
return (string) ($player->funding_mode ?? '') === self::CREDIT;
|
||||
}
|
||||
|
||||
public static function usesWallet(Player $player): bool
|
||||
|
||||
Reference in New Issue
Block a user