「折扣碼已移除」通知
何時應使用此工作流程
當折扣代碼被移除,且您需要立即記錄受影響的項目時,請使用此工作流程。它能協助您記錄被移除的代碼、其使用次數、活動詳情,並生成一份摘要,供您透過 Slack 傳送給財務或營運部門。
這對於外洩的網紅代碼、意外曝光的優惠券,或是任何在代碼移除後需要提交書面影響報告的情況,都特別有用。
您將打造什麼
- 一個 Slack 工作流程,用於從 webhook 接收「
text」變數,並將該文字發佈至 Slack 頻道。 - 由「**折扣碼已移除」**觸發的「Shopify Flow」工作流程。
- 一個**「取得折扣」資料**步驟,會根據觸發器的 ID 查詢折扣。
- 一個用於格式化移除報告的「**執行程式碼」**步驟。
- 一個 HTTP 請求步驟,用於將報告傳送至 Slack。
開始之前
- 請確認該應用程式已安裝,且可在 Shopify 管理後台的「Apps」>「Workflow Trigger Extensions」中看到。
- 請確保您能存取 Shopify Flow。
- 建立或選擇一個 Slack 頻道,讓警示訊息顯示於該頻道中。
- 建立一個以 webhook 為起點,並能接收
text欄位的 Slack 工作流程。
影片指南
您也可以觀看完整的實作影片。
請先建立 Slack 工作流程
在 Slack 中,建立一個觸發條件為「**以 webhook 開始」**的工作流程。新增一個名為 text 的文字變數,接著新增「傳送訊息」步驟,使用該 text 變數將訊息傳送至您選擇的頻道。

在Workflow Trigger Extensions中授予折扣權限
在觸發器出現在「Shopify Flow」之前,請先在應用程式中啟用折扣存取權限。
在 Shopify 的管理後台中,前往「應用程式」>「Workflow Trigger Extensions」。
在儀表板上,請尋找設定檢查清單。
啟用「授予折扣存取權限」。此設定將啟用與折扣相關的觸發事件,例如「折扣建立」、「折扣更新」、「折扣刪除」、「折扣碼新增」、「折扣碼移除」及「折扣過期」。

建立Shopify Flow
開啟 Shopify Flow 並建立一個新的工作流程。
搜尋與折扣相關的觸發條件,並從「Workflow Trigger Extensions」中選取「折扣代碼已移除」。

新增「Shopify」動作「取得折扣資料」。
在「選擇查詢以篩選資料」中,選擇**「進階**」。在「編輯查詢」中,使用觸發器中的折扣 ID,並移除 Shopify GID 前綴:
id:"{{discount.id|remove:'gid://shopify/DiscountCodeNode/'}}"
在「**取得折扣資料」**之後,新增一個「**執行程式碼」**步驟。
請在**「從前幾個步驟中選取輸入項目」**中使用以下 GraphQL 查詢:
query {
redeemCode {
code
usageCount
bulkRemoval
}
getDiscountData {
discount {
... on DiscountCodeBasic {
title
startsAt
endsAt
asyncUsageCount
codes {
code
asyncUsageCount
}
customerGets {
value {
... on DiscountAmount {
amount { amount currencyCode }
}
... on DiscountPercentage {
percentage
}
}
}
}
}
}
}請按以下方式定義輸出:
"Output of discount code removal report"
type Output {
"Discount code that was removed"
code: String!
"Discount campaign title"
title: String!
"Campaign start date"
startsAt: String!
"Campaign end date"
endsAt: String!
"Number of times it was redeemed"
usageCount: Int!
"Total discount impact as a formatted string"
totalImpact: String!
"When it was removed"
removedAt: String!
"Full summary for alerts"
summary: String!
}接著將這段程式碼貼到「**撰寫程式碼」**欄位中:
export default function main(input) {
// ── Trigger: removed code ──
const removedCode = input?.redeemCode?.code ?? "Unknown";
const usageCount = input?.redeemCode?.usageCount ?? null; // null = bulk removal or unknown
const bulkRemoval = input?.redeemCode?.bulkRemoval ?? false;
const currency = input?.shop?.currencyCode ?? "";
const shopName = input?.shop?.name ?? "(not bound)";
const removedAt = new Date().toISOString();
// ── Find matching discount ──
const dataItems = input?.getDiscountData ?? [];
let matchedDiscount = null;
for (const item of dataItems) {
const d = item?.discount?.DiscountCodeBasic ?? item?.discount;
if (!d) continue;
const codesArray = Array.isArray(d.codes)
? d.codes.map(c => (typeof c === "string" ? c : c?.code ?? ""))
: [];
if (codesArray.includes(removedCode)) {
matchedDiscount = d;
break;
}
}
// fallback to first entry if removed code already gone from list
if (!matchedDiscount && dataItems.length > 0) {
matchedDiscount =
dataItems[0]?.discount?.DiscountCodeBasic ?? dataItems[0]?.discount ?? null;
}
const title = matchedDiscount?.title ?? "Untitled";
const startsAt = matchedDiscount?.startsAt ?? "N/A";
const endsAt = matchedDiscount?.endsAt ?? "None (no expiry)";
const totalUsageCount = matchedDiscount?.asyncUsageCount ?? null;
// ── Discount value ──
const valueWrapper = matchedDiscount?.customerGets?.value ?? {};
const pctObj = valueWrapper?.DiscountPercentage ?? null;
const amtObj = valueWrapper?.DiscountAmount ?? valueWrapper?.amount ?? null;
let discountLabel = "Unknown";
if (pctObj?.percentage != null) {
discountLabel = `${(pctObj.percentage * 100).toFixed(0)}% off`;
} else if (amtObj?.amount != null) {
const cur = amtObj.currencyCode || currency;
discountLabel = `${cur} ${parseFloat(amtObj.amount).toFixed(2)} off`;
}
// ── This code's impact ──
let thisCodeImpact;
if (bulkRemoval) {
thisCodeImpact = `Not available - removed as part of bulk deletion`;
} else if (usageCount === null) {
thisCodeImpact = `Could not be determined`;
} else if (pctObj?.percentage != null) {
thisCodeImpact = `${discountLabel} × ${usageCount} use(s)`;
} else if (amtObj?.amount != null) {
const perUse = parseFloat(amtObj.amount);
const cur = amtObj.currencyCode || currency;
thisCodeImpact = `${cur} ${(perUse * usageCount).toFixed(2)} (${cur} ${perUse.toFixed(2)} × ${usageCount} uses)`;
} else {
thisCodeImpact = "Unknown";
}
// ── Campaign-wide impact using asyncUsageCount on the discount ──
let campaignImpact;
if (totalUsageCount === null) {
campaignImpact = "Not available";
} else if (pctObj?.percentage != null) {
campaignImpact = `${discountLabel} × ${totalUsageCount} total use(s) across all codes`;
} else if (amtObj?.amount != null) {
const perUse = parseFloat(amtObj.amount);
const cur = amtObj.currencyCode || currency;
campaignImpact = `${cur} ${(perUse * totalUsageCount).toFixed(2)} total (${cur} ${perUse.toFixed(2)} × ${totalUsageCount} uses)`;
} else {
campaignImpact = "Unknown";
}
// ── Per-code breakdown ──
const codesNodes = Array.isArray(matchedDiscount?.codes)
? matchedDiscount.codes
: [];
const allCodes = codesNodes
.map(c => (typeof c === "string" ? c : c?.code ?? ""))
.filter(Boolean);
const codesRoster = codesNodes.length
? codesNodes.map(c => {
const code = typeof c === "string" ? c : c?.code ?? "";
const used = c?.asyncUsageCount ?? "?";
const flag = code === removedCode ? " ← REMOVED" : "";
return `${code} (used ${used}×)${flag}`;
}).join("\n ")
: "N/A";
const usageCountLabel = bulkRemoval
? "N/A (bulk removal)"
: usageCount !== null
? String(usageCount)
: "Unknown";
const summary =
`DISCOUNT CODE REMOVED\n` +
`─────────────────────\n` +
`Store: ${shopName}\n` +
`Code removed: ${removedCode}\n` +
`Bulk removal: ${bulkRemoval ? "Yes" : "No"}\n` +
`Campaign: ${title}\n` +
`Discount: ${discountLabel}\n` +
`Valid from: ${startsAt}\n` +
`Valid until: ${endsAt}\n` +
`\n` +
`THIS CODE\n` +
` Times used: ${usageCountLabel}\n` +
` Impact: ${thisCodeImpact}\n` +
`\n` +
`CAMPAIGN TOTAL\n` +
` Total uses: ${totalUsageCount ?? "Unknown"}\n` +
` Total impact: ${campaignImpact}\n` +
`\n` +
`CODE BREAKDOWN\n` +
` ${codesRoster}\n` +
`\n` +
`Removed at: ${removedAt}`;
const safeSummary = summary
.replace(/\\/g, "\\\\") // escape backslashes first
.replace(/"/g, '\\"') // escape quotes
.replace(/\n/g, "\\n") // escape newlines
.replace(/\r/g, "\\r");
return {
code: removedCode,
bulkRemoval,
title,
discountLabel,
startsAt,
endsAt,
usageCount: usageCount ?? 0,
usageCountLabel,
thisCodeImpact,
totalUsageCount: totalUsageCount ?? 0,
campaignImpact,
allCodes,
codesRoster,
removedAt,
summary: safeSummary,
};
}
新增「HTTP 請求」動作。在「變數」或「**內容覆寫 (JSON)」**欄位中,傳送由「執行程式碼」步驟所回傳的摘要:
{
"body": {
"text": "{{runCode.summary}}"
}
}此操作會透過 text 變數,將生成的摘要發佈至 Slack 網路鉤子工作流程。

完成所有步驟後,請儲存此工作流程。
要進行測試,請從您的折扣活動中刪除一個折扣代碼,並確認 Slack 頻道是否收到摘要訊息。

Slack 通知的內容包含哪些項目
- 已移除的折扣代碼
- 無論是大量清運
- 活動名稱
- 折扣金額
- 開始與結束日期
- 已移除程式碼的執行次數
- 移除該程式碼的預估影響
- 整個活動的使用情況與影響
- 若有相關資料,將按代碼逐一分析
- 執行移除操作時的時間戳記
問題排除
請返回「應用程式」>「Workflow Trigger Extensions」,並確認**「授予折扣存取權限」**已啟用。只有在授予存取權限後,折扣觸發條件才會顯示。
請在「**編輯查詢」**中仔細檢查進階查詢。ID 應完全依照以下所示進行清理:
id:"{{discount.id|remove:'gid://shopify/DiscountCodeNode/'}}"請確保 Slack 工作流程是以 Webhook 為起點,且 Webhook 的 URL 即為您的 HTTP 請求所使用的網址,同時請求內容中須包含 text 欄位。
某些刪除操作可能是批次處理的一部分,或者可能無法回傳完整的使用詳情。在這些情況下,摘要仍會傳送現有最完整的資訊,並清楚標示缺失的值。
最佳實務
- 請將這些警示發送至專用的財務或營運通訊頻道,而非一般支援通訊頻道。
- 請為工作流程命名時採用具體的名稱(例如「移除折扣碼」),以便在 Shopify Flow 和 Slack 中輕鬆辨識。
- 請先使用非生產環境的折扣代碼進行測試,以確認訊息格式和 Webhook 的運作行為。

