feat: auto-close qris modal and harden pakasir webhook matching

This commit is contained in:
Dwindi Ramadhana
2026-02-15 00:52:11 +07:00
parent 0e4f3c3599
commit 87aa842e0b
3 changed files with 92 additions and 2 deletions

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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');