Đơn hàng
Tổng quan
| Method + path | Scope | Dùng để |
|---|---|---|
GET /orders | orders_list | Liệt kê đơn để đồng bộ (keyset), hoặc tra một đơn theo orderCode |
GET /orders/:id | orders_detail | Một đơn kèm dòng hàng và địa chỉ giao |
GET /order-statuses | orders_list | Danh mục trạng thái đơn của cửa hàng |
PUT /orders | orders_create + orders_update | Tạo hoặc cập nhật tối đa 50 đơn mỗi lần |
GET /orders/:id/payments | orders_detail | Kỳ thanh toán và lịch sử thanh toán của một đơn |
GET /payments | orders_list | Liệt kê khoản thanh toán của cửa hàng để đồng bộ (keyset), gồm cả khoản đã xoá |
POST /orders/:id/payments | orders_update | Ghi một khoản thanh toán cho đơn |
DELETE /orders/:id/payments/:paymentId | orders_update | Xoá một khoản thanh toán |
GET /payment-methods | orders_list | Danh mục phương thức thanh toán |
Chuẩn bị dữ liệu để tạo đơn
| Cần | Lấy ở |
|---|---|
| Khách của đơn | GET /customers, hoặc tạo trước bằng POST /customers |
variantId / sku của dòng hàng | GET /products |
shippingProvinceCode / shippingWardCode | GET /address/provinces, GET /address/wards |
statusId | GET /order-statuses |
Đơn tạo qua Open API không đi qua bước duyệt và không bắn thông báo nội bộ như đơn tạo trên giao diện; nguồn đơn ghi là api:<tên key>.
Đối tượng đơn hàng
{
"id": "b7c2…",
"orderCode": "DH-1001",
"customerId": "1f0c6a7e-5d1b-4a8e-9a63-2c1f0f6b9d10",
"statusId": "5a21…",
"paymentStatus": "unpaid",
"subtotal": 380000,
"taxAmount": 6400,
"shippingFee": 30000,
"discountAmount": 0,
"totalAmount": 416400,
"paidAmount": 0,
"debtAmount": 416400,
"currency": "VND",
"shipping": {
"name": "Nguyễn Thị Lan",
"phone": "0901234567",
"address": "12 Lý Thường Kiệt",
"provinceCode": "01",
"province": "Thành phố Hà Nội",
"wardCode": "00070",
"ward": "Phường Hoàn Kiếm"
},
"assignedStaffId": "7a2d…",
"source": "api:ERP Kho",
"items": [
{
"id": "e1f0…", "productId": "c3a9…", "variantId": "3e8b…",
"productName": "Áo thun basic", "variantName": "Trắng / M", "sku": "AO-THUN-M",
"quantity": 2, "unitPrice": 150000, "discountAmount": 0, "vatRate": 0,
"taxAmount": 0, "totalPrice": 300000, "notes": null
}
],
"createdAt": "2026-10-08T03:00:00.000Z",
"updatedAt": "2026-10-08T03:12:45.123Z"
}
source là nguồn tạo đơn (ui hoặc api:<tên key>). Tên tỉnh / phường do hệ thống tra từ mã, không lấy từ dữ liệu gửi lên. paidAmount, debtAmount và paymentStatus (unpaid · partially_paid · paid) do hệ thống tính từ lịch sử thanh toán.
Đọc đơn
GET /api/open/v1/orders?updatedSince=2026-10-01T00:00:00Z&updatedUntil=2026-10-08T00:00:00Z&limit=50&excludeSource=api:ERP%20Kho
GET /api/open/v1/orders?orderCode=DH-1001
GET /api/open/v1/orders/b7c2…
- Liệt kê theo keyset, trả
{ "data": [đơn…], "nextCursor": …, "window": {…} }— tối đa 50 đơn / lần, cửa sổ tối đa 30 ngày.excludeSourcebỏ các đơn được tạo từ nguồn đó. ?orderCode=trả{ "data": đơn | null }.GET /orders/:idtrả{ "data": đơn }, hoặc404 OPEN_API_ORDER_NOT_FOUND.
Key chỉ thấy những đơn mà người tạo key được thấy (theo phạm vi phân quyền đơn hàng).
Danh mục trạng thái
GET /api/open/v1/order-statuses
{
"data": [
{ "id": "0d6f…", "label": "Mới", "isInitial": true, "isCompleted": false, "isCancelled": false },
{ "id": "5a21…", "label": "Đang giao", "isInitial": false, "isCompleted": false, "isCancelled": false },
{ "id": "9c40…", "label": "Hoàn thành", "isInitial": false, "isCompleted": true, "isCancelled": false },
{ "id": "e117…", "label": "Đã huỷ", "isInitial": false, "isCompleted": false, "isCancelled": true }
]
}
Gửi statusId lấy từ danh mục này. Đơn mới luôn bắt đầu ở trạng thái isInitial.
Tạo hoặc cập nhật đơn
PUT /api/open/v1/orders
- Request Body
- Response
{
"orders": [
{
"orderCode": "DH-1001",
"customer": { "matchField": "customerCode", "value": "KH-001" },
"items": [
{ "sku": "AO-THUN-M", "quantity": 2, "unitPrice": 150000 },
{ "variantId": "3e8b…", "quantity": 1, "unitPrice": 90000, "discountAmount": 10000, "vatRate": 8 }
],
"shippingFee": 30000,
"shippingName": "Nguyễn Thị Lan",
"shippingPhone": "0901234567",
"shippingAddress": "12 Lý Thường Kiệt",
"shippingProvinceCode": "01",
"shippingWardCode": "00070",
"statusId": "5a21…"
},
{
"customer": { "id": "1f0c6a7e-5d1b-4a8e-9a63-2c1f0f6b9d10" },
"items": [{ "sku": "AO-THUN-L", "quantity": 1, "unitPrice": 150000 }]
}
]
}
{
"results": [
{ "index": 0, "status": "updated", "id": "b7c2…", "orderCode": "DH-1001" },
{ "index": 1, "status": "created", "id": "c90a…", "orderCode": "ORD-20261008-K3P9QZ" }
]
}
Nhận diện đơn bằng orderCode
| Gửi | Kết quả |
|---|---|
orderCode trùng một đơn đang có | Cập nhật đơn đó |
orderCode chưa có | Tạo đơn mới với đúng mã này |
Không gửi orderCode | Luôn tạo mới, mã tự sinh ORD-YYYYMMDD-XXXXXX |
Mỗi dòng kết quả trả orderCode — lưu lại để lần sau cập nhật đúng đơn.
Không gửi orderCode thì gửi lại (retry) sẽ tạo đơn trùng. Đặt orderCode = mã đơn bên hệ thống của bạn để mọi lần gửi đều idempotent. Mã đã dùng cho một đơn — kể cả đơn đã xoá — không dùng lại được (ORDER_CODE_DUPLICATE).
Trường của một đơn
| Trường | Kiểu | Ghi chú |
|---|---|---|
orderCode | chuỗi ≤ 50 | Mã đơn, duy nhất trong cửa hàng |
customer | object | Bắt buộc khi tạo: { "id": "<id khách>" } hoặc { "matchField": "phone" | "email" | "customerCode" | "<mã trường duy nhất>", "value": "…" }. Bỏ qua khi cập nhật |
items | mảng ≤ 200 | Bắt buộc khi tạo. Khi cập nhật: gửi ⇒ thay toàn bộ dòng hàng; không gửi ⇒ giữ nguyên |
statusId | uuid | Chuyển sang trạng thái này (đã đúng trạng thái thì bỏ qua) |
shippingFee | số ≥ 0 | |
shippingName, shippingPhone, shippingAddress | chuỗi | shippingAddress là số nhà, tên đường |
shippingProvinceCode | chuỗi | Mã tỉnh / thành — xem danh mục |
shippingWardCode | chuỗi | Mã phường / xã, ph ải thuộc tỉnh ở trên. Khi cập nhật: gửi một trong hai mã là thay cả cặp (mã vắng ⇒ xoá) |
Một dòng hàng:
| Trường | Kiểu | Ghi chú |
|---|---|---|
variantId hoặc sku | uuid / chuỗi | Phải có trong danh mục sản phẩm của cửa hàng, nếu không ⇒ OPEN_API_ITEM_NOT_FOUND |
quantity | số nguyên ≥ 1 | |
unitPrice | số ≥ 0 | |
discountAmount | số ≥ 0 | Giảm giá trên dòng |
vatRate | số ≥ 0 | % VAT |
productName | chuỗi | Mặc định lấy tên sản phẩm trong danh mục |
notes | chuỗi ≤ 1000 | Ghi chú của dòng hàng |
Tổng tiền, thuế và công nợ do hệ thống tính lại từ dòng hàng và phí giao hàng.
Đơn ở trạng thái hoàn thành hoặc đã huỷ không cập nhật được nữa — dòng đó trả status: "error". Chuyển trạng thái cũng tuân theo quy tắc của cửa hàng (trạng thái kết thúc bị khoá, bước cần duyệt).
Thanh toán
Mỗi đơn có lịch sử thanh toán (từng khoản khách đã trả) và có thể có kỳ thanh toán (trả góp theo đợt). Khi một khoản được ghi hoặc xoá, hệ thống tự tính lại paidAmount, debtAmount, paymentStatus của đơn và số đã trả của kỳ, rồi gửi webhook payment.created / payment.deleted kèm order.updated.
Đối tượng thanh toán
{
"id": "2a4c6e8b-0d1f-4b3a-9c5e-7f9b1d3a5c7e",
"externalId": "PT-2026-0001",
"orderId": "b7c2…",
"orderCode": "DH-1001",
"paymentTermId": null,
"amount": 300000,
"paymentMethod": "bank_transfer",
"reference": "FT26277123456",
"note": "Khách chuyển khoản đợt 1",
"paidAt": "2026-10-20T02:30:00.000Z",
"documents": [],
"createdById": "9e90…",
"source": "api:ERP Kho",
"createdAt": "2026-10-20T02:31:00.000Z",
"updatedAt": "2026-10-20T02:31:00.000Z",
"deletedAt": null
}
| Trường | Ghi chú |
|---|---|
externalId | Mã bên hệ thống của bạn (nếu bạn gửi khi tạo) |
paymentMethod | Mã trong danh mục phương thức |
reference | Mã giao dịch ngân hàng / ví |
documents | Chứng từ đính kèm trên giao diện (name, url) |
source | Nguồn lần ghi cuối (tạo hoặc xoá): ui hoặc api:<tên key> |
deletedAt | Có giá trị ⇒ khoản đã bị xoá |
Đọc thanh toán của một đơn
GET /api/open/v1/orders/b7c2…/payments
{
"data": {
"terms": [
{ "id": "4d1e…", "termNumber": 1, "name": "Đợt 1", "dueAmount": 300000, "dueDate": "2026-10-20", "paidAmount": 300000, "status": "paid", "notes": null },
{ "id": "8f2a…", "termNumber": 2, "name": "Đợt 2", "dueAmount": 116400, "dueDate": "2026-11-20", "paidAmount": 0, "status": "pending", "notes": null }
],
"payments": [ { "id": "2a4c…", "amount": 300000, "paymentTermId": "4d1e…", "…": "…" } ]
}
}
terms[].status: pending · partial · paid · overdue. Khoản đã xoá không có trong payments ở đây — dùng GET /payments để thấy cả khoản đã xoá.
Đồng bộ thanh toán của cả cửa hàng
GET /api/open/v1/payments?updatedSince=2026-10-01T00:00:00Z&limit=50&excludeSource=api:ERP%20Kho
Theo keyset như GET /orders: tối đa 50 khoản / lần, cửa sổ tối đa 30 ngày. Trả cả khoản đã xoá (deletedAt có giá trị) để bạn đối soát — xoá bên bạn khi gặp. excludeSource bỏ các khoản mà lần ghi cuối đến từ nguồn đó. Chỉ gồm khoản của các đơn người tạo key được thấy.
Ghi một khoản thanh toán
POST /api/open/v1/orders/b7c2…/payments
- Request Body
- Response
{
"externalId": "PT-2026-0001",
"amount": 300000,
"paymentMethod": "bank_transfer",
"paymentTermId": "4d1e…",
"reference": "FT26277123456",
"note": "Khách chuyển khoản đợt 1",
"paidAt": "2026-10-20T02:30:00Z"
}
{ "data": { "id": "2a4c…", "externalId": "PT-2026-0001", "amount": 300000, "…": "…" }, "created": true }
| Trường | Kiểu | Ghi chú |
|---|---|---|
amount | số > 0 | Bắt buộc, tối đa 2 chữ số thập phân |
paymentMethod | chuỗi | Bắt buộc, mã trong GET /payment-methods |
paidAt | ISO 8601 | Bắt buộc, thời điểm khách trả |
externalId | chuỗi ≤ 100 | Nên gửi — xem bên dưới |
paymentTermId | uuid | Kỳ thanh toán của chính đơn này (lấy ở terms), sai ⇒ ORDER_PAYMENT_TERM_NOT_FOUND |
reference | chuỗi ≤ 255 | Mã giao dịch |
note | chuỗi ≤ 2000 |
Tạo mới trả 201. Gửi lại cùng externalId cho cùng đơn ⇒ trả khoản đã có (200, "created": false), không ghi trùng.
Không có externalId thì gửi lại (retry khi mất mạng) sẽ ghi trùng khoản tiền và làm sai công nợ của đơn. Đặt externalId = mã phiếu thu bên hệ thống của bạn.
Khoản ghi qua API vẫn gửi thông báo nội bộ "đơn đã thanh toán" / "thanh toán một phần" cho nhân viên như khi ghi trên giao diện.
Xoá một khoản
DELETE /api/open/v1/orders/b7c2…/payments/2a4c…
Trả { "data": <khoản đã xoá, có deletedAt> }. Khoản không thuộc đơn ⇒ 404 OPEN_API_PAYMENT_NOT_FOUND. Không có API sửa một khoản: ghi sai thì xoá rồi ghi lại.
Phương thức thanh toán
GET /api/open/v1/payment-methods
{
"data": [
{ "code": "cash", "label": "Tiền mặt" },
{ "code": "bank_transfer", "label": "Chuyển khoản" },
{ "code": "momo", "label": "MoMo" },
{ "code": "zalopay", "label": "ZaloPay" },
{ "code": "other", "label": "Khác" }
]
}