subMinutes($staleMinutes); $orders = TicketOrder::query() ->whereIn('status', ['pending_confirm', 'partial_pending_confirm']) ->where('updated_at', '<=', $cutoff) ->orderBy('id') ->limit($limit) ->get(); $summary = ['scanned' => 0, 'confirmed' => 0, 'refunded' => 0]; foreach ($orders as $order) { $result = DB::transaction(function () use ($order): string { $lockedOrder = TicketOrder::query() ->whereKey($order->id) ->lockForUpdate() ->first(); if ($lockedOrder === null || ! in_array($lockedOrder->status, ['pending_confirm', 'partial_pending_confirm'], true)) { return 'skipped'; } $hasPostedDeduct = WalletTxn::query() ->where('biz_type', 'bet_deduct') ->where('biz_no', $lockedOrder->order_no) ->where('status', 'posted') ->exists(); if ($hasPostedDeduct) { return $this->confirmOrder($lockedOrder); } return $this->refundOrderWithoutDeduct($lockedOrder); }); if ($result === 'skipped') { continue; } $summary['scanned']++; if ($result === 'confirmed') { $summary['confirmed']++; } if ($result === 'refunded') { $summary['refunded']++; } } return $summary; } private function confirmOrder(TicketOrder $lockedOrder): string { $draw = Draw::query()->whereKey($lockedOrder->draw_id)->first(); if ($draw === null || ! $this->drawHallSnapshot->isBettingOpen($draw)) { return $this->refundStalePendingOrder( $lockedOrder, $draw === null ? 'draw_missing' : 'draw_no_longer_open', ); } $items = TicketItem::query() ->where('order_id', $lockedOrder->id) ->where('status', 'pending_confirm') ->lockForUpdate() ->get(); foreach ($items as $item) { $item->forceFill([ 'status' => 'pending_draw', 'fail_reason_code' => null, 'fail_reason_text' => null, ])->save(); $this->jackpotContribution->recordFromPlacedTicketItem($item, $draw, (string) $lockedOrder->currency_code); } $hasFailures = TicketItem::query() ->where('order_id', $lockedOrder->id) ->where('status', 'failed') ->exists(); $lockedOrder->forceFill([ 'status' => $hasFailures ? 'partial_failed' : 'placed', ])->save(); return 'confirmed'; } private function refundStalePendingOrder(TicketOrder $lockedOrder, string $reasonCode): string { $hasPostedDeduct = WalletTxn::query() ->where('biz_type', 'bet_deduct') ->where('biz_no', $lockedOrder->order_no) ->where('status', 'posted') ->exists(); if ($hasPostedDeduct) { $this->ticketWallet->reverseBetDeduct($lockedOrder); } $this->ticketWallet->releaseReservedBetDeduct($lockedOrder, $reasonCode.'_release'); return $this->refundPendingConfirmItems($lockedOrder, $reasonCode); } private function refundOrderWithoutDeduct(TicketOrder $lockedOrder): string { return $this->refundPendingConfirmItems($lockedOrder, 'pending_confirm_timeout'); } private function refundPendingConfirmItems(TicketOrder $lockedOrder, string $reasonCode): string { $items = TicketItem::query() ->where('order_id', $lockedOrder->id) ->where('status', 'pending_confirm') ->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) $lockedOrder->draw_id, $item, $locks); } $item->forceFill([ 'status' => 'refunded', 'fail_reason_code' => $reasonCode, 'fail_reason_text' => $reasonCode.'_refund', 'risk_locked_amount' => 0, ])->save(); } $hasFailures = TicketItem::query() ->where('order_id', $lockedOrder->id) ->where('status', 'failed') ->exists(); $lockedOrder->forceFill([ 'status' => $hasFailures ? 'partial_failed' : 'refunded', ])->save(); return 'refunded'; } }