跳轉到

Callback 與事件

分流規則

Handler 先判斷 payload 是否有 event

event 類型
Task terminal callback
device.bound / bank.bound / balance.changed / device.locked / device.unlocked

既有 task handler 不必改;用有無 event 即可安全分流。

flowchart TD
  R[收到 POST callback] --> E{payload 有 event?}
  E -->|否| T[task terminal:task_id / status / result]
  E -->|是| V{event 值}
  V -->|device.bound / bank.bound| B[enrollment 綁定]
  V -->|balance.changed| C[餘額異動]
  V -->|device.locked / unlocked| L[鎖狀態]

Delivery matrix

Payload 接收端去重鍵 現行投遞保證 PAYMENT fallback
Task terminal task_id(callback 的 idempotency_key 同值) best-effort,無持久化重送 GET /v1/tasks/{task_id}?idempotency_key=
device.bound enrollment_id(= event idempotency_key best-effort GET /v1/enrollments/{id}
bank.bound enrollment_id best-effort GET enrollment + GET device
balance.changed 由事件內容/上游規則 best-effort 後續 auto transaction_pull callback 或主動查 task
device.locked event idempotency_key(如 lock-42 DB outbox,重試至 2xx GET device
device.unlocked event idempotency_key DB outbox,重試至 2xx GET device

不要假設 exactly-once

best-effort callback 具備 durable delivery。 locked/unlocked 雖 outbox 重試至 2xx,仍可能重複投遞 → 必須以 idempotency_key 冪等落地。 Worker 不跟隨 3xx;3xx 視同失敗。

Handler 實作要求

  1. 快速回 2xx
  2. 先以去重鍵冪等落地
  3. 再異步做 ingest/matching
  4. 權威狀態有疑慮時一律 GET,不依賴「沒收到 callback = 沒發生」

Task terminal callback

{
  "task_id": "01J…",
  "idempotency_key": "01J…",
  "device_id": "TH-XXXX",
  "type": "transfer",
  "status": "succeeded",
  "result": {},
  "error": null
}
  • status 為終態:succeededfailed(對應 store state)。
  • idempotency_key = task_id(server 任務 id,不是 client 的 ORD key)。
  • URL 為 task 登記的 callback_url(或 tenant 預設)。

Bound events

enrollmentidempotency_key = enrollment_id。永不帶 PIN。

balance.changed

{
  "event": "balance.changed",
  "device_id": "TH-XXXX",
  "tenant_id": "…",
  "old": {"accounts": []},
  "new": {"accounts": []},
  "ts": 0
}

best-effort。Farm 可能接著自動入列 transaction_pulltrigger=balance_changed), PAYMENT 會再收一筆event的 task callback。

Lock events

{
  "event": "device.locked",
  "idempotency_key": "lock-42",
  "device_id": "TH-XXXX",
  "device_code": "TH-XXXX",
  "bank_code": "SCB",
  "reason": "pin_locked",
  "source": "task",
  "task_id": "01J…",
  "locked_at": 1754712345.6
}

device.unlocked 另含 unlocked_atunlocked_by"tenant" / "admin")、notereasonsourcetask_id解鎖前快照。

  • 送到 tenantcallback_url(不是單一 task callback)。
  • Outbox 退避 min(30 × 2^attempts, 3600) 秒,不放棄
  • 未設 callback_url 時事件仍留存,補上後可送出。

非 PAYMENT 下單的 task callback

balance.changed 觸發的 auto transaction_pull 會以 task terminal 形狀回呼。 以 resultpayload 脈絡(或業務側關聯)辨識,勿假設每筆 task 皆由己方 POST