預計送達日計算:在結帳時承諾跨兩國的送達日期

顯示送達日期的結帳頁面,必須依倉庫所在國計算出貨日,依目的國計算承運天數。Narwhal 提供兩國的國定假日,截止時間與承運天數仍是商店自己的設定。

購物車裡的日期是依兩個日曆承諾的

客戶只看到一個日期,但由兩個國家共同決定。倉庫只在本國工作日出貨,承運商只在目的國工作日投遞。對德國倉庫來說是平常日子的平安夜,對波蘭買家卻是國定假日。這個請求從範例訂單當天起,查詢波蘭的下一個休息日。

curl,目的國的下一個休息日
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
  "https://api.narwhalapi.com/v1/calendars/holidays/POL/next?from_date=2026-12-21"
回應
{
  "country": "POL",
  "coverage": [
    {
      "scope": "national_public",
      "subdivision_codes": []
    }
  ],
  "holiday": {
    "date": "2026-12-24",
    "name": "Christmas Eve",
    "holiday_key": "christmas_eve",
    "local_name": "Wigilia Bożego Narodzenia",
    "scope": "national_public",
    "subdivision_codes": []
  }
}

holiday 物件給出 date、英文 name、local_name 和 scope。對德國發出同樣的請求會回傳 12 月 25 日的聖誕節,所以波蘭的第一個休息日比德國早一天。coverage 顯示兩個日曆都只包含全國性假日。

依截止時間、出貨日和承運天數得出承諾日期

各項輸入的來源不同。下表顯示哪些來自商店,哪些來自日曆。

輸入擁有者範例
下單截止時間商店設定倉庫當地時間 16:00
出貨日倉庫日曆依截止規則之後的第一個營業工作日
承運天數承運合約3 天
送達日目的國日曆只計算營業的工作日
國定假日Narwhal 年度清單每個國家每年一次請求

下面的函式在訂單於截止時間之後或休息日送達時,把訂單順延到倉庫的下一個工作日。然後依目的國的營業工作日計算承運天數。它對每個國家和年份只載入一次,當計數跨過 1 月 1 日時再載入下一年。

python,出貨日與送達日
import json, os, urllib.request
from datetime import datetime, time, timedelta
from functools import cache

def get(path):
    req = urllib.request.Request("https://api.narwhalapi.com" + path,
                                 headers={"Authorization": "Bearer " + os.environ["NARWHAL_API_KEY"]})
    with urllib.request.urlopen(req, timeout=10) as r:
        return json.load(r)

@cache
def closed_days(country, year):
    return {h["date"] for h in get(f"/v1/calendars/holidays/{country}/{year}")["holidays"]}

def is_open(day, country):
    return day.weekday() < 5 and day.isoformat() not in closed_days(country, day.year)

def promise(placed, cutoff, origin, dest, transit_days):
    dispatch = placed.date()
    if placed.time() >= cutoff or not is_open(dispatch, origin):
        dispatch += timedelta(1)
        while not is_open(dispatch, origin):
            dispatch += timedelta(1)
    day, left = dispatch, transit_days
    while left:
        day += timedelta(1)
        left -= is_open(day, dest)
    return dispatch, day

CUTOFF = time(16, 0)
for placed, dest in [
    (datetime(2026, 12, 21, 14, 0), "POL"),
    (datetime(2026, 12, 21, 17, 30), "POL"),
    (datetime(2026, 12, 21, 17, 30), "DEU"),
    (datetime(2026, 12, 31, 10, 0), "POL"),
]:
    dispatch, arrive = promise(placed, CUTOFF, "DEU", dest, 3)
    print(f"{placed:%a %d %b %H:%M} to {dest}: dispatch {dispatch:%a %d %b}, delivery {arrive:%a %d %b %Y}")
產出
Mon 21 Dec 14:00 to POL: dispatch Mon 21 Dec, delivery Mon 28 Dec 2026
Mon 21 Dec 17:30 to POL: dispatch Tue 22 Dec, delivery Tue 29 Dec 2026
Mon 21 Dec 17:30 to DEU: dispatch Tue 22 Dec, delivery Mon 28 Dec 2026
Thu 31 Dec 10:00 to POL: dispatch Thu 31 Dec, delivery Thu 07 Jan 2027

12 月 21 日星期一 14:00 的訂單當天出貨,12 月 28 日星期一送達。同一訂單如果在 17:30 下單,則於 12 月 22 日星期二出貨,12 月 29 日星期二送達,比第一種晚一天,因為平安夜在波蘭是休息日。如果改寄德國,17:30 的訂單會在 12 月 28 日星期一送達。12 月 31 日星期四的訂單會跨年,跳過 1 月 1 日和波蘭的 1 月 6 日主顯節,在 2027 年 1 月 7 日星期四送達。

顯示一個範圍,並把規則放在同一處

承運商通常報一個範圍,例如 2 到 3 天。對兩個端點各執行一次函式。對上面 17:30 的訂單,承運 2 天是 12 月 28 日星期一,3 天是 12 月 29 日星期二,所以購物車可以顯示在這兩個日期之間送達。截止時間、承運天數和週末規則應放在同一個函式裡,讓購物車、確認信和訂單頁面都呼叫它,這樣三處不會顯示不同的日期。請先把下單時間轉換為倉庫的時鐘,因為買家的晚上在倉庫可能已經是隔天早上,而截止判斷讀取的是倉庫時間。

日曆列出的是國定假日。它不知道倉庫停工、承運商的旺季附加費日期或地區性休息。德國的回應只包含全國範圍,所以邦假日不在其中。請把這些日期加入函式所讀取的集合。

每一步該用哪個國家的日曆?

出貨國決定出貨日。目的國決定承運天數,因為最後一段配送發生在那裡。如果包裹經過第三國,只有在承運商計算該國天數時,才把它的天數加入計數。

在休息日下的訂單會怎樣?

它從倉庫的下一個工作日開始處理。函式在套用截止時間之前,先用 is_open 檢查訂單日期,所以週六或假日的訂單會在下一個營業日出貨。截止時間是商店的規則,依倉庫當地時間比較。

結帳頁面每次檢視購物車都要呼叫 API 嗎?

不需要。同一國家同一年的年度清單對所有訂單都一樣,所以腳本把它存在快取中,一次請求就能服務所有購物車。請依適合你的頻率更新快取。加上工作日指南展示了在單一國家內同樣的計算,國定假日檢查指南展示了用於確認步驟的單一日期端點。

週六可以算作送達日嗎?

可以,前提是承運商週六投遞。把 is_open 中的星期判斷從 day.weekday() < 5 改為 day.weekday() < 6。假日檢查仍然保留,所以在目的國是國定假日的週六依然會被跳過。如果倉庫週六休息而承運商不休息,請為出貨和送達分別保留一個判斷。

發布於 。

後續步驟