Chuyển tới nội dung chính

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:

  1. Đă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ằng X-Merchant-Key của merchant cha → 403.
  2. App phải đăng ký FCM token qua card-core (POST /merchant/devices) — không gọi thẳng Notification Center.
  3. 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.
  4. Thanh toán không đổi, chỉ cần gửi external_ref và enhanced_data lấy từ payment_template của cuốc.
  5. 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.typeTiê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_statusoffered (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.statusdispatching 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_atnull = đón ngay; có giá trị = giờ hẹn
sell_priceGiá ước tính tổng đài báo khách — đồng hồ mới là giá thật
noteLời dặn của tổng đài
payment_templateChỉ 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ến assigned. 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" khi offer_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"
}
}
  1. Tài xế nhập số tiền theo đồng hồ như hiện nay.
  2. App tạo thanh toán bằng luồng cũ — POST /merchant/payment-intents (QR) hoặc POST /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_km nếu có)
    • không gửi sub_account_id khác — giao dịch phải do chính tài xế này tạo
  3. 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ỗiNghĩaApp xử lý
409 payment_not_foundChưa có giao dịch đã capture mang mã cuốcChưa thanh toán xong — chờ / hướng dẫn thu tiền
409 payment_not_settledGiao 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_mismatchGiao 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​

HTTPcodeKhi nào
401unauthorizedToken hết hạn — refresh như mọi API /merchant khác
403driver_session_required / sub_account_session_requiredGọi bằng merchant key thay vì token tài xế
404not_foundCuốc không tồn tại hoặc không phải của bạn
409invalid_assignment_state / invalid_booking_stateCuốc đã đổi trạng thái — tải lại
409payment_*Xem §5
503payments_unavailable / push_unavailableServer chưa bật tính năng — báo backend

7. Cấu hình (backend)​

NơiBiếnGhi chú
card-coreNOTIFICATION_CENTER_ORIGINĐã có (OTP/Telegram)
card-coreNOTIFICATION_CENTER_PUSH_KEYKey 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-centerEXTERNAL_PUSH_API_KEYSDanh 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-centermigration AddExternalRecipients1791600000000Chạy tự động khi boot