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 實作要求¶
- 快速回 2xx
- 先以去重鍵冪等落地
- 再異步做 ingest/matching
- 權威狀態有疑慮時一律 GET,不依賴「沒收到 callback = 沒發生」
Task terminal callback¶
{
"task_id": "01J…",
"idempotency_key": "01J…",
"device_id": "TH-XXXX",
"type": "transfer",
"status": "succeeded",
"result": {},
"error": null
}
status為終態:succeeded或failed(對應 storestate)。idempotency_key=task_id(server 任務 id,不是 client 的 ORD key)。- URL 為 task 登記的
callback_url(或 tenant 預設)。
Bound events¶
見 enrollment。idempotency_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_pull(trigger=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_at、unlocked_by("tenant" / "admin")、note;
reason/source/task_id 為解鎖前快照。
- 送到 tenant 層
callback_url(不是單一 task callback)。 - Outbox 退避
min(30 × 2^attempts, 3600)秒,不放棄。 - 未設
callback_url時事件仍留存,補上後可送出。
非 PAYMENT 下單的 task callback¶
balance.changed 觸發的 auto transaction_pull 會以 task terminal 形狀回呼。
以 result/payload 脈絡(或業務側關聯)辨識,勿假設每筆 task 皆由己方 POST。