割引コード削除に関する通知

このワークフローを使用するタイミング

割引コードが削除され、その影響を受けた内容を直ちに記録する必要がある場合は、このワークフローをご利用ください。これにより、削除されたコード、その使用回数、キャンペーンの詳細、およびSlackを通じて経理や運営部門に送信できる要約情報を把握することができます。

これは、インフルエンサーコードの流出や、予定外のクーポン公開、あるいはコードが削除された後に影響に関する報告書を作成する必要がある場合などに特に役立ちます。

制作内容

  • Webhookからtext変数を受け取り、そのテキストをSlackチャンネルに投稿するSlackワークフロー。
  • 割引コードの削除」によってトリガーされる「Shopify Flow」ワークフロー。
  • トリガーからのIDを使用して割引を検索する「割引を取得データステップ。
  • 削除レポートの書式設定を行う「Run」コードステップ。
  • レポートをSlackに送信するHTTPリクエストステップ。

始める前に

  • アプリがインストールされており、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の管理画面で、「Apps」>「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のWebhookワークフローに投稿されます。

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の動作を確認してください。