feat: add idempotency support for settlement adjustments and payments, fix rebate handling
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled

- 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
This commit is contained in:
2026-06-23 16:49:57 +08:00
parent 9a7522e928
commit 3c57941895
20 changed files with 734 additions and 127 deletions

View File

@@ -220,18 +220,29 @@ final class PlayerCreditService
return;
}
$this->decreaseUsedCredit($player, $amountMinor);
$now = now();
DB::table('credit_ledger')->insert([
'owner_type' => 'player',
'owner_id' => $player->id,
'amount' => $amountMinor,
'reason' => 'game_settlement_win',
'ref_type' => 'ticket_item',
'ref_id' => $ticketItemId,
'created_at' => now(),
'updated_at' => now(),
]);
// 先写 credit_ledger以 (ref_type, ref_id, reason) partial unique 索引为幂等闸门。
// 已存在同 (ticket_item, game_settlement_win) 的流水则直接返回,避免并发/重入场景下重复扣减 used_credit。
try {
DB::table('credit_ledger')->insert([
'owner_type' => 'player',
'owner_id' => $player->id,
'amount' => $amountMinor,
'reason' => 'game_settlement_win',
'ref_type' => 'ticket_item',
'ref_id' => $ticketItemId,
'created_at' => $now,
'updated_at' => $now,
]);
} catch (QueryException $e) {
if ($this->isUniqueViolation($e)) {
return;
}
throw $e;
}
$this->decreaseUsedCredit($player, $amountMinor);
}
public function assertMayPlaceBet(Player $player, int $amountMinor): void
@@ -276,38 +287,60 @@ final class PlayerCreditService
return;
}
$this->decreaseUsedCredit($player, $amountMinor);
$now = now();
DB::table('credit_ledger')->insert([
'owner_type' => 'player',
'owner_id' => $player->id,
'amount' => $amountMinor,
'reason' => 'bet_hold_release',
'ref_type' => 'ticket_item',
'ref_id' => $ticketItemId,
'created_at' => now(),
'updated_at' => now(),
]);
// 先写 credit_ledger以 (ref_type, ref_id, reason) partial unique 索引为幂等闸门。
// 已存在同 (ticket_item, bet_hold_release) 的流水则直接返回,避免并发/重入场景下重复扣减 used_credit。
try {
DB::table('credit_ledger')->insert([
'owner_type' => 'player',
'owner_id' => $player->id,
'amount' => $amountMinor,
'reason' => 'bet_hold_release',
'ref_type' => 'ticket_item',
'ref_id' => $ticketItemId,
'created_at' => $now,
'updated_at' => $now,
]);
} catch (QueryException $e) {
if ($this->isUniqueViolation($e)) {
return;
}
throw $e;
}
$this->decreaseUsedCredit($player, $amountMinor);
}
public function reverseBetHold(Player $player, int $amountMinor): void
public function reverseBetHold(Player $player, int $amountMinor, int $ticketOrderId): void
{
if ($amountMinor <= 0 || ! PlayerFundingMode::usesCredit($player)) {
return;
}
$this->decreaseUsedCredit($player, $amountMinor);
$now = now();
DB::table('credit_ledger')->insert([
'owner_type' => 'player',
'owner_id' => $player->id,
'amount' => $amountMinor,
'reason' => 'bet_hold_release',
'ref_type' => 'bet',
'ref_id' => null,
'created_at' => now(),
'updated_at' => now(),
]);
// 先写 credit_ledger以 (ref_type, ref_id, reason) partial unique 索引为幂等闸门。
// 已存在同 (ticket_order, bet_hold_release) 的流水则直接返回,避免并发/重入场景下重复扣减 used_credit。
try {
DB::table('credit_ledger')->insert([
'owner_type' => 'player',
'owner_id' => $player->id,
'amount' => $amountMinor,
'reason' => 'bet_hold_release',
'ref_type' => 'ticket_order',
'ref_id' => $ticketOrderId,
'created_at' => $now,
'updated_at' => $now,
]);
} catch (QueryException $e) {
if ($this->isUniqueViolation($e)) {
return;
}
throw $e;
}
$this->decreaseUsedCredit($player, $amountMinor);
}
public function reverseGameSettlement(Player $player, int $gameWinLossSigned, int $ticketItemId): void
@@ -317,46 +350,51 @@ final class PlayerCreditService
}
$now = now();
$amountMinor = abs($gameWinLossSigned);
if ($gameWinLossSigned > 0) {
$amountMinor = $gameWinLossSigned;
$this->decreaseUsedCredit($player, $amountMinor);
// 先写 credit_ledger以 (ref_type, ref_id, reason) partial unique 索引为幂等闸门。
try {
DB::table('credit_ledger')->insert([
'owner_type' => 'player',
'owner_id' => $player->id,
'amount' => $amountMinor,
'amount' => $gameWinLossSigned > 0 ? $amountMinor : -$amountMinor,
'reason' => 'game_settlement_reversal',
'ref_type' => 'ticket_item',
'ref_id' => $ticketItemId,
'created_at' => $now,
'updated_at' => $now,
]);
} catch (QueryException $e) {
if ($this->isUniqueViolation($e)) {
return;
}
throw $e;
}
if ($gameWinLossSigned > 0) {
$this->decreaseUsedCredit($player, $amountMinor);
return;
}
$amountMinor = abs($gameWinLossSigned);
$currency = (string) $player->default_currency;
$majorDelta = CreditAmountScale::minorToMajor($amountMinor, $currency);
$row = DB::table('player_credit_accounts')
->where('player_id', $player->id)
->lockForUpdate()
->first();
if ($row === null) {
return;
}
DB::table('player_credit_accounts')
->where('player_id', $player->id)
->update([
'used_credit' => DB::raw('used_credit + '.$majorDelta),
'used_credit' => (int) $row->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,
]);
}
/**