Alertes concernant la suppression des codes de réduction

Quand utiliser ce workflow ?

Utilisez ce workflow lorsqu’un code de réduction est supprimé et que vous avez besoin d’un relevé immédiat des éléments concernés. Il vous permet de recenser le code supprimé, son nombre d’utilisations, les détails de la campagne, ainsi qu’un récapitulatif que vous pouvez envoyer au service financier ou aux opérations via Slack.

Cette fonctionnalité s'avère particulièrement utile en cas de fuite de codes d'influenceurs, de divulgation imprévue de bons de réduction ou dans toute situation où vous avez besoin d'un rapport d'impact documenté après la suppression d'un code.

Ce que vous allez créer

  • Un workflow Slack qui reçoit une variable « text » via un webhook et publie ce même texte dans un canal Slack.
  • Un workflow Shopify Flow déclenché par la suppression d'un code de réduction.
  • Une étape de traitement « Obtenir la remise » qui recherche la remise à l'aide de l'identifiant fourni par le déclencheur.
  • Une étape « Exécuter le code » qui génère un rapport de suppression.
  • Une étape « Requête HTTP » qui envoie le rapport vers Slack.

Avant de commencer

  • Assurez-vous que l'application est bien installée et qu'elle apparaît dans l'interface d'administration d'Shopify, sous « **Apps » > « Workflow Trigger Extensions **».
  • Assurez-vous d'avoir accès à Shopify Flow.
  • Créez ou sélectionnez un canal Slack dans lequel les alertes doivent s'afficher.
  • Créez un workflow Slack qui se déclenche par un webhook et accepte un champ « text ».

Guide vidéo

Vous pouvez également visionner la vidéo complète de la mise en œuvre.

Commencez par créer le workflow Slack

Dans Slack, créez un workflow dont le déclencheur est « Commence par un webhook ». Ajoutez une variable de texte nommée « text », puis ajoutez l'étape « Message » qui envoie un message au canal de votre choix en utilisant cette même variable « text ».

Workflow Slack qui se déclenche via un webhook et envoie la variable « text » à un canal Slack
Workflow Slack qui se déclenche via un webhook et envoie la variable « text » vers un canal Slack

Accorder un accès à tarif réduit dans l'Workflow Trigger Extensions

Avant que le déclencheur n'apparaisse dans « Shopify Flow », activez l'accès aux remises dans l'application.

Dans l'interface d'administration d'Shopify, accédez à Applications > Workflow Trigger Extensions.

Dans le tableau de bord, recherchez la liste de contrôle de configuration.

Activez l'option « Accorder l'accès aux remises ». Cela active les déclencheurs liés aux remises, tels que « Remise créée », « Remise mise à jour », « Remise supprimée », « Code de remise ajouté », « Code de remise supprimé » et « Remise expirée ».

Workflow Trigger Extensions Tableau de bord indiquant que l'accès à Grant Discount est activé
Workflow Trigger Extensions Tableau de bord indiquant que l'accès à Grant Discount est activé

Construire l’Shopify Flow

Ouvrez Shopify Flow et créez un nouveau workflow.

Recherchez les déclencheurs liés aux réductions et sélectionnez « Code de réduction supprimé » dans «Workflow Trigger Extensions ».

Shopify Flow Sélecteur de déclenchement avec code de réduction : sélection désactivée
Shopify Flow Sélecteur de déclenchement avec code de réduction : « Sélectionné » supprimé

Ajoutez l'action « Obtenir les données de réduction » de l'Shopify.

Dans « Sélectionner une requête pour filtrer les données », choisissez « Avancé ». Dans « Modifier la requête », utilisez l'ID de remise issu du déclencheur et supprimez le préfixe GID « Shopify » :

bash
id:"{{discount.id|remove:'gid://shopify/DiscountCodeNode/'}}"
Récupérer les données relatives aux promotions à l'aide d'une requête avancée avec l'identifiant de promotion nettoyé
Récupérer les données relatives aux promotions à l'aide d'une requête avancée avec l'identifiant de promotion nettoyé

Ajoutez une étape « Exécuter le code » après « Récupérer les données sur les réductions ».

Utilisez la requête GraphQL suivante dans les champs de saisie « Sélectionner » des étapes précédentes :

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

Définissez les sorties comme suit :

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

Ensuite, collez ce code dans le champ « Écrire le code » :

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,
  };
}
Exécuter une étape de code dans « Shopify Flow » à l'aide de la requête, des résultats et du générateur de rapports JavaScript
Exécuter une étape de code dans « Shopify Flow » à l'aide de la requête, des résultats et du générateur de rapports JavaScript

Ajoutez l'action « Requête HTTP ». Dans le champ « Variables » ou « Remplacement du corps (JSON) », envoyez le résumé renvoyé par l'étape « Exécuter le code » :

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

Cette opération publie le résumé généré dans le workflow du webhook Slack à l'aide de la variable « text ».

Action « Requête HTTP » dans « Shopify Flow » : envoi du résumé du code d'exécution (runCode) dans le champ de texte
Action « Requête HTTP » dans « Shopify Flow » : envoi du résumé du code d'exécution dans le champ de texte

Enregistrez le flux de travail une fois que toutes les étapes sont en place.

Pour tester, supprimez un code de réduction de votre campagne promotionnelle et vérifiez que le canal Slack reçoit bien le message récapitulatif.

Message Slack indiquant que le code de réduction a été supprimé (résumé)
Message Slack indiquant que le code de réduction a été supprimé (résumé)

Contenu de l'alerte Slack

  • Le code de réduction supprimé
  • Qu'il s'agisse d'un enlèvement en gros
  • Le titre de la campagne
  • La valeur actualisée
  • Dates de début et de fin
  • Fréquence d'utilisation du code supprimé
  • Impact estimé de la suppression du code
  • Utilisation et impact à l'échelle de la campagne
  • Une ventilation par code, lorsqu'elle est disponible
  • L'horodatage correspondant au moment où la suppression a été effectuée

Dépannage

Retournez dans « Applications » > « Workflow Trigger Extensions » et vérifiez que l'option « Accorder l'accès à la remise » est activée. Les déclencheurs de remise n'apparaissent qu'une fois l'accès accordé.

Vérifiez bien la requête avancée dans « Modifier la requête ». L'ID doit être saisi exactement comme indiqué :

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

Assurez-vous que le workflow Slack démarre par un webhook, que l'URL du webhook correspond à celle utilisée par votre requête HTTP et que le corps de la requête inclut le champ « text ».

Certaines suppressions peuvent s'inscrire dans le cadre d'une action groupée ou ne pas fournir toutes les informations relatives à l'utilisation. Dans ces cas-là, le récapitulatif fournira tout de même les meilleures informations disponibles et indiquera clairement les valeurs manquantes.

Bonnes pratiques

  • Envoyez ces alertes vers un canal dédié aux finances ou aux opérations plutôt que vers un canal d'assistance général.
  • Choisissez un nom précis pour le flux de travail, par exemple « Code de réduction supprimé », afin qu'il soit facile à identifier dans Shopify Flow et sur Slack.
  • Commencez par effectuer un test avec un code de réduction hors environnement de production afin de vérifier le format du message et le fonctionnement du webhook.