where('draw_id', $draw->id) ->whereIn('status', ['settled_win', 'settled_lose', 'pending_payout']) ->exists(); if ($hasBlockedItems) { throw new \RuntimeException('draw_has_settled_tickets'); } $orders = TicketOrder::query() ->where('draw_id', $draw->id) ->whereNotIn('status', ['refunded', 'failed']) ->orderBy('id') ->get(); foreach ($orders as $order) { $this->refundOrder($draw, $order); } } private function refundOrder(Draw $draw, TicketOrder $order): void { $lockedOrder = TicketOrder::query()->whereKey($order->id)->lockForUpdate()->first(); if ($lockedOrder === null || in_array($lockedOrder->status, ['refunded', 'failed'], true)) { return; } $items = TicketItem::query() ->where('order_id', $lockedOrder->id) ->whereIn('status', ['pending_confirm', 'pending_draw', 'pending_payout']) ->with('combinations') ->lockForUpdate() ->get(); foreach ($items as $item) { $locks = []; foreach ($item->combinations as $combo) { $locks[] = [ 'number_4d' => (string) $combo->number_4d, 'amount' => (int) $combo->estimated_payout, ]; } if ($locks !== []) { $this->riskPool->release((int) $draw->id, $item, $locks); } $item->forceFill([ 'status' => 'refunded', 'fail_reason_code' => 'draw_cancelled', 'fail_reason_text' => 'draw_cancelled_refund', 'risk_locked_amount' => 0, ])->save(); } $hasPostedDeduct = WalletTxn::query() ->where('biz_type', 'bet_deduct') ->where('biz_no', $lockedOrder->order_no) ->where('status', 'posted') ->exists(); if ($hasPostedDeduct) { $this->ticketWallet->reverseBetDeduct($lockedOrder); } $lockedOrder->forceFill(['status' => 'refunded'])->save(); } }