IPN có thể không tới: server bạn đang deploy, endpoint 500, firewall chặn, hoặc cổng gặp sự cố. Vì vậy không được coi IPN là cơ chế duy nhất — cần một luồng chủ động hỏi lại.
Luồng kép (push + pull):
1. Khi tạo đơn, ghi payment_status = pending kèm expires_at (thường 15 phút, đúng thời hạn của cổng).
2. Một job định kỳ quét các đơn pending đã quá n phút và gọi API truy vấn giao dịch của cổng (VNPay có API querydr, MoMo có endpoint kiểm tra trạng thái theo orderId) để lấy trạng thái thật.
3. Kết quả trả về success thì chạy đúng hàm xử lý mà IPN gọi, không viết luồng thứ hai song song. Hàm này phải idempotent: cập nhật có điều kiện WHERE status = 'pending' để IPN tới muộn không cộng đơn lần hai.
4. Quá hạn mà cổng báo chưa thanh toán thì chuyển expired, nhả tồn kho hoặc chỗ đã giữ.
UPDATE orders SET status = 'paid', paid_at = now()
WHERE id = $1 AND status = 'pending';
-- 0 dòng bị ảnh hưởng = đã xử lý rồi, bỏ qua phần việc sauGiãn nhịp job theo cấp số nhân (1, 3, 10, 30 phút) thay vì quét dày, tránh gọi API cổng quá nhiều.
Đơn nào sau vài lần vẫn không rõ thì đẩy vào danh sách chờ người xử lý thủ công, và luôn có endpoint nội bộ cho phép admin đối chiếu rồi xác nhận tay — thực tế vận hành ở VN vẫn cần đường này.