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, ]; } }