「折扣碼已移除」通知

何時應使用此工作流程

當折扣代碼被移除,且您需要立即記錄受影響的項目時,請使用此工作流程。它能協助您記錄被移除的代碼、其使用次數、活動詳情,並生成一份摘要,供您透過 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 變數將訊息傳送至您選擇的頻道。

一個由 Webhook 觸發,並將文字變數傳送至 Slack 頻道的 Slack 工作流程
一個由 Webhook 觸發,並將文字變數傳送至 Slack 頻道的 Slack 工作流程

在Workflow Trigger Extensions中授予折扣權限

在觸發器出現在「Shopify Flow」之前,請先在應用程式中啟用折扣存取權限。

在 Shopify 的管理後台中,前往「應用程式」>Workflow Trigger Extensions」。

在儀表板上,請尋找設定檢查清單。

啟用「授予折扣存取權限」。此設定將啟用與折扣相關的觸發事件,例如「折扣建立」、「折扣更新」、「折扣刪除」、「折扣碼新增」、「折扣碼移除」及「折扣過期」。

Workflow Trigger Extensions 儀表板顯示「Grant Discount Access」已啟用
Workflow Trigger Extensions 儀表板顯示「Grant Discount Access」已啟用

建立Shopify Flow

開啟 Shopify Flow 並建立一個新的工作流程。

搜尋與折扣相關的觸發條件,並從「Workflow Trigger Extensions」中選取「折扣代碼已移除」。

Shopify Flow 觸發器選擇器中的「已移除折扣碼」選項
Shopify Flow 觸發器選擇器中的「已移除優惠碼」選項

新增「Shopify」動作「取得折扣資料」。

在「選擇查詢以篩選資料」中,選擇**「進階**」。在「編輯查詢」中,使用觸發器中的折扣 ID,並移除 Shopify GID 前綴:

bash
id:"{{discount.id|remove:'gid://shopify/DiscountCodeNode/'}}"
使用經過清理的折扣 ID 進行進階查詢,以取得折扣資料
使用經過清理的折扣 ID 進行進階查詢,以取得折扣資料

在「**取得折扣資料」**之後,新增一個「**執行程式碼」**步驟。

請在**「從前幾個步驟中選取輸入項目」**中使用以下 GraphQL 查詢:

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
            }
          }
        }
      }
    }
  }
}

請按以下方式定義輸出:

typescript
"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!
}

接著將這段程式碼貼到「**撰寫程式碼」**欄位中:

javascript
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,
  };
}
在 Shopify Flow 中執行程式碼步驟,並包含查詢、輸出結果及 JavaScript 報表建構器
在 Shopify Flow 中執行程式碼步驟,並包含查詢、輸出結果及 JavaScript 報表建構器

新增「HTTP 請求」動作。在「變數「**內容覆寫 (JSON)」**欄位中,傳送由「執行程式碼」步驟所回傳的摘要:

css
{
  "body": {
    "text": "{{runCode.summary}}"
  }
}

此操作會透過 text 變數,將生成的摘要發佈至 Slack 網路鉤子工作流程。

Shopify Flow 中的 HTTP 請求動作,用於將 runCode 摘要傳送至文字欄位
Shopify Flow 中的 HTTP 請求動作,將 runCode 摘要傳送至文字欄位

完成所有步驟後,請儲存此工作流程。

要進行測試,請從您的折扣活動中刪除一個折扣代碼,並確認 Slack 頻道是否收到摘要訊息。

Slack 訊息中顯示的折扣碼已被移除,摘要如下
Slack 訊息中顯示的折扣代碼已移除摘要

Slack 通知的內容包含哪些項目

  • 已移除的折扣代碼
  • 無論是大量清運
  • 活動名稱
  • 折扣金額
  • 開始與結束日期
  • 已移除程式碼的執行次數
  • 移除該程式碼的預估影響
  • 整個活動的使用情況與影響
  • 若有相關資料,將按代碼逐一分析
  • 執行移除操作時的時間戳記

問題排除

請返回「應用程式>「Workflow Trigger Extensions」,並確認**「授予折扣存取權限」**已啟用。只有在授予存取權限後,折扣觸發條件才會顯示。

請在「**編輯查詢」**中仔細檢查進階查詢。ID 應完全依照以下所示進行清理:

bash
id:"{{discount.id|remove:'gid://shopify/DiscountCodeNode/'}}"

請確保 Slack 工作流程是以 Webhook 為起點,且 Webhook 的 URL 即為您的 HTTP 請求所使用的網址,同時請求內容中須包含 text 欄位。

某些刪除操作可能是批次處理的一部分,或者可能無法回傳完整的使用詳情。在這些情況下,摘要仍會傳送現有最完整的資訊,並清楚標示缺失的值。

最佳實務

  • 請將這些警示發送至專用的財務或營運通訊頻道,而非一般支援通訊頻道。
  • 請為工作流程命名時採用具體的名稱(例如「移除折扣碼」),以便在 Shopify Flow 和 Slack 中輕鬆辨識。
  • 請先使用非生產環境的折扣代碼進行測試,以確認訊息格式和 Webhook 的運作行為。