外觀
訂單 #284427 取消後取貨完成、發票未開立排查報告
日期:2026-05-05 站台:HARU (letsharu.com) 撰寫:Nel Tseng
總結
顧客 3/29 取消訂單,但藍新與超商物流系統未同步取消,貨已寄達門市,顧客 4/2 仍前往取貨並付款,訂單因此自動恢復為「已完成」。
CTK 自製的金財通發票外掛 ctkpro-invoice 規定「曾經取消過的訂單不開發票」,本訂單因此漏開。每日 4 次的補開排程查詢條件設計上無法涵蓋此情境,亦未補上。CTK 交接文件將此情境列為「非系統問題」。
建議處理方向
- 手動轉訂單狀態:將 #284427 從「完成」切換為「處理中」再切回「完成」
- 請 CTK 處理
- 評估替換:重做金財通發票串接外掛,或改用其他電子發票服務商與對應外掛
詳細排查、程式碼追蹤、影響範圍與技術細節見以下章節。
一、問題描述
訂單 #284427 顧客以超商取貨付款下單,3/29 自行取消訂單,4/2 仍至超商取貨並完成付款,藍新回傳付款資料後,訂單狀態由「已取消」自動轉為「完成」。然而完成後發票未自動開立,且每日 4 次的補開排程亦未補開,金財通後台無對應發票。
二、訂單事件鏈
顧客:蔡秉軒 / 會員 ID 34426 / iPhone Safari
| 時間 | 事件 | 訂單狀態 | 觸發來源 |
|---|---|---|---|
| 3/26 18:58:56 | 顧客下單 (1,379 元,超商取貨付款,門市:東航) | 等待付款中 | 顧客 |
| 3/26 19:00:08 | 藍新第 1 次回傳:店家、藍新交易序號 26032619000531322 | 等待付款中 → 處理中 | 藍新 callback |
| 3/26 19:00:11 | 子主題 newebpay_cvscom_fix_status 攔截:店家有了但未含付款時間 | 處理中 → 等待付款中 | 子主題客製 |
| 3/29 14:18:49 | WC my-account 自助取消 (備註內容「顧客已取消訂單」) | 等待付款中 → 已取消 | WooCommerce 系統 (顧客觸發) |
| 4/2 15:23:07 | 顧客至東航門市取貨並付款 (藍新交易序號 26032619000531322) | (WC 仍為已取消) | 顧客 |
| 4/2 15:25:42 | 藍新第 2 次回傳:含付款時間 2026-04-02 15:23:07 | (備註寫入) | 藍新 callback |
| 4/2 15:25:49 | 「超商取貨付款已完成」 | 已取消 → 完成 | 藍新外掛 |
備註:發票類型為個人發票 (_billing_invoiceFlag、_billing_needUBN、_billing_UBN 皆為空,無載具 meta),金額 1,379 元有開立必要。
三、CTK 交接文件對此情境的說明
CTK 在 2026-03 交接文件 0326 會議記錄段「已知問題」中將此情境列為:
CTK 交接文件原文 (PDF 第 19 頁)
客人取消訂單但已取貨的發票未自動開立(非系統問題)
實際排查結果:此情境是 CTK 自製外掛 ctkpro-invoice 的程式邏輯設計問題,並非單純的人工流程或第三方因素。詳細分析見第四節。
四、排查過程
4-1 訂單 postmeta 比對
訂單 postmeta 中無任何發票相關欄位 (invoice_number、invoice_date 皆缺),確認發票從未開立。
關鍵 meta:
| 欄位 | 值 |
|---|---|
_payment_method | newebpay |
_nwpSelectedPayment | CVSCOMPayed |
_CVSCOMNotPayed | 0 |
_newebpayStoreName | 東航 |
_date_paid | 1774522808 (2026-03-26 19:00:08) |
_date_completed | 1775114742 (2026-04-02 15:25:42) |
second_time_gateway | 1 (藍新第 2 次 callback 已記錄完成) |
invoice_number | (不存在) |
_date_paid 為藍新第 1 次 callback 寫入的時間,並非顧客實際付款時間。實際付款時間在訂單備註 (4/2 15:23:07),未寫入 postmeta。藍新交易序號 (26032619000531322) 也只在備註出現,未保存於 postmeta。
雙重確認 invoice meta:分別比對 invoice_number、_invoice_number、einvoice_number、bp_invoice_no、carrier、bankpro_*,全部不存在。發票確實從未開立 (非寫入失敗或欄位名差異)。
4-2 ctkpro-invoice 開立邏輯追蹤
外掛主邏輯位於 app/Controllers/InvoiceController.php 的 trigger_invoice_status 方法,hook 點為 woocommerce_order_status_changed:
php
public function trigger_invoice_status($order_id, $old_status, $new_status)
{
$do_not_pass = ['cancelled', 'failed', 'refund-requested',
'refund-approved', 'refund-cancelled', 'refunded'];
$pass = ['completed'];
// 原始狀態已經為關閉等特殊狀態: 不觸發
if (in_array($old_status, $do_not_pass)) {
return;
}
// 新狀態為完成時觸發開發票
if (in_array($new_status, $pass)) {
$this->invoiceService->create_invoice($order_id);
}
}判斷規則:
old_status為cancelled/failed/refund-*任一者 → 直接 return,不開發票new_status為completed才觸發開立
訂單 #284427 的最後一次狀態變化為 cancelled → completed,old_status = 'cancelled' 命中第一道條件,直接 return 不進入開立流程。
4-3 補開排程邏輯追蹤
排程主邏輯位於 app/Controllers/ScheduleController.php 的 ctk_check_invoice 方法,每日 08:20 / 14:20 / 20:20 / 02:20 各執行一次:
php
$stamp_now = $time - (8 * 60 * 60);
// 至一小時內的訂單先無視(可能正在跑流程或發票開一半)
$stamp_end = $stamp_now - (1 * 60 * 60);
$stamp_begin = $stamp_end - (8 * 60 * 60);
$args = [
'date_paid' => "$stamp_begin...$stamp_end",
'return' => 'ids',
];
$orders = wc_get_orders($args);
foreach ($orders as $order_id) {
$invoice_number = get_post_meta($order_id, 'invoice_number', true) ?? '';
$status = get_post_status($order_id);
if (!$invoice_number && 'wc-completed' === $status) {
$this->invoiceService->create_invoice($order_id);
}
}判斷規則:
- 查詢條件:
date_paid落在「執行時間 -9 小時 ~ 執行時間 -1 小時」這 8 小時窗口 - 命中後:訂單狀態為
wc-completed且invoice_number為空 → 補開 - 排程時間 02:20 / 08:20 / 14:20 / 20:20 為台北時區 (cron timestamp 以 UTC 儲存)
對訂單 #284427 而言:
_date_paid = 2026-03-26 19:00:08(藍新第 1 次 callback 寫入)- 真正完成時間為 4/2 15:25:42,相隔 7 天
- 排程窗口最多回溯 9 小時,#284427 的
_date_paid(3/26) 早於任何 4 月排程的查詢範圍 - 實測 6 個時段 (4/2 02:20 / 08:20 / 14:20 / 20:20、4/3 02:20 / 08:20) 模擬排程查詢,#284427 全部
has_284427=NO
排程設計上僅能補開「付款日當天 9 小時內因故未開立」的訂單,付款日與完成日跨日的情境不在排程涵蓋範圍。
4-4 InvoiceService::create_invoice 額外風險
讀完 app/Services/InvoiceService.php 後另外發現兩項與本案相關的設計弱點:
- API 失敗無重試機制:金財通 API 回錯誤時,
create_invoice僅寫一條 order note,不更新任何 meta。代表下一次排程窗口若再命中該訂單會重試;但若窗口已過 (如 #284427),永遠不會再重試。 get_date_completed()->format()未檢查 null:若訂單尚未真正落定 date_completed 即觸發,會 fatal error。本案訂單 date_completed 完整,未發生此問題,但屬潛在風險。
外掛無 logger 機制 (debug.log 不存在、wc-logs 無 ctkpro-invoice 條目),無法從 log 量化過去排程實際補開了多少筆,亦無法回頭辨識「漏網」與「API 失敗」的比例。
五、判定結果
直接原因
訂單 #284427 的最後一次狀態變化為 cancelled → completed,ctkpro-invoice 即時開立邏輯規則「old_status 為 cancelled 時直接 return」攔下,發票未自動開立。事後亦無人工或批次再次切換訂單狀態,未觸發任何後續開立。
根本原因
CTK 自製外掛 ctkpro-invoice 的兩層發票機制設計覆蓋不全:
- 即時開立機制 (InvoiceController):
cancelled列入old_status黑名單,「先取消後完成」的訂單第一次 trigger 時必定被擋。 - 補開排程機制 (ScheduleController):以
_date_paid為基準、8 小時窗口。_date_paid為藍新第 1 次 callback (選店時) 寫入,跨日完成的訂單永遠落不進排程窗口。
兩層機制都擋掉後,唯一補救路徑為人工介入:客服或批次任務後續再切一次訂單狀態 (例如 完成 → 保留 → 完成、完成 → 處理中 → 完成),新的 old_status 不在黑名單,即觸發開立。
與 CTK 交接文件說明對照
CTK 交接文件將此情境標註為「非系統問題」,從上述設計推斷,這句話的實際意義可能是「需靠人工再切一次狀態才會補開,自動流程不處理」。
影響範圍
過去 6 個月發生「已取消 → 完成」轉換的訂單共 5 筆 (以 order note 含「已取消 變更為 完成」字串為盤點條件):
| 訂單 | 下單日 | 取貨完成日 | 後續狀態變更 | 發票 | 補開觸發 |
|---|---|---|---|---|---|
| #284427 | 03-26 | 04-02 | 無 | NULL | 漏開 |
| #284198 | 03-19 | 03-21 | 03-25 大量編輯 完成→保留→完成 | ZG84814128 | 保留→完成 觸發 |
| #283229 | 02-20 | 02-22 | 04-01 完成→處理中→完成 | ZG84814245 | 處理中→完成 觸發 |
| #282394 | 01-23 | 01-30 | 01-30 等待付款中→完成 | XE23797277 | 等待付款中→完成 觸發 |
| #281694 | 01-07 | 01-12 | 無 | XE23796885 | 不明 (與 #284427 流程相同卻有發票) |
5 筆中:
- 3 筆(#284198 / #283229 / #282394):事後有人工或批次切換狀態,重新觸發開立而成功補開
- 1 筆(#281694):流程與 #284427 幾乎相同 (取消 → 取貨完成 → 無後續變更),但仍有發票。原因不明,可能涉及外掛歷史版本差異或其他補救路徑,需請 CTK 確認
- 1 筆(#284427):漏開
無法 100% 還原 #281694 的補開機制,代表此漏洞的觸發條件不可預測,未來仍有再次漏開的可能性。
HPOS 相容性確認
ctkpro-invoice 依賴 get_post_meta() / get_post_status() 等舊 API。已驗證 HARU 站未啟用 HPOS (無 wpk6_wc_orders 表),訂單存於 wpk6_posts + wpk6_postmeta。若日後啟用 HPOS,此外掛的即時開立與補開排程整套會失效,需同步重構。
六、建議處理方向
問題源自 CTK 自製外掛 ctkpro-invoice 的兩處邏輯設計。我方不直接修改該外掛,避免動到後責任歸屬轉移。
1. 請 CTK 處理
外掛由 CTK 開發與維護,邏輯細節、原始設計意圖、後續修補責任皆在原廠。
2. 評估替換方案
若 CTK 無法處理或不再支援,可評估以下方向:
- 重做金財通發票串接外掛:保留現有金財通服務商,重新實作串接層
- 改用其他發票服務商:評估市場上其他電子發票服務 (如綠界、ezPay、SmilePay 等) 與對應的成熟外掛,整體替換金財通
3. 過渡期人工處理 #284427
在處理方向定案前,#284427 一筆可選擇下列任一方式補開:
- 方式 A:登入金財通後台手動開立
- 方式 B:由後台將該訂單狀態從「完成」切換為「處理中」再切回「完成」,會觸發
ctkpro-invoice自動開立 (此為過去 6 個月其他 4 筆同類訂單的實際補救方式)
過去 6 個月「先取消後完成」共 5 筆,僅 #284427 漏網,其餘 4 筆皆已透過事後狀態切換成功補開,可作為操作參考。
