feat: enhance agent management and validation logic
- Updated AGENTS.md to clarify agent account restrictions and permissions. - Implemented checks in AgentNodeAdminUserStoreController and AgentNodeRoleStoreController to restrict admin user and role creation to the agent's own node. - Enhanced validation in AdminPlayerStoreController and AdminPlayerUpdateController to enforce credit limit and rebate rate rules based on player funding mode. - Refactored various request classes to utilize shared admin account field rules for consistency. - Improved error handling in services related to credit allocation and rebate limits to ensure proper validation and messaging.
This commit is contained in:
78
app/Services/Ticket/TicketLineInstantRebateApplicator.php
Normal file
78
app/Services/Ticket/TicketLineInstantRebateApplicator.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Ticket;
|
||||
|
||||
use App\Models\Player;
|
||||
use App\Support\PlayerFundingMode;
|
||||
|
||||
/**
|
||||
* 下注行回水:钱包盘立减实扣;信用盘仅展示账期回水预估,实扣仍按全额占用授信。
|
||||
*/
|
||||
final class TicketLineInstantRebateApplicator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InstantRebateResolver $instantRebateResolver,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $evaluated
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function apply(Player $player, array $evaluated): array
|
||||
{
|
||||
$resolved = $this->instantRebateResolver->resolveForPlayer(
|
||||
$player,
|
||||
(string) $evaluated['play_code'],
|
||||
(float) $evaluated['rebate_rate_snapshot'],
|
||||
);
|
||||
|
||||
$evaluated['rule_snapshot_json']['base_rebate_rate'] = number_format($resolved['base_rebate_rate'], 4, '.', '');
|
||||
$evaluated['rule_snapshot_json']['player_addon_rebate_rate'] = number_format($resolved['player_addon_rebate_rate'], 4, '.', '');
|
||||
$evaluated['rule_snapshot_json']['rebate_inherited_from_agent'] = $resolved['inherited_from_agent'];
|
||||
$evaluated['rule_snapshot_json']['instant_rebate_applied'] = ! PlayerFundingMode::usesCredit($player);
|
||||
|
||||
$finalRate = $resolved['final_rebate_rate'];
|
||||
$evaluated['rebate_rate_snapshot'] = number_format($finalRate, 4, '.', '');
|
||||
|
||||
if (PlayerFundingMode::usesCredit($player)) {
|
||||
$evaluated['actual_deduct_amount'] = (int) $evaluated['total_bet_amount'];
|
||||
|
||||
return $evaluated;
|
||||
}
|
||||
|
||||
$evaluated['actual_deduct_amount'] = max(
|
||||
0,
|
||||
(int) floor((int) $evaluated['total_bet_amount'] * (1 - $finalRate)),
|
||||
);
|
||||
|
||||
return $evaluated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认弹窗展示用回水:信用盘为账期回水预估,钱包盘为下注立减额。
|
||||
*
|
||||
* @param array<string, mixed> $evaluated
|
||||
*/
|
||||
public function displayRebateAmount(Player $player, array $evaluated): int
|
||||
{
|
||||
$bet = (int) $evaluated['total_bet_amount'];
|
||||
|
||||
if (PlayerFundingMode::usesCredit($player)) {
|
||||
$rate = (float) ($evaluated['rebate_rate_snapshot'] ?? 0);
|
||||
|
||||
return (int) floor($bet * max(0.0, min(1.0, $rate)));
|
||||
}
|
||||
|
||||
return max(0, $bet - (int) $evaluated['actual_deduct_amount']);
|
||||
}
|
||||
|
||||
/** 落库订单/注单上的即时回水合计(信用盘为 0)。 */
|
||||
public function persistedInstantRebateAmount(Player $player, array $evaluated): int
|
||||
{
|
||||
if (PlayerFundingMode::usesCredit($player)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $this->displayRebateAmount($player, $evaluated);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ final class TicketPlacementService
|
||||
public function __construct(
|
||||
private readonly PlayCatalogResolver $catalogResolver,
|
||||
private readonly PlayRuleEngine $ruleEngine,
|
||||
private readonly InstantRebateResolver $instantRebateResolver,
|
||||
private readonly TicketLineInstantRebateApplicator $rebateApplicator,
|
||||
private readonly RiskPoolService $riskPoolService,
|
||||
private readonly TicketWalletService $ticketWalletService,
|
||||
private readonly JackpotContributionService $jackpotContribution,
|
||||
@@ -132,7 +132,7 @@ final class TicketPlacementService
|
||||
$resolved['play_config'],
|
||||
$resolved['odds_items'],
|
||||
);
|
||||
$evaluated = $this->applyCreditLineInstantRebatePolicy($player, $evaluated);
|
||||
$evaluated = $this->rebateApplicator->apply($player, $evaluated);
|
||||
|
||||
$locks = array_map(fn (array $combo): array => [
|
||||
'number_4d' => $combo['number_4d'],
|
||||
@@ -141,7 +141,7 @@ final class TicketPlacementService
|
||||
// place 阶段以 acquire 的原子扣减结果为准,允许单行售罄后形成混合成功/失败结果。
|
||||
|
||||
$evaluatedLines[] = $evaluated;
|
||||
$rebateAmount = (int) $evaluated['total_bet_amount'] - (int) $evaluated['actual_deduct_amount'];
|
||||
$rebateAmount = $this->rebateApplicator->persistedInstantRebateAmount($player, $evaluated);
|
||||
$totalBet += (int) $evaluated['total_bet_amount'];
|
||||
$totalRebate += $rebateAmount;
|
||||
$totalActualDeduct += (int) $evaluated['actual_deduct_amount'];
|
||||
@@ -283,7 +283,7 @@ final class TicketPlacementService
|
||||
continue;
|
||||
}
|
||||
|
||||
$rebateAmount = (int) $evaluated['total_bet_amount'] - (int) $evaluated['actual_deduct_amount'];
|
||||
$rebateAmount = $this->rebateApplicator->persistedInstantRebateAmount($player, $evaluated);
|
||||
$item->forceFill([
|
||||
'actual_deduct_amount' => (int) $evaluated['actual_deduct_amount'],
|
||||
'risk_locked_amount' => $lockedAmount,
|
||||
@@ -370,7 +370,7 @@ final class TicketPlacementService
|
||||
])->save();
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
DB::transaction(function () use ($order): void {
|
||||
DB::transaction(function () use ($order, $player): void {
|
||||
$items = TicketItem::query()
|
||||
->where('order_id', $order->id)
|
||||
->where('status', 'pending_confirm')
|
||||
@@ -396,10 +396,12 @@ final class TicketPlacementService
|
||||
}
|
||||
|
||||
$order->forceFill(['status' => 'refunded'])->save();
|
||||
|
||||
// Reverse the bet deduct first to restore balance
|
||||
if (! PlayerFundingMode::usesCredit($player)) {
|
||||
$this->ticketWalletService->reverseBetDeduct($order);
|
||||
$this->ticketWalletService->releaseReservedBetDeduct($order, 'wallet_deduct_failed_release');
|
||||
}
|
||||
$this->ticketWalletService->reverseBetDeduct($order);
|
||||
});
|
||||
|
||||
throw $e;
|
||||
@@ -526,37 +528,4 @@ final class TicketPlacementService
|
||||
|
||||
return str_pad($number, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $evaluated
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function applyCreditLineInstantRebatePolicy(Player $player, array $evaluated): array
|
||||
{
|
||||
$resolved = $this->instantRebateResolver->resolveForPlayer(
|
||||
$player,
|
||||
(string) $evaluated['play_code'],
|
||||
(float) $evaluated['rebate_rate_snapshot'],
|
||||
);
|
||||
|
||||
$evaluated['rule_snapshot_json']['base_rebate_rate'] = number_format($resolved['base_rebate_rate'], 4, '.', '');
|
||||
$evaluated['rule_snapshot_json']['player_addon_rebate_rate'] = number_format($resolved['player_addon_rebate_rate'], 4, '.', '');
|
||||
$evaluated['rule_snapshot_json']['rebate_inherited_from_agent'] = $resolved['inherited_from_agent'];
|
||||
|
||||
if (PlayerFundingMode::usesCredit($player)) {
|
||||
$evaluated['rebate_rate_snapshot'] = '0.0000';
|
||||
$evaluated['actual_deduct_amount'] = (int) $evaluated['total_bet_amount'];
|
||||
|
||||
return $evaluated;
|
||||
}
|
||||
|
||||
$finalRate = $resolved['final_rebate_rate'];
|
||||
$evaluated['rebate_rate_snapshot'] = number_format($finalRate, 4, '.', '');
|
||||
$evaluated['actual_deduct_amount'] = max(
|
||||
0,
|
||||
(int) floor((int) $evaluated['total_bet_amount'] * (1 - $finalRate)),
|
||||
);
|
||||
|
||||
return $evaluated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ final class TicketPreviewService
|
||||
public function __construct(
|
||||
private readonly PlayCatalogResolver $catalogResolver,
|
||||
private readonly PlayRuleEngine $ruleEngine,
|
||||
private readonly InstantRebateResolver $instantRebateResolver,
|
||||
private readonly TicketLineInstantRebateApplicator $rebateApplicator,
|
||||
private readonly RiskPoolService $riskPoolService,
|
||||
private readonly DrawHallSnapshotBuilder $drawHallSnapshot,
|
||||
) {}
|
||||
@@ -65,7 +65,7 @@ final class TicketPreviewService
|
||||
$resolved['play_config'],
|
||||
$resolved['odds_items'],
|
||||
);
|
||||
$evaluated = $this->applyPlayerInstantRebate($player, $evaluated);
|
||||
$evaluated = $this->rebateApplicator->apply($player, $evaluated);
|
||||
|
||||
$locks = array_map(fn (array $combo): array => [
|
||||
'number_4d' => $combo['number_4d'],
|
||||
@@ -81,7 +81,7 @@ final class TicketPreviewService
|
||||
}
|
||||
}
|
||||
|
||||
$rebateAmount = (int) $evaluated['total_bet_amount'] - (int) $evaluated['actual_deduct_amount'];
|
||||
$rebateAmount = $this->rebateApplicator->displayRebateAmount($player, $evaluated);
|
||||
$totalBet += (int) $evaluated['total_bet_amount'];
|
||||
$totalRebate += $rebateAmount;
|
||||
$totalActualDeduct += (int) $evaluated['actual_deduct_amount'];
|
||||
@@ -130,42 +130,10 @@ final class TicketPreviewService
|
||||
'total_rebate_amount' => $totalRebate,
|
||||
'total_actual_deduct' => $totalActualDeduct,
|
||||
'total_estimated_payout' => $totalEstimatedPayout,
|
||||
'instant_rebate_applied' => ! PlayerFundingMode::usesCredit($player),
|
||||
],
|
||||
'lines' => $lines,
|
||||
'warnings' => $warningRows,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $evaluated
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function applyPlayerInstantRebate(Player $player, array $evaluated): array
|
||||
{
|
||||
$resolved = $this->instantRebateResolver->resolveForPlayer(
|
||||
$player,
|
||||
(string) $evaluated['play_code'],
|
||||
(float) $evaluated['rebate_rate_snapshot'],
|
||||
);
|
||||
|
||||
$evaluated['rule_snapshot_json']['base_rebate_rate'] = number_format($resolved['base_rebate_rate'], 4, '.', '');
|
||||
$evaluated['rule_snapshot_json']['player_addon_rebate_rate'] = number_format($resolved['player_addon_rebate_rate'], 4, '.', '');
|
||||
$evaluated['rule_snapshot_json']['rebate_inherited_from_agent'] = $resolved['inherited_from_agent'];
|
||||
|
||||
if (PlayerFundingMode::usesCredit($player)) {
|
||||
$evaluated['rebate_rate_snapshot'] = '0.0000';
|
||||
$evaluated['actual_deduct_amount'] = (int) $evaluated['total_bet_amount'];
|
||||
|
||||
return $evaluated;
|
||||
}
|
||||
|
||||
$finalRate = $resolved['final_rebate_rate'];
|
||||
$evaluated['rebate_rate_snapshot'] = number_format($finalRate, 4, '.', '');
|
||||
$evaluated['actual_deduct_amount'] = max(
|
||||
0,
|
||||
(int) floor((int) $evaluated['total_bet_amount'] * (1 - $finalRate)),
|
||||
);
|
||||
|
||||
return $evaluated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,14 +133,13 @@ final class TicketWalletService
|
||||
return;
|
||||
}
|
||||
|
||||
if (WalletTxn::query()->where('biz_type', 'bet_deduct')->where('biz_no', $order->order_no)->where('status', self::TXN_POSTED)->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (WalletTxn::query()->where('biz_type', self::BIZ_BET_RESERVE_RELEASE)->where('idempotent_key', $releaseIdempotentKey)->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Even if bet_deduct exists, we still need to release frozen balance
|
||||
// because finalizeReservedBetDeduct may have failed to release it completely
|
||||
|
||||
$wallet = PlayerWallet::query()
|
||||
->where('player_id', $order->player_id)
|
||||
->where('wallet_type', 'lottery')
|
||||
|
||||
Reference in New Issue
Block a user