送达日期估算:在结账时承诺跨两国的送达日期

显示送达日期的结账页面,必须按仓库所在国计算发货日,按目的国计算承运天数。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。假日检查仍然保留,所以在目的国是公共假日的周六依然会被跳过。如果仓库周六休息而承运商不休息,请为发货和送达分别保留一个判断。

发布于。

后续步骤