diff --git a/app/app/Http/Controllers/Billing/PakasirController.php b/app/app/Http/Controllers/Billing/PakasirController.php index 26c5e8a..2635030 100644 --- a/app/app/Http/Controllers/Billing/PakasirController.php +++ b/app/app/Http/Controllers/Billing/PakasirController.php @@ -222,6 +222,33 @@ class PakasirController extends Controller return response()->json(['ok' => true, 'cancelled' => true]); } + public function paymentStatus(Request $request): JsonResponse + { + $user = $request->user(); + if (!$user) { + return response()->json(['error' => 'auth_required'], 401); + } + + $orderRef = trim((string) $request->input('order_id', '')); + $query = Order::where('user_id', $user->id)->where('provider', 'pakasir'); + if ($orderRef !== '') { + $query->where('provider_ref', $orderRef); + } + + $order = $query->orderByDesc('id')->first(); + if (!$order) { + return response()->json(['ok' => true, 'found' => false, 'status' => null, 'paid' => false]); + } + + return response()->json([ + 'ok' => true, + 'found' => true, + 'status' => $order->status, + 'paid' => $order->status === 'paid', + 'order_id' => $order->provider_ref, + ]); + } + public function webhook(Request $request): JsonResponse { $payload = $request->all(); @@ -245,18 +272,44 @@ class PakasirController extends Controller private function handlePakasirPayload(array $payload): void { - $status = strtolower((string) ($payload['status'] ?? '')); + $data = $payload; + if (is_array($payload['payment'] ?? null)) { + $data = $payload['payment']; + } elseif (is_array($payload['data'] ?? null)) { + $data = $payload['data']; + } + + $status = strtolower((string) ($data['status'] ?? $payload['status'] ?? '')); if (!in_array($status, ['paid', 'success', 'settlement', 'completed'], true)) { + Log::info('Pakasir webhook ignored: status not paid', [ + 'status' => $status, + 'payload_status' => $payload['status'] ?? null, + ]); return; } - $orderId = (string) ($payload['order_id'] ?? ''); + $orderId = trim((string) ( + $data['order_id'] + ?? $payload['order_id'] + ?? $data['merchant_ref'] + ?? $payload['merchant_ref'] + ?? $data['invoice_id'] + ?? $payload['invoice_id'] + ?? '' + )); if ($orderId === '') { + Log::warning('Pakasir webhook paid event missing order reference', ['payload' => $payload]); return; } $order = Order::where('provider', 'pakasir')->where('provider_ref', $orderId)->first(); if (!$order) { + if (preg_match('/^DW-(\d+)-/', $orderId, $matches) === 1) { + $order = Order::where('provider', 'pakasir')->where('id', (int) $matches[1])->first(); + } + } + if (!$order) { + Log::warning('Pakasir webhook order not found', ['order_id' => $orderId, 'payload' => $payload]); return; } diff --git a/app/resources/views/site/pricing.blade.php b/app/resources/views/site/pricing.blade.php index cfa0ff9..caed561 100644 --- a/app/resources/views/site/pricing.blade.php +++ b/app/resources/views/site/pricing.blade.php @@ -557,6 +557,7 @@ const cancelBtn = document.getElementById('qris-cancel'); let currentOrderId = null; let modalOpen = false; + let pollTimer = null; const openModal = () => { if (!modal) return; @@ -569,6 +570,38 @@ modal.classList.add('hidden'); modal.classList.remove('flex'); modalOpen = false; + if (pollTimer) { + clearInterval(pollTimer); + pollTimer = null; + } + }; + + const startPolling = () => { + if (!currentOrderId) return; + if (pollTimer) { + clearInterval(pollTimer); + } + pollTimer = setInterval(async () => { + if (!modalOpen || !currentOrderId) return; + try { + const res = await fetch("{{ route('billing.pakasir.status') }}", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + ...(csrf ? { 'X-CSRF-TOKEN': csrf } : {}), + }, + body: JSON.stringify({ order_id: currentOrderId }), + }); + const data = await res.json().catch(() => null); + if (res.ok && data?.paid) { + closeModal(); + window.location.href = "{{ route('dashboard.billing', ['status' => 'success']) }}"; + } + } catch (e) { + // keep polling silently + } + }, 4000); }; cancelBtn?.addEventListener('click', async () => { @@ -658,6 +691,7 @@ qrExpiry.textContent = formatted ? `Expires ${formatted}` : 'Complete within 30 minutes'; } openModal(); + startPolling(); btn.disabled = false; btn.textContent = original; } catch (e) { diff --git a/app/routes/web.php b/app/routes/web.php index 56214a0..bafac05 100644 --- a/app/routes/web.php +++ b/app/routes/web.php @@ -41,6 +41,9 @@ Route::middleware('auth')->group(function () { Route::post('/billing/pakasir/cancel', [PakasirController::class, 'cancelPending']) ->middleware('verified') ->name('billing.pakasir.cancel'); + Route::post('/billing/pakasir/status', [PakasirController::class, 'paymentStatus']) + ->middleware('verified') + ->name('billing.pakasir.status'); Route::get('/dashboard/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::patch('/dashboard/profile', [ProfileController::class, 'update'])->name('profile.update');