App tài xế XPay Merchant — nhận cuốc từ tổng đài
Tài liệu dành cho đội app XPay Merchant (phần tài xế). Tổng đài (CS Portal) chọn một tài xế XPay cho chuyến; tài xế nhận push, nhận/từ chối cuốc trong app, chạy chuyến, rồi thu tiền bằng đúng luồng thanh toán hiện có của app. Chuyến tự đóng khi giao dịch đã capture.
Năm điều quan trọng:
- Đăng nhập như hiện tại (
/merchant/login, token sub-account của tài xế). Không có token mới. Gọi bằngX-Merchant-Keycủa merchant cha →403. - App phải đăng ký FCM token qua card-core (
POST /merchant/devices) — không gọi thẳng Notification Center. - Push chỉ là lời nhắc. Nguồn sự thật là
GET /merchant/rides— app vẫn nên tải lại danh sách khi mở app / quay lại foreground. - Thanh toán không đổi, chỉ cần gửi
external_refvàenhanced_datalấy từpayment_templatecủa cuốc. - Tài xế chỉ thấy cuốc của chính mình; id cuốc của người khác trả
404.
Contract đầy đủ: GET /swagger (tag merchant).
1. Luồng
CS chọn tài xế ──push "Cuốc xe mới XP-1048"──▶ app
│ GET /merchant/rides/:id
┌───────────┴───────────┐
POST …/accept POST …/decline ──▶ cuốc về lại tổng đài
(hoặc tổng đài nhận giùm)
│
POST …/pickup (đã đón khách)
│
thu tiền: luồng thanh toán cũ với payment_template
│
POST …/complete ──▶ chuyến đóng, final_fare = số đã capture
CS có thể rút cuốc bất cứ lúc nào trước khi xong (đổi tài xế, khách huỷ) → app nhận push
DISPATCH.RIDE_WITHDRAWN, cuốc chuyển offer_status: "withdrawn".
Tổng đài cũng có thể nhận giùm (tài xế đã đồng ý qua điện thoại): cuốc chuyển
offer_status: "accepted" mà tài xế không cần bấm nhận — không có push riêng, app thấy khi
tải lại cuốc. Từ đó làm tiếp như đã tự nhận: đón khách → thu tiền → kết thúc.
2. Đăng ký thiết bị
POST /merchant/devices
Authorization: Bearer <access token tài xế>
{ "device_token": "<FCM registration token>", "platform": "android" }
- Gọi sau mỗi lần đăng nhập và mỗi khi FCM đổi token (
onTokenRefresh). - Mỗi tài khoản giữ 1 thiết bị cho mỗi
platform(android/ios/web); đăng ký thiết bị mới thay thiết bị cũ. Token đang gắn với tài khoản khác sẽ được chuyển sang tài khoản này (điện thoại đổi người dùng thì không nhận nhầm cuốc của người trước). - Đăng xuất:
POST /merchant/devices/deactivate { "device_token": "…" }. - Lỗi:
403 sub_account_session_required(đang dùng merchant key),503 push_unavailable(server chưa cấu hình push),502 push_gateway_error(Notification Center lỗi — thử lại sau).
Push nhận được
data.type | Tiêu đề (vi) | Làm gì |
|---|---|---|
ride_offer | "Cuốc xe mới XP-1048" — "Đón {điểm đón} → {điểm đến} lúc {giờ}. Mở app để nhận cuốc." | Mở GET /merchant/rides/{data.assignment_id} |
ride_withdrawn | "Cuốc XP-1048 đã huỷ" | Tải lại cuốc; đóng màn hình cuốc nếu đang mở |
data luôn có { type, booking_id, assignment_id, ref } (đều là chuỗi).
3. Danh sách và chi tiết cuốc
GET /merchant/rides?scope=active ← mặc định: đang chờ nhận + đang chạy
GET /merchant/rides?scope=history ← đã xong / đã từ chối / bị rút (50 gần nhất)
GET /merchant/rides/{assignment_id}
{
"assignment_id": "9c1e…",
"offer_status": "offered",
"booking": {
"id": "b7d2…",
"ref": "XP-1048",
"status": "dispatching",
"rider_name": "Lê Hoàng Tuấn",
"rider_phone": "+84912345678",
"pickup_name": "SATRA Đồng Khởi",
"pickup_address": "58 Đồng Khởi, Phường Sài Gòn, TP.HCM",
"pickup_lat": null, "pickup_lng": null,
"dropoff_name": "Sân bay Tân Sơn Nhất, ga quốc nội",
"dropoff_address": null,
"dropoff_lat": null, "dropoff_lng": null,
"pickup_at": null,
"seats": 4,
"note": null,
"final_fare": null
},
"vehicle_plate": "51G-246.80",
"vehicle_model": "Toyota Vios",
"pickup_eta": null,
"sell_price": "200000.00",
"note": "Khách đứng sảnh B",
"offered_at": "2026-10-09T06:58:12.000Z",
"responded_at": null,
"payment_template": null
}
| Field | Ý nghĩa |
|---|---|
offer_status | offered (chờ bạn trả lời) · accepted (bạn nhận, hoặc tổng đài nhận giùm) · declined · withdrawn (tổng đài đã rút) |
booking.status | dispatching khi đang chờ bạn nhận · assigned · riding · completed · cancelled · pending (đã về lại tổng đài) |
booking.rider_* | Người đi — gọi người này khi tới đón (đặt hộ thì là người được đặt hộ) |
booking.pickup_at | null = đón ngay; có giá trị = giờ hẹn |
sell_price | Giá ước tính tổng đài báo khách — đồng hồ mới là giá thật |
note | Lời dặn của tổng đài |
payment_template | Chỉ có khi offer_status = accepted và chuyến chưa đóng (§5) |
Thời gian là ISO UTC — hiển thị theo giờ Việt Nam.
4. Nhận, từ chối, đón khách
POST /merchant/rides/{assignment_id}/accept { "pickup_eta": "2026-10-09T14:05:00+07:00" } ← pickup_eta tuỳ chọn
POST /merchant/rides/{assignment_id}/decline { "reason": "Đang chở khách khác" }
POST /merchant/rides/{assignment_id}/pickup
accept: cuốc thuộc về bạn, chuyếnassigned. Xe trên cuốc là xe trong hồ sơ tài xế. Cuốc tổng đài đã nhận giùm thì trả về cuốc như cũ (không lỗi) — app nên ẩn nút "Nhận cuốc" khioffer_status = accepted.decline: dùng cả khi trả lại cuốc đã nhận (xe hỏng, không tới được) — cuốc về lại tổng đài. Không làm được khi đã đón khách (riding).pickup: đã đón khách →riding.- Mọi lệnh trả về cuốc đã cập nhật.
409 invalid_assignment_state= cuốc đã bị rút hoặc đã có người khác nhận → tải lại cuốc.
5. Thu tiền và kết thúc chuyến
Sau khi nhận cuốc, payment_template có dạng:
{
"external_ref": "XP-1048",
"product_type": "taxi_trip",
"enhanced_data": {
"type": "taxi_trip",
"pickup_location": "SATRA Đồng Khởi",
"destination": "Sân bay Tân Sơn Nhất, ga quốc nội",
"plate_number": "51G-246.80"
}
}
- Tài xế nhập số tiền theo đồng hồ như hiện nay.
- App tạo thanh toán bằng luồng cũ —
POST /merchant/payment-intents(QR) hoặcPOST /merchant/payments/authorize— với:external_ref=payment_template.external_ref(bắt buộc giữ nguyên, đây là khoá khớp)product_type="taxi_trip"enhanced_data=payment_template.enhanced_data+fare,sub_fee(vàdistance_kmnếu có)- không gửi
sub_account_idkhác — giao dịch phải do chính tài xế này tạo
- Khi giao dịch đã captured (QR đã quét / thẻ đã xác thực xong), gọi:
POST /merchant/rides/{assignment_id}/complete { "auth_id": "<auth_id của giao dịch>" }
auth_id tuỳ chọn — bỏ trống thì server tự tìm giao dịch đã capture mang external_ref của
cuốc. Server chỉ đóng chuyến khi giao dịch cùng merchant, do chính tài xế này tạo, đúng
external_ref, đã capture; final_fare = số tiền thực capture.
| Lỗi | Nghĩa | App xử lý |
|---|---|---|
409 payment_not_found | Chưa có giao dịch đã capture mang mã cuốc | Chưa thanh toán xong — chờ / hướng dẫn thu tiền |
409 payment_not_settled | Giao dịch còn chờ xác thực (details.status) | Đợi khách nhập OTP/PIN rồi gọi lại |
409 payment_mismatch | Giao dịch không phải của cuốc này (sai external_ref, tài xế khác, merchant khác) | Kiểm tra lại external_ref lúc tạo thanh toán |
Tổng đài vẫn có thể đóng chuyến thủ công nếu cần.
6. Bảng lỗi chung
| HTTP | code | Khi nào |
|---|---|---|
| 401 | unauthorized | Token hết hạn — refresh như mọi API /merchant khác |
| 403 | driver_session_required / sub_account_session_required | Gọi bằng merchant key thay vì token tài xế |
| 404 | not_found | Cuốc không tồn tại hoặc không phải của bạn |
| 409 | invalid_assignment_state / invalid_booking_state | Cuốc đã đổi trạng thái — tải lại |
| 409 | payment_* | Xem §5 |
| 503 | payments_unavailable / push_unavailable | Server chưa bật tính năng — báo backend |
7. Cấu hình (backend)
| Nơi | Biến | Ghi chú |
|---|---|---|
| card-core | NOTIFICATION_CENTER_ORIGIN | Đã có (OTP/Telegram) |
| card-core | NOTIFICATION_CENTER_PUSH_KEY | Key gửi kèm X-Service-Key; thiếu → không push (cuốc vẫn hiện khi app tải danh sách) |
| notification-center | EXTERNAL_PUSH_API_KEYS | Danh sách key được phép gọi /external-push (chứa key của card-core). Thiếu → từ chối mọi request |
| notification-center | migration AddExternalRecipients1791600000000 | Chạy tự động khi boot |