POS Integration: Vì sao cần thiết?
Phần mềm POS truyền thống và thanh toán số vẫn còn nhiều khoảng cách:
- Thu ngân nhập số tiền thủ công vào máy POS → dễ nhầm lẫn
- Khách thanh toán QR nhưng nhân viên phải kiểm tra app ngân hàng để xác nhận
- Không đối soát được giao dịch ngân hàng với đơn hàng POS
Tích hợp TConnect API vào POS giải quyết tất cả: số tiền từ đơn hàng tự động điền vào QR, xác nhận thanh toán tự động, đơn hàng đóng ngay sau khi nhận tiền.
Kiến trúc tích hợp POS
[POS Software]
│
│ 1. Tạo đơn hàng
▼
POST /openapi/v1/qr/create
│
│ 2. Nhận QR code + order binding
▼
[Hiển thị QR trên màn hình khách / in QR]
│
│ 3. Khách quét & thanh toán
▼
[TConnect IPN] → [POS Backend] → Đóng đơn tự độngHai mô hình phổ biến
Mô hình 1: POS Software — tích hợp qua API, hiển thị QR trên màn hình.
Mô hình 2: POS Hardware — máy POS vật lý (terminal) kết nối qua TConnect SDK.
Implement: POS Software Integration
1. Tạo Virtual Account cho mỗi quầy
// Chạy một lần khi setup quầy — không cần tạo lại mỗi ngày
function setup_cashier_va(cashier_id, cashier_name):
va = tconnect.create_va(
bank_code: "970454", // BIDV — thay theo ngân hàng merchant
account_name: "QUAY {cashier_id} - {uppercase(cashier_name)}"
)
// Lưu va_number vào database của quầy
db.update_cashier(cashier_id, va_number=va.va_number)
return va.va_number
// Kết quả: QUAY 01 - THU NGAN A → VA3001252. Tạo QR khi checkout
// Tạo QR động cho từng đơn hàng.
// order_id encode cả cashier + invoice để dễ đối soát.
function create_checkout_qr(cashier, order):
order_id = "{cashier.store_code}-{cashier.id}-{order.invoice_id}"
qr = tconnect.create_qr(
order_id: order_id,
va: cashier.va_number,
bincode: "970454",
service_code: "bidv-qr",
amount: order.total_amount
)
return {
qr_image: qr.image_png_base64,
qr_content: qr.qr_content,
order_id: order_id,
amount: order.total_amount,
expires_at: current_time() + 15 minutes
}3. Hiển thị QR trên màn hình
// Component màn hình quầy: hiển thị QR và đếm ngược thời gian
function CheckoutQR(qrData, onPaymentConfirmed):
state timeLeft = 900 // 15 phút tính bằng giây
state paid = false
// Countdown timer: giảm timeLeft mỗi giây
on mount:
start timer every 1 second:
if timeLeft > 0: decrease timeLeft by 1
// Lắng nghe IPN qua WebSocket
on mount:
connect to WebSocket at "wss://api.yourpos.com/ws/payment"
on message received:
data = parse_json(message)
if data.type == "payment_confirmed":
set paid = true
wait 2 seconds then call onPaymentConfirmed(data.order_id)
on unmount: close WebSocket
render:
if paid:
display "Thanh toán thành công!"
else:
display QR image from "data:image/png;base64," + qrData.qr_image
display formatCurrency(qrData.amount)
if timeLeft > 0:
display "Hết hạn sau {minutes}:{seconds}"
else:
display "QR đã hết hạn — tạo QR mới"4. Xử lý IPN và đóng đơn
// IPN endpoint: POST /webhook/tconnect/ipn
function ipn_handler(request):
body = parse_json(request)
txn = decrypt_aes(body.data, AES_KEY)
// Parse order_id format: STORE01-CASH01-INV12345
parts = split(txn.order_id, separator="-")
store_code = parts[0]
cashier_id = parts[1]
invoice_id = parts[2]
if txn.status == "SUCCESS":
// Đóng đơn trong POS
pos_db.close_invoice(
invoice_id: invoice_id,
payment_method: "BANK_TRANSFER",
payment_ref: txn.retrieval_ref_no,
paid_amount: txn.amount
)
// Notify màn hình quầy qua WebSocket
ws_manager.notify_cashier(
cashier_id: cashier_id,
event: {
type: "payment_confirmed",
order_id: txn.order_id,
amount: txn.amount
}
)
return { status: "ok" }Đối soát cuối ca (End-of-Day Reconciliation)
// Đối soát tất cả giao dịch trong ca làm việc
function reconcile_shift(store_code, cashier_id, shift_start, shift_end):
// Lấy tất cả IPN đã nhận trong ca
ipn_transactions = db.get_ipn_transactions(
cashier_id: cashier_id,
from_time: shift_start,
to_time: shift_end
)
// Lấy tất cả đơn hàng trong ca từ POS
pos_invoices = pos_db.get_invoices(
cashier_id: cashier_id,
from_time: shift_start,
to_time: shift_end,
payment_method: "BANK_TRANSFER"
)
// Đối chiếu — xây map theo order_id
ipn_by_order = map each txn by txn.order_id
pos_by_invoice = map each invoice by "STORE01-{cashier_id}-{invoice.id}"
matched = []
unmatched_ipn = []
unmatched_pos = []
for each (order_id, ipn) in ipn_by_order:
if order_id exists in pos_by_invoice:
pos = pos_by_invoice[order_id]
if ipn.amount == pos.total:
append { order_id, amount: ipn.amount } to matched
else:
// Số tiền không khớp — cần review
append { order_id, ipn_amount: ipn.amount,
pos_amount: pos.total, discrepancy: true } to matched
else:
append ipn to unmatched_ipn
return {
total_matched: count(matched),
total_matched_amount: sum of matched.amount where discrepancy is not set,
discrepancies: filter matched where discrepancy == true,
unmatched_ipn: unmatched_ipn,
unmatched_pos: unmatched_pos
}Tích hợp POS Hardware (Terminal vật lý)
Với máy POS vật lý (Android terminal), tích hợp qua TConnect POS SDK:
// Android (Java/Kotlin)
TConnectPOS pos = new TConnectPOS.Builder()
.setPartnerCode("YOUR_PARTNER_CODE")
.setAesKey(AES_KEY_BYTES)
.setEnvironment(Environment.PRODUCTION)
.build();
// Khi khách checkout
PaymentRequest request = new PaymentRequest.Builder()
.setOrderId("ORDER_001")
.setAmount(500000L)
.setCurrency("VND")
.setVaNumber("VA300125")
.build();
pos.requestPayment(request, new PaymentCallback() {
@Override
public void onSuccess(PaymentResult result) {
// result.getTransactionId()
// result.getAmount()
closeInvoice(result);
}
@Override
public void onFailed(PaymentError error) {
showError(error.getMessage());
}
});Checklist trước khi go-live
- Test QR tạo đúng với số tiền từ POS
- Test IPN nhận và đóng đơn tự động
- Test WebSocket notify màn hình quầy
- Test timeout QR và tạo QR mới
- Implement đối soát cuối ca
- Backup plan khi internet down (fallback sang tiền mặt)
- Training nhân viên quy trình xử lý khi QR lỗi
Kết luận
Tích hợp POS với TConnect API không yêu cầu thay đổi toàn bộ hệ thống hiện tại — chỉ cần thêm một layer API call khi tạo đơn và một IPN handler để đóng đơn. Kết quả: thu ngân nhanh hơn, ít sai sót hơn, và báo cáo đối soát tự động cuối ca.