- 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
152 lines
5.5 KiB
PHP
152 lines
5.5 KiB
PHP
<?php
|
||
|
||
namespace App\Services\AgentSettlement;
|
||
|
||
use App\Models\Player;
|
||
use App\Services\Player\PlayerCreditService;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Validation\ValidationException;
|
||
|
||
final class SettlementPaymentService
|
||
{
|
||
public function __construct(
|
||
private readonly AgentSettlementBillGuard $billGuard,
|
||
private readonly PlayerCreditService $playerCreditService,
|
||
private readonly PeriodCloseRebateService $periodCloseRebate,
|
||
private readonly AgentSettlementPeriodCompletionService $periodCompletion,
|
||
) {}
|
||
|
||
public function confirmBill(int $billId): void
|
||
{
|
||
$this->billGuard->markConfirmed($billId);
|
||
}
|
||
|
||
/**
|
||
* @param array{method?: string|null, proof?: string|null, remark?: string|null, idempotency_key?: string|null} $meta
|
||
*/
|
||
public function recordPayment(int $billId, int $amount, int $adminUserId, array $meta = []): void
|
||
{
|
||
$idempotencyKey = $meta['idempotency_key'] ?? null;
|
||
|
||
if ($idempotencyKey !== null && $idempotencyKey !== '') {
|
||
$existing = DB::table('payment_records')
|
||
->where('settlement_bill_id', $billId)
|
||
->where('idempotency_key', $idempotencyKey)
|
||
->first();
|
||
if ($existing !== null) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
DB::transaction(function () use ($billId, $amount, $adminUserId, $meta, $idempotencyKey): void {
|
||
if ($idempotencyKey !== null && $idempotencyKey !== '') {
|
||
$existing = DB::table('payment_records')
|
||
->where('settlement_bill_id', $billId)
|
||
->where('idempotency_key', $idempotencyKey)
|
||
->lockForUpdate()
|
||
->first();
|
||
if ($existing !== null) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
$bill = DB::table('settlement_bills')->where('id', $billId)->lockForUpdate()->first();
|
||
if ($bill === null) {
|
||
throw new \InvalidArgumentException('bill_not_found');
|
||
}
|
||
|
||
$this->billGuard->assertPeriodMutable($billId);
|
||
if (! in_array((string) $bill->status, ['confirmed', 'partial_paid', 'overdue'], true)) {
|
||
throw ValidationException::withMessages([
|
||
'bill' => ['not_payable'],
|
||
]);
|
||
}
|
||
|
||
$unpaid = abs((int) $bill->unpaid_amount);
|
||
if ($amount > $unpaid) {
|
||
throw ValidationException::withMessages([
|
||
'amount' => ['exceeds_unpaid'],
|
||
]);
|
||
}
|
||
|
||
$payAmount = $amount;
|
||
if ($payAmount <= 0) {
|
||
return;
|
||
}
|
||
|
||
[$payerType, $payerId, $payeeType, $payeeId] = $this->resolvePayerPayee($bill);
|
||
|
||
DB::table('payment_records')->insert([
|
||
'settlement_bill_id' => $billId,
|
||
'payer_type' => $payerType,
|
||
'payer_id' => $payerId,
|
||
'payee_type' => $payeeType,
|
||
'payee_id' => $payeeId,
|
||
'amount' => $payAmount,
|
||
'method' => $meta['method'] ?? null,
|
||
'proof' => $meta['proof'] ?? null,
|
||
'remark' => $meta['remark'] ?? null,
|
||
'idempotency_key' => $idempotencyKey,
|
||
'status' => 'confirmed',
|
||
'created_by' => $adminUserId,
|
||
'confirmed_by' => $adminUserId,
|
||
'confirmed_at' => now(),
|
||
'created_at' => now(),
|
||
'updated_at' => now(),
|
||
]);
|
||
|
||
$newPaid = (int) $bill->paid_amount + $payAmount;
|
||
$newUnpaid = max(0, (int) $bill->unpaid_amount - $payAmount);
|
||
$status = $newUnpaid === 0 ? 'settled' : 'partial_paid';
|
||
|
||
DB::table('settlement_bills')->where('id', $billId)->update([
|
||
'paid_amount' => $newPaid,
|
||
'unpaid_amount' => $newUnpaid,
|
||
'status' => $status,
|
||
'updated_at' => now(),
|
||
]);
|
||
|
||
if ($bill->owner_type === 'player' && (int) $bill->owner_id > 0) {
|
||
$player = Player::query()->find((int) $bill->owner_id);
|
||
if ($player !== null) {
|
||
if ((int) $bill->net_amount > 0) {
|
||
$this->playerCreditService->releaseFromSettlement($player, $newPaid, $billId);
|
||
} elseif ((int) $bill->net_amount < 0) {
|
||
$this->playerCreditService->applySettlementPayout($player, $newPaid, $billId);
|
||
}
|
||
|
||
if ($status === 'settled') {
|
||
$this->periodCloseRebate->markRebatesSettledForBill($billId);
|
||
}
|
||
}
|
||
}
|
||
|
||
$this->periodCompletion->syncIfReady((int) $bill->settlement_period_id);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* net_amount > 0:owner 应付 counterparty;< 0:counterparty 应付 owner。
|
||
*
|
||
* @return array{0: string, 1: int, 2: string, 3: int}
|
||
*/
|
||
private function resolvePayerPayee(object $bill): array
|
||
{
|
||
if ((int) $bill->net_amount < 0) {
|
||
return [
|
||
(string) $bill->counterparty_type,
|
||
(int) $bill->counterparty_id,
|
||
(string) $bill->owner_type,
|
||
(int) $bill->owner_id,
|
||
];
|
||
}
|
||
|
||
return [
|
||
(string) $bill->owner_type,
|
||
(int) $bill->owner_id,
|
||
(string) $bill->counterparty_type,
|
||
(int) $bill->counterparty_id,
|
||
];
|
||
}
|
||
}
|