xStore.checkout
Quy trình thanh toán bốn bước, hoặc placeOrder() một lần gọi khi giao diện chỉ có một biểu mẫu — rồi pay() để khởi tạo thanh toán ở cổng, và paymentReturn() để đọc kết quả khi khách quay lại từ cổng.
Phương thức
| Phương thức | Cú pháp | Mô tả | Từ phiên bản |
|---|---|---|---|
start |
xStore.checkout.start(cartToken): Promise<CheckoutSession> |
Bước 1 — mở phiên thanh toán từ giỏ hàng, trả về checkout id cho các bước sau. | 1.0 |
setAddress |
xStore.checkout.setAddress(checkoutId, address): Promise<CheckoutSession> |
Bước 2 — đặt địa chỉ giao hàng. Gửi kèm province_code / ward_code (từ shipping.provinces() / shipping.wards()) để có báo giá vận chuyển khớp vùng; thiếu chúng thì rơi về so khớp theo tên, có thể không tìm ra vùng nào. | 1.0 |
setShipping |
xStore.checkout.setShipping(checkoutId, { shipping_tier }): Promise<CheckoutSession> |
Bước 3 — chọn gói vận chuyển theo tier ('instant' | 'same_day' | 'standard' | 'pickup', lấy từ shipping.rates()). MÁY CHỦ tính phí; không còn trường shipping_amount để gửi. shipping_method_id vẫn được chấp nhận (đã lỗi thời) nhưng nay mang giá trị tier — dùng shipping_tier cho mã mới. | 1.0 |
initiatePayment |
xStore.checkout.initiatePayment(checkoutId, { payment_method }): Promise<CheckoutSession> |
Bước 4 — ghi nhận phương thức thanh toán vào phiên. KHÔNG trả về đường dẫn chuyển hướng của cổng thanh toán — đó là việc của checkout.pay() ở bước 6. | 1.0 |
placeOrder |
xStore.checkout.placeOrder(cartToken, { address, shipping, payment }): Promise<PlacedOrderRead> |
Biến giỏ hàng thành ĐƠN HÀNG thật trong MỘT lần gọi — thay cho cả bốn bước ở trên khi giao diện chỉ có một biểu mẫu. Bắt buộc gửi kèm address và payment (shipping tuỳ chọn). An toàn khi bấm hai lần: lần sau trả về đúng đơn đã tạo cho giỏ đó kèm already_placed = true. Chỉ xoá giỏ khi promise này THÀNH CÔNG và có order_id — đừng dựa vào việc phản hồi không có khoá error. Đơn được tạo ở đây CHƯA thanh toán — gọi checkout.pay() ngay sau đó để khởi tạo cổng. | 2.1 |
paymentMethods |
xStore.checkout.paymentMethods(): Promise<CursorPaginatedResponse<PaymentMethodRead>> |
Cổng thanh toán tenant này thực sự bật, để dựng giao diện chọn — đừng viết cứng danh sách cổng. payment_methods trên mỗi dòng là KHOÁ hiển thị của cổng ('qr', 'atm_card', 'credit_card', 'wallet', 'installment', …), không phải tiếng Việt; client tự dịch và luôn cần phương án dự phòng hiện thẳng khoá đó. instruction là dòng hướng dẫn tenant tự nhập (số tài khoản khi chuyển khoản), có thể null. | 1.0 |
pay |
xStore.checkout.pay(cartToken, { provider? }): Promise<PayInitiatedRead> |
Bước 6 — khởi tạo thanh toán ở cổng CHO ĐƠN ĐÃ TẠO bằng placeOrder(). Nhận cartToken, KHÔNG PHẢI order_id: route này không yêu cầu đăng nhập, và nhận order_id trong thân yêu cầu sẽ cho phép bất kỳ ai đoán được id khởi tạo thanh toán hộ người khác. provider tuỳ chọn, mặc định là phương thức đã chọn lúc đặt hàng — truyền rõ để cho khách bấm lại bằng cổng khác mà không cần đặt lại đơn. redirect_url là null với cổng không cần chuyển hướng (COD) — luôn kiểm tra trước khi điều hướng, đừng giả định luôn có URL. | 1.0 |
paymentReturn |
xStore.checkout.paymentReturn({ provider?, query_string }): Promise<PaymentReturnRead> |
Đọc kết quả cổng thanh toán trả về trên trình duyệt khách. query_string phải là chuỗi NGUYÊN VĂN cổng gửi lại (không tự parse rồi build lại) vì chữ ký của cổng tính trên đúng chuỗi đó. Phương thức này KHÔNG GHI GÌ vào đơn hàng — việc chốt đơn do IPN máy-tới-máy của cổng đảm nhiệm, chạy độc lập và có thể tới trước hoặc sau khi khách bấm quay lại. settled: false KHÔNG PHẢI là thất bại, chỉ nghĩa là IPN chưa tới; đọc status ('succeeded' | 'pending' | 'failed' | 'cancelled' | 'unknown') để biết cổng trả lời gì. provider tuỳ chọn — để trống thì máy chủ tự hỏi các cổng tenant đã kết nối. | 1.0 |
Ví dụ đầy đủ
<script>
// Bốn bước PHẢI gọi đúng thứ tự: mỗi bước kiểm tra kết quả của bước trước.
// getToken() chạy cục bộ nên không await.
async function placeOrder(address, shippingTier, paymentMethod) {
const cartToken = xStore.cart.getToken();
if (!cartToken) {
alert('Giỏ hàng đang trống.');
return;
}
try {
// 1. Mở phiên thanh toán từ giỏ hiện tại.
let session = await xStore.checkout.start(cartToken);
// 2. Địa chỉ nhận hàng — gửi kèm province_code / ward_code để có báo giá
// vận chuyển khớp vùng, thay vì rơi về so khớp theo tên.
session = await xStore.checkout.setAddress(session.id, address);
// 3. Gói vận chuyển. MÁY CHỦ tính phí — không còn shipping_amount để gửi.
session = await xStore.checkout.setShipping(session.id, {
shipping_tier: shippingTier,
});
// 4. Ghi nhận phương thức thanh toán vào phiên. KHÔNG trả về đường dẫn
// chuyển hướng — bước 6 (checkout.pay) mới làm việc đó.
session = await xStore.checkout.initiatePayment(session.id, {
payment_method: paymentMethod,
});
// 5. Tạo đơn hàng thật từ các lựa chọn ở trên.
const don = await xStore.checkout.placeOrder(cartToken, {
address,
shipping: { shipping_tier: shippingTier },
payment: { payment_method: paymentMethod },
});
// 6. Khởi tạo thanh toán ở cổng CHO ĐƠN VỪA TẠO. Gửi cartToken, không phải
// order_id — route này không yêu cầu đăng nhập.
const thanhToan = await xStore.checkout.pay(cartToken);
// redirect_url là null với cổng không cần chuyển hướng (COD) — không phải
// lỗi, chỉ là không có đường dẫn để rời trang.
window.location.href = thanhToan.redirect_url || '/tai-khoan/don-hang/' + don.order_id;
} catch (err) {
if (err.status === 401) {
window.location.href = '/dang-nhap?next=' + encodeURIComponent(location.pathname);
} else {
alert(err.message);
}
}
}
// MỘT biểu mẫu, MỘT lần gọi: placeOrder() gộp địa chỉ + vận chuyển + phương
// thức thanh toán, nhưng vẫn cần pay() sau đó để lấy đường dẫn cổng thanh toán.
async function placeOrderInOneCall(address, shippingTier, paymentMethod) {
const cartToken = xStore.cart.getToken();
if (!cartToken) return;
const don = await xStore.checkout.placeOrder(cartToken, {
address,
shipping: { shipping_tier: shippingTier },
payment: { payment_method: paymentMethod },
});
// Bấm hai lần vẫn an toàn: lần sau trả về đúng đơn cũ.
console.log(don.code, don.already_placed);
const thanhToan = await xStore.checkout.pay(cartToken);
window.location.href = thanhToan.redirect_url || '/tai-khoan/don-hang/' + don.order_id;
}
// Danh sách cổng thanh toán tenant này đã bật — dùng để dựng giao diện chọn,
// đừng viết cứng danh sách cổng trong giao diện.
async function renderPaymentMethods() {
const trang = await xStore.checkout.paymentMethods();
document.getElementById('payment').innerHTML = trang.items.map((pm) => `
<label>
<input type="radio" name="payment_method" value="${pm.code}">
${pm.label}
${pm.instruction ? `<small>${pm.instruction}</small>` : ''}
</label>
`).join('');
}
// Trang khách quay về sau khi rời cổng thanh toán.
async function renderPaymentResult() {
// ⚠️ query_string là chuỗi NGUYÊN VĂN — không tự parse rồi build lại,
// chữ ký của cổng tính trên đúng chuỗi cổng gửi.
const ketQua = await xStore.checkout.paymentReturn({
query_string: window.location.search.slice(1),
});
// ⚠️ paymentReturn KHÔNG GHI GÌ — nó chỉ đọc lại trạng thái, đơn được chốt
// bằng IPN máy-tới-máy của cổng, chạy độc lập và có thể tới trước hoặc sau
// khi khách bấm quay lại.
//
// ⚠️ settled: false KHÔNG PHẢI là thất bại — chỉ nghĩa là IPN chưa tới.
// Hiển thị "đang xác nhận", ĐỪNG hiển thị "thất bại".
const el = document.getElementById('result');
if (!ketQua.settled) {
el.textContent = 'Đơn hàng đang được xác nhận…';
} else if (ketQua.status === 'succeeded') {
el.textContent = `Thanh toán thành công cho đơn ${ketQua.code}.`;
} else {
el.textContent = 'Thanh toán chưa hoàn tất: ' + ketQua.status;
}
}
</script>
Thư viện gọi tới /api/... trên chính tên miền cửa hàng, nên không cần khai báo địa chỉ máy chủ ở bất kỳ đâu.
Xem thêm Sự kiện để giao diện tự cập nhật sau mỗi thao tác.