網店與市集
退款回倉,只回一次
退款發出,那件貨應該重新變成可售。回倉本身只是一次呼叫,難處在於它必須剛好發生一次:重試的任務如果再加回一件,就會憑空製造出並不存在的庫存,而事後減一是修正,不是還原。Shopify 對自己的庫存變更做了同樣的判斷,把 idempotency key 改為必填。
- 觸發
- 兩個
- 執行頻率
- 每 10 分鐘
- 工具
- 5 件
- 需要 durable execution
- 兩者擇一
流程
上面那條負責回覆客戶。下面那條定時調整庫存,前面有一道檢查擋住:同一筆退款是否已經回過倉。
退款成立
Shopify Admin API
認領退款編號
Sheets · Airtable
通知客戶
Slack · WhatsApp
每 10 分鐘
n8n 排程
撈出未回倉
n8n
回倉是否尚未執行?
Temporal · Inngest
調整庫存,附 key
Shopify Admin API
不作處理
—
逐步拆解
- 01
退款成立
Shopify Admin API退款 webhook 觸發,內含退款編號,以及退款當時選定的回倉方式。兩樣都讀進來,但先不要寫回任何系統。
- 02
認領退款編號
Sheets · Airtable一個退款編號一行,並在該編號上設唯一約束。寫得入這一行就是認領成功;如果行已經存在,代表這筆退款處理過,沒有欠下任何動作。這張表就是 idempotency 紀錄,而它便宜,因為它屬於你自己。
- 03
通知客戶
Slack · WhatsApp向客戶確認退款已經發出,同時通知處理退貨的同事。這條訊息講的是錢,不是庫存——兩件事不同,也不必同時到達。
- 04
每 10 分鐘
n8n 排程回倉依排程執行,不依 webhook 執行,所以流程停機期間收到的退款,下一輪仍然會被撿起來。
- 05
撈出未回倉
n8n取出已認領但未確認回倉的紀錄。認領寫得入、確認沒有寫入,正正就是當機那一種情況——所以認領與確認要分開兩個欄位,不是一個開關。
- 06
回倉是否尚未執行?
Temporal · Inngest先確認這筆退款未回過倉,然後才執行,而且兩者放在同一次可以整段重試的執行裡。n8n 與 Make 都不會記住那次調整已經生效,所以回應遺失之後的重試會再加一件貨,而庫存數字沒有辦法指出哪一件是多出來的。要麼用 durable execution,要麼用平台強制的 idempotency key。
- 07
調整庫存,附 key
Shopify Admin API這一步只適用於記錄為不回倉的退款,或者經由退貨退款介面發出、本身不含回倉指令的退款。以取消或退貨方式建立的退款,Shopify 已經回過倉;在這裡再加一件,正正就是這個案例要防止的幽靈庫存。調整時用一條由退款編號推導出來的 key,重試就不會再加一件。
平台不容許的事
回倉只是一次呼叫,但這次呼叫代表什麼、可以安全地做多少次,由 Shopify 定。
refundCreate 的說明是「creates a refund for an order, allowing you to process returns and issue payments back to customers」,而它正是可以指定行項目回倉方式的那個 mutation;相對之下 returnRefund「focuses solely on handling the financial refund without any restocking input」。回倉是退款當下就要決定的事,不是事後補上去的一步。
Shopify Admin GraphQL API — refundCreate回倉方式是列舉值,不是是與否。CANCEL 的說明是「use this when restocking unfulfilled line items」;RETURN 是「use this when restocking line items that were fulfilled」;NO_RESTOCK 記錄的是該行「was not restocked」。舊有的 LEGACY_RESTOCK 已標為棄用,「is not accepted when creating new refunds」。選錯的話,那件貨會由錯誤的狀態回來,或者根本不回來。
Shopify Admin GraphQL API — RefundLineItemRestockTypeinventoryAdjustQuantities 調整的是差額,不是絕對數量。Shopify 文件寫明:「as of version 2026-01, this mutation supports an optional idempotency key using the @idempotent directive」,以及「as of version 2026-04, the idempotency key is required and must be provided using the @idempotent directive」。平台把這個 key 由選填改為必填,理由與這個流程要解的問題一樣:差額加了兩次,不能靠再加一次還原。
Shopify Admin GraphQL API — inventoryAdjustQuantitiesWhatsApp 的客服視窗,由用戶主動發訊息或致電開始計算,到期前再聯絡會重置;「when the window closes, you can only send pre-approved template messages」。所以客戶上次聯絡之後隔了幾日才發出的退款確認,必須用模板訊息,不能用自由文字。
WhatsApp Cloud API — Send messages
什麼情況不值得做
以下三種情況,維護成本會高於多出來的假庫存。
你根本不會把退款的貨回倉——退回來的貨會報廢、送檢,或者改由另一個渠道賣。這樣退款與庫存本來就是兩件事,硬要接成一條流程,等於製造一種你的營運上並不存在的關聯。
退款少到一個人逐筆處理,而且記得住自己處理過。整個流程的存在意義就是保證只做一次;一星期只碰五筆退款的人,本來就做得到,而且更便宜。
你沒有地方記錄回倉發生過。沒有那一行認領紀錄,這裡每一步都只是猜測:流程分不出一筆未處理的退款,與一筆處理過但忘記了的退款。