approveInternal($batch, $admin->id, $remark); } public function approveBySystem(SettlementBatch $batch, ?string $remark = null): SettlementBatch { return $this->approveInternal($batch, null, $remark); } private function approveInternal(SettlementBatch $batch, ?int $reviewedBy, ?string $remark): SettlementBatch { return DB::transaction(function () use ($batch, $reviewedBy, $remark): SettlementBatch { /** @var SettlementBatch $locked */ $locked = SettlementBatch::query()->whereKey($batch->id)->lockForUpdate()->firstOrFail(); if ($locked->status !== SettlementBatchStatus::PendingReview->value) { throw new \RuntimeException('settlement_not_pending_review'); } $locked->forceFill([ 'status' => SettlementBatchStatus::Approved->value, 'review_status' => 'approved', 'reviewed_by' => $reviewedBy, 'reviewed_at' => now(), 'review_remark' => $remark, ])->save(); return $locked->refresh(); }); } public function reject(SettlementBatch $batch, AdminUser $admin, ?string $remark = null): SettlementBatch { return DB::transaction(function () use ($batch, $admin, $remark): SettlementBatch { /** @var SettlementBatch $locked */ $locked = SettlementBatch::query()->whereKey($batch->id)->lockForUpdate()->firstOrFail(); if ($locked->status !== SettlementBatchStatus::PendingReview->value) { throw new \RuntimeException('settlement_not_pending_review'); } $itemIds = $locked->details()->pluck('ticket_item_id')->map(fn ($id) => (int) $id)->all(); if ($itemIds !== []) { TicketItem::query() ->whereIn('id', $itemIds) ->update([ 'status' => 'pending_draw', 'win_amount' => 0, 'jackpot_win_amount' => 0, 'settled_at' => null, ]); } $this->restoreJackpotPoolAfterReject($locked); $locked->forceFill([ 'status' => SettlementBatchStatus::Rejected->value, 'review_status' => 'rejected', 'reviewed_by' => $admin->id, 'reviewed_at' => now(), 'review_remark' => $remark, ])->save(); return $locked->refresh(); }); } public function payout(SettlementBatch $batch): SettlementBatch { return DB::transaction(function () use ($batch): SettlementBatch { /** @var SettlementBatch $locked */ $locked = SettlementBatch::query()->whereKey($batch->id)->lockForUpdate()->firstOrFail(); if ($locked->status !== SettlementBatchStatus::Approved->value || $locked->review_status !== 'approved') { throw new \RuntimeException('settlement_not_approved'); } $batchItemIds = $locked->details()->pluck('ticket_item_id')->map(fn ($id) => (int) $id)->all(); $hasUnsettled = TicketItem::query() ->where('draw_id', $locked->draw_id) ->whereIn('status', [ 'pending_confirm', 'partial_pending_confirm', 'pending_draw', ]) ->exists(); if ($hasUnsettled) { throw new \RuntimeException('draw_has_unsettled_tickets'); } if ($batchItemIds !== []) { $orphanPendingPayout = TicketItem::query() ->where('draw_id', $locked->draw_id) ->where('status', 'pending_payout') ->whereNotIn('id', $batchItemIds) ->exists(); if ($orphanPendingPayout) { throw new \RuntimeException('draw_has_unsettled_tickets'); } } $details = $locked->details()->with(['ticketItem.order'])->get(); $playerCurrencyTotals = []; foreach ($details as $detail) { $item = $detail->ticketItem; if ($item === null) { continue; } $finalCredit = (int) $detail->win_amount + (int) $detail->jackpot_allocation_amount; if ($finalCredit > 0) { $pid = (int) $item->player_id; $currency = strtoupper((string) ($item->order?->currency_code ?? 'NPR')); $aggregateKey = $pid.':'.$currency; if (! isset($playerCurrencyTotals[$aggregateKey])) { $playerCurrencyTotals[$aggregateKey] = [ 'player_id' => $pid, 'currency_code' => $currency, 'amount' => 0, ]; } $playerCurrencyTotals[$aggregateKey]['amount'] += $finalCredit; $item->forceFill(['status' => 'settled_win', 'settled_at' => now()])->save(); } elseif ($item->status !== 'settled_lose') { $item->forceFill(['status' => 'settled_lose', 'settled_at' => now()])->save(); } } foreach ($playerCurrencyTotals as $entry) { $amount = (int) $entry['amount']; if ($amount <= 0) { continue; } $player = Player::query()->whereKey((int) $entry['player_id'])->firstOrFail(); $this->wallet->creditSettlementPayout( $player, (string) $entry['currency_code'], $amount, (int) $locked->id ); } $orderIds = TicketItem::query() ->whereIn('id', $locked->details()->pluck('ticket_item_id')) ->pluck('order_id') ->unique() ->all(); foreach ($orderIds as $orderId) { $pending = TicketItem::query() ->where('order_id', $orderId) ->whereNotIn('status', ['settled_win', 'settled_lose']) ->exists(); if (! $pending) { TicketOrder::query()->whereKey($orderId)->update(['status' => 'settled']); } } $locked->forceFill([ 'status' => SettlementBatchStatus::Paid->value, 'paid_at' => now(), ])->save(); Draw::query()->whereKey($locked->draw_id)->update([ 'status' => DrawStatus::Settled->value, 'settle_version' => (int) $locked->settle_version, ]); return $locked->refresh(); }); } private function restoreJackpotPoolAfterReject(SettlementBatch $batch): void { $restoreAmount = (int) $batch->total_jackpot_payout_amount; if ($restoreAmount <= 0) { return; } $details = $batch->details()->with(['ticketItem.order'])->get(); $restoreByCurrency = []; foreach ($details as $detail) { $amount = (int) $detail->jackpot_allocation_amount; if ($amount <= 0) { continue; } $currency = strtoupper((string) ($detail->ticketItem?->order?->currency_code ?? 'NPR')); $restoreByCurrency[$currency] = ($restoreByCurrency[$currency] ?? 0) + $amount; } if ($restoreByCurrency === []) { return; } foreach ($restoreByCurrency as $currency => $amount) { $pool = JackpotPool::query() ->where('currency_code', $currency) ->where('status', 1) ->lockForUpdate() ->first(); if ($pool === null) { continue; } $pool->forceFill([ 'current_amount' => (int) $pool->current_amount + (int) $amount, ])->save(); } } }