為服務等級協議(SLA)執行按工作日計時的違約計時器

按工作日計時的違約計時器,如果合約沒有寫明所用日曆、起算日和時區,就會向兩個方向出錯。Narwhal 提供的是某個國家某一年的假日,其餘規則由服務台自己的程式碼和合約決定。

用合約可以明確指定的日曆來啟動計時器

如果計時器在假日照常運行,就會顯示合約從未允許的違約;如果它在合約沒有排除的日子停下,又會掩蓋真實的違約。解決辦法是使用雙方都能指認的日曆。下面的請求讀取新加坡 2026 年的假日,只保留 Deepavali 前後的兩個項目。

curl,查詢一個國家一年的假日
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
  "https://api.narwhalapi.com/v1/calendars/holidays/SGP/2026"
回應,兩個項目
{
  "country": "SGP",
  "coverage": [
    {
      "scope": "national_public",
      "subdivision_codes": []
    }
  ],
  "holidays": [
    {
      "date": "2026-11-08",
      "name": "Deepavali",
      "holiday_key": "deepavali",
      "scope": "national_public",
      "subdivision_codes": []
    },
    {
      "date": "2026-11-09",
      "name": "Deepavali (Observed)",
      "holiday_key": "deepavali",
      "scope": "national_public",
      "subdivision_codes": [],
      "observed": true
    }
  ]
}

每個項目都有 date、name、scope 和 subdivision_codes。Deepavali 的日期是 11 月 8 日週日;標有 observed: true 的項目是 11 月 9 日週一的補假,落在平日上。coverage 清單給出日曆包含的 scope,新加坡為 national_public。API 只提供假日,計時器的其餘部分屬於合約約定的事項。

執行程式碼之前合約必須定義的內容

程式碼需要表中每一列都有明確答案。合約沒有規定的地方,服務台和客戶會在第一張有爭議的工單上才發現。

設定需要向合約提出的問題由誰提供
日曆哪個國家,用哪種假日 scope?先看合約,再用 API 的 scope 過濾
計入的日子週六和週日是否排除?合約
起算日受理當天算第 0 天還是第 1 天?合約
截止日期到期時間是當天結束時嗎?合約
時區以哪個地區的當地日期開始計時?合約
暫停時間客戶答覆期間計時是否暫停?合約和工單系統
每天的時數工作日是否指辦公時間?合約和支援排班

每一列都有書面答案,計時器就成了一個不留任何主觀判斷的函式。API 只解決日曆這一列,別的什麼都不管。

計算到期日並判斷是否違約

腳本把假日日期收集到一個集合裡,把不在集合中的平日視為工作日。受理當天為第 0 天,所以從隔天開始計數。第一個程式碼區塊把工單時間戳記轉換成合約所在時區的日期,第二個區塊檢驗已解決的工單。

python,按工作日計時的違約計時器
import json, os, urllib.request
from datetime import date, datetime, timedelta
from zoneinfo import ZoneInfo

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=30) as r:
        return json.load(r)

def holiday_dates(country, years):
    return {date.fromisoformat(h["date"]) for y in years for h in get(f"/v1/calendars/holidays/{country}/{y}")["holidays"]}

def is_working_day(d, off):
    return d.weekday() < 5 and d not in off

def due_date(opened, working_days, off):
    d, left = opened, working_days
    while left:
        d += timedelta(1)
        left -= is_working_day(d, off)
    return d

def elapsed_working_days(opened, resolved, off):
    return sum(is_working_day(opened + timedelta(n), off) for n in range(1, (resolved - opened).days + 1))

off = holiday_dates("SGP", (2026,))
sg = ZoneInfo("Asia/Singapore")

# Clock start: the ticket timestamp becomes a date in the contract's time zone.
created = datetime(2026, 11, 5, 20, 0, tzinfo=ZoneInfo("America/New_York"))
opened = created.astimezone(sg).date()
print("created", created.date(), "in New York is", opened, "in Singapore")
print("3 working days from the Singapore date:", due_date(opened, 3, off))
print("3 working days from the New York date: ", due_date(created.date(), 3, off))

# Breach test: compare the resolution date with the due date.
opened, resolved = date(2026, 2, 13), date(2026, 2, 27)
due = due_date(opened, 5, off)
late = elapsed_working_days(opened, resolved, off) - 5
print("5 working days from", opened, "is due", due, "; resolved", resolved, "so", late, "working days late")
產出
created 2026-11-05 in New York is 2026-11-06 in Singapore
3 working days from the Singapore date: 2026-11-12
3 working days from the New York date:  2026-11-11
5 working days from 2026-02-13 is due 2026-02-24 ; resolved 2026-02-27 so 3 working days late

紐約的工單建立於 11 月 5 日週四 20:00,此時新加坡已是 11 月 6 日週五。從新加坡日期起算 3 個工作日,到期日為 11 月 12 日週四,因為 11 月 9 日週一是 Deepavali 的補假。如果從紐約日期起算,同一張工單會在 11 月 11 日週三到期,提前一天,所以時區這一列決定了服務台是否違約。第二張工單在 2 月 13 日週五受理,因農曆新年占去 2 月 17 日週二和 18 日週三,到期日為 2 月 24 日週二。它在 2 月 27 日週五才解決,晚了 3 個工作日。

遇到落在週末的假日,計時器怎麼處理?

平日判斷會先把該日排除,所以不會被扣兩次。新加坡回應中,Hari Raya Puasa 是 2026 年 3 月 21 日週六,Deepavali 是 11 月 8 日週日,兩者都不會改變計數。補假是帶有 observed: true 的獨立項目,所以計時器無需額外程式碼就會把它排除。

SLA 應該使用哪種 scope?

請使用與客戶業務所遵循的日曆相符的 scope。新加坡只回傳 national_public;印尼回傳 national_public 和 collective_leave;澳洲回傳帶有 subdivision_codes 的地區項目,以及 bank scope。年度端點有一個選用的 scope 過濾參數。當 SLA 要排除某一類日子時傳入它,並在每次請求中使用同一個 scope,這樣計數結果才能重現。

API 能計算工作小時數嗎?

不能。日曆端點回傳的是假日日期。營業時間、午休,以及客戶答覆期間的暫停,都來自支援排班和工單系統。如果 SLA 是以小時為單位寫的,就用這段程式碼得到工作日,再把排班套用到第一天和最後一天。

未結工單很多時,如何減少請求次數?

每個國家和年份只取得一次,把日期集合放在記憶體或自己的資料表裡。該國家和年份的所有工單都重複使用這個集合。只有在工單跨入尚未載入的年份時,才再次呼叫年度端點。計算兩個日期之間的營業日這篇指南用試算表方式示範了同樣的運算。

發布於 。

後續步驟