Tất cả bài viết

Tính phí ship theo khoảng cách bằng Directions API

Hướng dẫn tính phí ship/cước vận chuyển theo khoảng cách và thời gian thực tế bằng /v1/directions của GoGoDuk — tối ưu cho xe máy, miễn phí.

Phí ship đồng giá nghe thì gọn, nhưng càng chạy càng lệch: đơn ở xa thì lỗ, đơn ở gần thì khách thấy đắt và bỏ giỏ hàng. Cách công bằng hơn là tính phí theo khoảng cách và thời gian thực tế của tuyến đường. Bài này hướng dẫn lấy hai con số đó từ Directions API của GoGoDuk rồi biến thành công thức tính cước ship — với routing tối ưu cho xe máy, đúng cách phần lớn shipper ở Việt Nam di chuyển.

Vì sao tính theo khoảng cách thực, không phải chim bay

Nhiều người tính phí bằng khoảng cách đường chim bay (Haversine) giữa hai toạ độ. Vấn đề là khoảng cách chim bay gần như luôn ngắn hơn quãng đường thật. Ví dụ một shop ở Quận 1 giao cho khách ở Thủ Đức (TP.HCM): đường chim bay có thể chỉ ~9 km, nhưng quãng đường xe máy thật — qua cầu, đường một chiều, vòng xoay — thường dài hơn đáng kể. Tính phí theo chim bay nghĩa là bạn đang trợ giá cho mọi đơn đi xa.

Mô hình này khác với tính phí theo vùng (zone-based): chia thành phố thành các quận/phường rồi gán phí cố định cho mỗi vùng. Nếu bạn cần mô hình đó, xem bài Xây dựng phân vùng giao hàng (Delivery Zones). Quy tắc chọn nhanh:

  • Theo vùng — đơn giản, dễ đoán, hợp khi bạn chỉ phục vụ vài quận nội thành.
  • Theo khoảng cách — chính xác và công bằng, hợp khi bán kính giao rộng hoặc muốn phí phản ánh đúng chi phí từng đơn.

Lấy khoảng cách & thời gian từ /v1/directions

Endpoint trả về định dạng tương thích Google Directions, nên hai trường ta cần nằm ngay trong routes[0].legs[0].

Request

Gọi trực tiếp bằng curl hoặc fetch — SDK TypeScript hiện chưa bọc directions, nên ở đây dùng request thô. Ví dụ shop ở Quận 1 giao cho khách ở Thủ Đức:

curl -G "https://api.gogoduk.com/v1/directions" \
  -H "X-API-Key: $GOGODUK_API_KEY" \
  --data-urlencode "origin=10.7769,106.7009" \
  --data-urlencode "destination=10.8505,106.7717" \
  --data-urlencode "vehicle=motobike"
const res = await fetch(
  "https://api.gogoduk.com/v1/directions?" +
    new URLSearchParams({
      origin: "10.7769,106.7009",       // shop ở Quận 1
      destination: "10.8505,106.7717",  // khách ở Thủ Đức
      vehicle: "motobike",
    }),
  { headers: { "X-API-Key": process.env.GOGODUK_API_KEY! } },
);
const { status, routes } = await res.json();
  • origin / destination — dạng lat,lng.
  • vehicle — hiện hỗ trợ motobike (mặc định); car sẽ có sau.
  • Scope key cần có: routing:read.

Response

JSON rút gọn — ta chỉ quan tâm distance.valueduration.value của leg đầu:

{
  "status": "OK",
  "routes": [
    {
      "legs": [
        {
          "distance": { "text": "12.4 km", "value": 12380.5 },
          "duration": { "text": "28 phút", "value": 1683.0 }
        }
      ]
    }
  ]
}

Lưu ý đơn vị, dễ sai nhất ở đây:

  • distance.value tính bằng mét (chia 1000 ra km).
  • duration.value tính bằng giây (chia 60 ra phút).

Công thức tính phí ship

Hãy tách phần tính phí thành một hàm thuần, dễ test và tái sử dụng. Các con số VND dưới đây chỉ là minh hoạ — không phải giá của GoGoDuk, cũng không phải biểu cước của hãng giao hàng nào; bạn thay bằng chính sách giá của mình.

type FeeConfig = {
  baseFee: number;      // phí cho quãng đầu (vd 2 km đầu)
  baseKm: number;       // số km đã gồm trong baseFee
  perKm: number;        // phí mỗi km vượt baseKm
  roundTo: number;      // làm tròn phí lên bội số này
};
 
// CHỈ MINH HOẠ — thay bằng chính sách giá của bạn.
const config: FeeConfig = {
  baseFee: 15000,   // 15.000đ cho 2 km đầu
  baseKm: 2,
  perKm: 5000,      // 5.000đ mỗi km tiếp theo
  roundTo: 1000,    // làm tròn lên 1.000đ
};
 
function calcShippingFee(distanceMeters: number, cfg: FeeConfig): number {
  const km = distanceMeters / 1000;
  const extraKm = Math.max(0, Math.ceil(km - cfg.baseKm)); // tính tròn km vượt
  const raw = cfg.baseFee + extraKm * cfg.perKm;
  return Math.ceil(raw / cfg.roundTo) * cfg.roundTo;
}
 
// Ví dụ: tuyến 12.380,5 m -> 12.380,5 / 1000 = 12,38 km
// extraKm = ceil(12,38 - 2) = 11  ->  15.000 + 11 * 5.000 = 70.000đ
calcShippingFee(12380.5, config); // 70000

Phụ phí theo thời gian

duration.value cho phép cộng thêm phụ phí cho tuyến mất nhiều thời gian (giờ cao điểm, đường kẹt) — distance và duration bổ sung cho nhau:

function withTimeSurcharge(
  baseFee: number,
  durationSeconds: number,
): number {
  const minutes = durationSeconds / 60;
  // CHỈ MINH HOẠ: vượt 30 phút thì + 1.000đ mỗi 10 phút tiếp theo.
  if (minutes <= 30) return baseFee;
  const extraBlocks = Math.ceil((minutes - 30) / 10);
  return baseFee + extraBlocks * 1000;
}

Xử lý tuyến ESTIMATED

Khi engine không nối được hai điểm (đảo, mạng đường đứt đoạn, input lệch), GoGoDuk không trả lỗi mà trả tuyến ước lượng với status: "ESTIMATED": lúc này distance là khoảng cách chim bay nhân hệ số đường bộ, duration ước theo vận tốc trung bình. Bạn vẫn luôn có một con số để tính phí, nhưng nên xử lý minh bạch:

if (status === "ESTIMATED") {
  // hoặc đánh dấu "phí tạm tính" cho khách,
  // hoặc yêu cầu xác nhận tay trước khi chốt đơn.
}

Hai cách thường dùng: (1) vẫn báo phí nhưng gắn cờ "phí ước lượng", hoặc (2) với đơn giá trị cao thì bắt nhân viên xác nhận trước khi chốt.

Lưu ý khi đưa lên production

  • Tính phí ở backend, không ở client. Nếu để JavaScript trình duyệt tính, khách có thể chỉnh giá trước khi gửi đơn.
  • Cache theo cặp origin→destination. Cùng một shop giao tới cùng một địa chỉ thì khoảng cách không đổi — cache lại để khỏi gọi lại mỗi lần.
  • Hiện chỉ có point-to-point, chưa có distance-matrix. Nếu một đơn có nhiều điểm giao, gọi /v1/directions nhiều lần cho từng chặng (batch matrix nằm trong roadmap, chưa khả dụng).
  • Khi khách thả ghim trên bản đồ thay vì gõ địa chỉ, dùng reverse geocoding để ra tỉnh/huyện và toạ độ trước, rồi mới đưa toạ độ vào directions.

Khi nào không nên tự build

Thẳng thắn: Google Directions và Goong Directions đều đã trưởng thành và rất ổn định — nếu bạn đã trả tiền và hài lòng, không nhất thiết phải đổi. Điểm mạnh của GoGoDuk ở bài toán này là miễn phí vĩnh viễnhồ sơ định tuyến xe máy tinh chỉnh cho đường Việt Nam, nên quãng đường (và do đó phí ship) khớp với cách shipper thật chạy. Lưu ý hồ sơ car chưa có; nếu đội xe của bạn là ô tô, hãy chờ bản cập nhật đó.

Kết luận

Công thức gọn lại chỉ là: gọi /v1/directions → lấy distance.value (mét) và duration.value (giây) → đưa vào hàm tính phí theo bậc, cộng phụ phí thời gian nếu cần, và xử lý riêng tuyến ESTIMATED.

Có câu hỏi hay muốn khoe sản phẩm? Tham gia nhóm hỗ trợ Telegram của chúng tôi.

Muốn dùng GoGoDuk?

Miễn phí trọn đời — 100 request/ngày mỗi tài khoản, không cần thẻ tín dụng. Giới hạn cao hơn theo yêu cầu.

Đăng ký →