189 lines
6.8 KiB
PHP
189 lines
6.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services\AgentSettlement;
|
|
|
|
use App\Models\Player;
|
|
use App\Models\TicketItem;
|
|
use App\Services\Player\PlayerCreditService;
|
|
use App\Support\PlayerFundingMode;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* 彩票注单游戏结算侧入账:快照、占成流水、回水计提、玩家已用额度。
|
|
*/
|
|
final class AgentGameSettlementRecorder
|
|
{
|
|
public function __construct(
|
|
private readonly BetSettlementSnapshotBuilder $snapshotBuilder,
|
|
private readonly ShareSettlementCalculator $calculator,
|
|
private readonly PlayerCreditService $playerCreditService,
|
|
) {}
|
|
|
|
public function shouldRecord(TicketItem $item): bool
|
|
{
|
|
$player = $item->player;
|
|
if ($player === null) {
|
|
return false;
|
|
}
|
|
|
|
return PlayerFundingMode::usesCredit($player)
|
|
&& (int) ($player->agent_node_id ?? 0) > 0;
|
|
}
|
|
|
|
public function recordForTicketItem(TicketItem $item, int $netWin, string $terminalStatus): void
|
|
{
|
|
if (! $this->shouldRecord($item)) {
|
|
return;
|
|
}
|
|
|
|
$player = $item->player;
|
|
if ($player === null) {
|
|
return;
|
|
}
|
|
|
|
$gameType = trim((string) ($item->play_code ?? '')) ?: '*';
|
|
$snapshot = $this->snapshotBuilder->buildForPlayer($player, $gameType);
|
|
$baseRebateRate = $this->baseRebateRateForTicketItem($item);
|
|
$basicRebateRate = $this->normalizeRate($baseRebateRate + (float) $snapshot['rebate_rate']);
|
|
$shareSnapshot = [
|
|
...$snapshot,
|
|
'base_rebate_rate' => $baseRebateRate,
|
|
'basic_rebate_rate' => $basicRebateRate,
|
|
];
|
|
|
|
$gameWinLoss = $this->platformWinLoss($item, $netWin, $terminalStatus);
|
|
$validBet = (int) $item->total_bet_amount;
|
|
$basicRebate = (int) round($validBet * $basicRebateRate);
|
|
$extraRebate = (int) round($validBet * $snapshot['extra_rebate_rate']);
|
|
|
|
$extraByCode = [];
|
|
if ($extraRebate > 0 && $snapshot['chain_codes'] !== []) {
|
|
$leaf = $snapshot['chain_codes'][0];
|
|
$extraByCode[$leaf] = $extraRebate;
|
|
}
|
|
|
|
$result = $this->calculator->calculate(
|
|
sharedNetWinLoss: 0,
|
|
totalSharesByCode: $snapshot['total_shares'],
|
|
extraRebateByCode: $extraByCode,
|
|
gameWinLoss: $gameWinLoss,
|
|
basicRebate: $basicRebate,
|
|
chainFromPlayer: $snapshot['chain_codes'],
|
|
);
|
|
|
|
$settledAt = now();
|
|
|
|
DB::transaction(function () use ($item, $player, $snapshot, $shareSnapshot, $basicRebateRate, $gameWinLoss, $basicRebate, $result, $settledAt, $validBet, $extraRebate, $gameType): void {
|
|
$item->forceFill([
|
|
'agent_node_id' => $snapshot['agent_node_id'],
|
|
'share_snapshot' => $shareSnapshot,
|
|
'agent_rebate_rate_snapshot' => $basicRebateRate,
|
|
'agent_settled_at' => $settledAt,
|
|
])->save();
|
|
|
|
DB::table('share_ledger')->insert([
|
|
'ticket_item_id' => $item->id,
|
|
'player_id' => $player->id,
|
|
'agent_node_id' => $snapshot['agent_node_id'],
|
|
'agent_path' => json_encode($snapshot['agent_path']),
|
|
'share_snapshot' => json_encode($shareSnapshot),
|
|
'game_win_loss' => (int) round($gameWinLoss),
|
|
'basic_rebate' => $basicRebate,
|
|
'shared_net_win_loss' => (int) round($result->sharedNetWinLoss),
|
|
'allocations_json' => json_encode($result->finalProfits),
|
|
'settled_at' => $settledAt,
|
|
'created_at' => $settledAt,
|
|
'updated_at' => $settledAt,
|
|
]);
|
|
|
|
if ($basicRebate > 0) {
|
|
DB::table('rebate_records')->insert([
|
|
'player_id' => $player->id,
|
|
'ticket_item_id' => $item->id,
|
|
'game_type' => $gameType,
|
|
'valid_bet_amount' => $validBet,
|
|
'rebate_rate' => $basicRebateRate,
|
|
'rebate_amount' => $basicRebate,
|
|
'rebate_type' => 'basic',
|
|
'owner_agent_id' => $snapshot['agent_node_id'],
|
|
'status' => 'accrued',
|
|
'created_at' => $settledAt,
|
|
'updated_at' => $settledAt,
|
|
]);
|
|
}
|
|
|
|
if ($extraRebate > 0) {
|
|
DB::table('rebate_records')->insert([
|
|
'player_id' => $player->id,
|
|
'ticket_item_id' => $item->id,
|
|
'game_type' => $gameType,
|
|
'valid_bet_amount' => $validBet,
|
|
'rebate_rate' => $snapshot['extra_rebate_rate'],
|
|
'rebate_amount' => $extraRebate,
|
|
'rebate_type' => 'extra',
|
|
'owner_agent_id' => $snapshot['agent_node_id'],
|
|
'status' => 'accrued',
|
|
'created_at' => $settledAt,
|
|
'updated_at' => $settledAt,
|
|
]);
|
|
}
|
|
|
|
$holdAmount = (int) $item->actual_deduct_amount;
|
|
if ($holdAmount > 0) {
|
|
$this->playerCreditService->releaseBetHold($player, $holdAmount, $item->id);
|
|
}
|
|
|
|
if ($gameWinLoss > 0) {
|
|
$this->playerCreditService->applySettledLoss($player, (int) round($gameWinLoss), $item->id);
|
|
} elseif ($gameWinLoss < 0) {
|
|
$this->playerCreditService->applySettledWin($player, (int) round(abs($gameWinLoss)), $item->id);
|
|
}
|
|
});
|
|
}
|
|
|
|
private function baseRebateRateForTicketItem(TicketItem $item): float
|
|
{
|
|
$ruleSnapshot = is_array($item->rule_snapshot_json) ? $item->rule_snapshot_json : [];
|
|
$baseFromRule = isset($ruleSnapshot['base_rebate_rate'])
|
|
? (float) $ruleSnapshot['base_rebate_rate']
|
|
: null;
|
|
|
|
if ($baseFromRule !== null && $baseFromRule > 0) {
|
|
return $this->normalizeRate($baseFromRule);
|
|
}
|
|
|
|
$oddsSnapshot = is_array($item->odds_snapshot_json) ? $item->odds_snapshot_json : [];
|
|
foreach ($oddsSnapshot as $row) {
|
|
if (! is_array($row)) {
|
|
continue;
|
|
}
|
|
|
|
if (! array_key_exists('rebate_rate', $row)) {
|
|
continue;
|
|
}
|
|
|
|
return $this->normalizeRate((float) $row['rebate_rate']);
|
|
}
|
|
|
|
return 0.0;
|
|
}
|
|
|
|
private function normalizeRate(float $rate): float
|
|
{
|
|
return max(0.0, min(1.0, $rate));
|
|
}
|
|
|
|
private function platformWinLoss(TicketItem $item, int $netWin, string $terminalStatus): float
|
|
{
|
|
if ($terminalStatus === 'settled_lose') {
|
|
return (float) max(0, (int) $item->actual_deduct_amount);
|
|
}
|
|
|
|
if ($netWin > 0) {
|
|
return -1 * (float) $netWin;
|
|
}
|
|
|
|
return 0.0;
|
|
}
|
|
}
|