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(); $playerTotals = []; $currencyByPlayer = []; 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; $playerTotals[$pid] = ($playerTotals[$pid] ?? 0) + $finalCredit; $currencyByPlayer[$pid] = strtoupper((string) ($item->order?->currency_code ?? 'NPR')); $item->forceFill(['status' => 'settled_win', 'settled_at' => now()])->save(); } elseif ($item->status !== 'settled_lose') { $item->forceFill(['status' => 'settled_lose', 'settled_at' => now()])->save(); } } foreach ($playerTotals as $playerId => $amount) { if ($amount <= 0) { continue; } $player = Player::query()->whereKey($playerId)->firstOrFail(); $this->wallet->creditSettlementPayout($player, $currencyByPlayer[$playerId] ?? 'NPR', $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; } $orderId = TicketItem::query()->where('draw_id', $batch->draw_id)->value('order_id'); $currencyCode = strtoupper((string) (TicketOrder::query() ->whereKey($orderId) ->value('currency_code') ?? 'NPR')); $pool = JackpotPool::query() ->where('currency_code', $currencyCode) ->where('status', 1) ->lockForUpdate() ->first(); if ($pool === null) { return; } $pool->forceFill([ 'current_amount' => (int) $pool->current_amount + $restoreAmount, ])->save(); } }