feat: auto-close qris modal and harden pakasir webhook matching
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user