La demande client
L’objectif est de développer une promotion appliquant des réductions sur une sélection de bougies dans le panier. Le moteur de promotions standard de Shopify ne permettant pas de répondre à cette exigence, la Product Discount API est utilisée pour une personnalisation avancée.
Le principe de la promotion
Une réduction de 2 € est appliquée pour l’achat de trois bougies, à condition qu’au moins l’une d’elles soit une “bougie du mois”.
L’offre est cumulable : pour six bougies achetées, si deux d’entre elles sont des “bougies du mois”, la réduction passe à 4 €.
Les cas particuliers
Le client indique que certaines situations peuvent complexifier l’application de l’offre. Sans définir de règle générale, il identifie les cas suivants :
- 3 bougies du mois → réduction de 2 €.
- 1 bougie normale + 2 bougies du mois → réduction de 2 €.
- 2 bougies normales + 4 bougies du mois → réduction de 4 €.
Formalisation du problème
Le client parvient à obtenir le bon résultat, mais sans pouvoir expliciter son raisonnement.
Nous structurons donc l’approche en deux étapes :
- Mathématique : formaliser le problème en une règle générale.
- Informatique : traduire cette règle en algorithme dans Shopify.
Formule mathématique
Définition des variables
Soit :
- le nombre de bougies achetées.
- le nombre de bougies du mois parmi les bougies achetées.
Règle d’application des réductions
Les réductions s’appliquent par groupe de trois bougies achetées. Chaque groupe permet d’obtenir au maximum une réduction sur une bougie du mois.
- Si un groupe contient plusieurs bougies du mois, une seule bénéficie de la réduction.
- Si un groupe ne contient aucune bougie du mois, aucune réduction ne s’applique.
Calcul du nombre de réduction possibles
Le premier critère à considérer est le nombre total de groupes de trois bougies formés. Ce nombre est donné par :
Il représente le nombre maximal de réductions possibles, sans encore prendre en compte la présence des bougies du mois.
Formule finale
Le nombre total de réductions ne peut dépasser le nombre de bougies du mois disponibles. Il faut donc comparer le nombre de réductions théoriques avec .
Ainsi, la réduction totale appliquée est donnée par la formule suivante
où :
- est le montant de la réduction par groupe en l’occurence 2 €.
- représente le nombre maximum de groupes de 3 bougies.
- est le nombre de bougies du mois.
Les avantages de l’approche mathématique
- La solution a une complexité constante soit .
- La formule est générale et fonctionne pour tous les cas.
- La solution est indépendante de tout langage de programmation, ce qui la rend universelle et facilement adaptable.
Implémentation dans Shopify
Créer l’application
Assurez-vous d’avoir installer la CLI Shopify, vous pouvez retrouver les instructions ici.
L’application doit regrouper toutes les extensions et API Functions nécessaires. Pour l’initialiser, utilisez la commande suivante :
shopify app init
Pour des raisons de concision, nous sélectionnons Build an extension-only app
Il s’agit d’une nouvelle application :
Définissez un nom pour l’application :
Si tout se passe correctement, vous verrez ce message de confirmation :
Accédez au répertoire de votre projet :
cd tutorial
Une fois l’application créée, voici la structure du projet généré :
├── extensions
├── node_modules
├── package.json
├── package-lock.json
├── README.md
├── SECURITY.md
└── shopify.app.toml
Générer l’API Product Discount
Dans le dossier tutoriel, générez l’API Product Discount avec la commande suivante :
shopify app generate extension
Une invite de commande propose une liste d’extensions. Sélectionnez : Discount products - Function.
Ensuite, nommez la Product Discount Function et choisissez un langage de programmation. Pour simplifier l’exemple, nous utilisons JavaScript, mais il est fortement recommandé d’opter pour un langage typé (comme TypeScript ou Rust).
La fonction se trouve dans le dossier extensions de l’application. Pour y accéder, utilisez :
cd extensions
La structure est la suivante :
.
└── product-discount
├── generated
│ └── api.ts
├── locales
│ └── en.default.json
├── package.json
├── schema.graphql
├── shopify.extension.toml
├── src
│ ├── index.js
│ ├── run.graphql
│ ├── run.js
│ └── run.test.js
└── vite.config.js
La logique métier de la réduction est implémentée dans le fichier src/run.js
Installer l’application sur le store
Saisir la commande
npm run dev
Une fenêtre s’ouvre
Présentation de l’API Product Discount
L’API Product Discount permet de créer des réductions appliquées directement aux produits présents dans le panier.
- Définir le paramètre de la fonction : il contient les informations du panier et des produits.
- Implémenter la logique métier : déterminer les réductions applicables selon les règles définies.
- Retourner les promotions appliquées : la fonction renvoie la liste des réductions associées aux produits.
Définition du paramètre de la fonction
Le paramètre de la fonction est définie via GraphQL dans le fichier run.graphql. Par défaut, seules les informations suivantes sont disponibles :
- L’ID des lignes du panier.
- Leur quantité associée.
query RunInput {
cart {
lines {
id
quantity
}
}
}
Dans le panier, nous ciblons uniquement les bougies. Il faut donc récupérer les attributs permettant de les identifier :
- L’ID de la variante : pour appliquer la réduction au bon produit.
- Le type de produit : ici “Bougie”.
- Le tag “bougie du mois” : pour distinguer ces bougies des autres.
- La quantité associée : essentielle pour le calcul des réductions.
query RunInput {
cart {
lines {
quantity
merchandise {
... on ProductVariant {
id
product {
productType
hasTags(tags: ["bougie du mois"]) {
hasTag
tag
}
}
}
}
}
}
discountNode {
metafield(
namespace: "$app:product-discount"
key: "function-configuration"
) {
value
}
}
}
Tous les attributs disponibles peuvent être retrouvés dans le fichier schema.graphql.
Pour générer les types associés à cet appel GraphQL, utilisez la commande suivante :
shopify app function typegen
Définition de la logique métier
Le fichier run.js contient la fonction principale qui applique la logique métier :
const EMPTY_DISCOUNT = {
discountApplicationStrategy: DiscountApplicationStrategy.First,
discounts: [],
};
export function run(input) {
const configuration = JSON.parse(
input?.discountNode?.metafield?.value ?? "{}",
);
return EMPTY_DISCOUNT;
}
- Pour garder cet article concis, la variable configuration est ignorée.
- L’approche proposée est volontairement rigide afin de simplifier l’explication.
Le paramètre input est défini dans la section précédente via GraphQL. Il contient :
- Les lignes du panier
- La quantité des articles
- Le type et le nom du produit
- La présence du tag “bougie du mois”
Pour éviter de surcharger la fonction run, nous regroupons les fonctions auxiliaires dans le fichier src/tools.js :
.
└── product-discount
├── generated
│ └── api.ts
├── locales
│ └── en.default.json
├── package.json
├── schema.graphql
├── shopify.extension.toml
├── src
│ ├── index.js
│ ├── run.graphql
│ ├── run.js
│ ├── run.test.js
│ └── tools.js
└── vite.config.js
Récupérer et compter les bougies
Nous avons désormais tous les outils nécessaires pour appliquer notre logique. L’étape suivante consiste à :
- Récupérer toutes les bougies présentes dans le panier.
- Compter le nombre de bougies du mois.
Dans le fichier src/tools.js nous définissons les fonctions suivantes :
// Filtre tous les produits du panier pour ne garder que les bougies
export function getCandles(input) {
return input.cart.lines.filter(
({ merchandise: { product } }) => product.productType === "Bougie",
);
}
// Filtre tous les produits du panier pour ne garder que les bougies du mois
export function getMonthlyCandles(input) {
const { lines } = input.cart;
return lines.filter(({ merchandise }) =>
merchandise.product.hasTags?.some(isMonthlyCandle),
);
}
// Vérifie si un élément est une "bougie du mois"
export function isMonthlyCandle({ tag, hasTag }) {
return tag === "bougie du mois" && hasTag;
}
// Additionne les quantités
export function getTotalQuantity(lines) {
return lines.reduce((acc, line) => acc + line.quantity, 0);
}
Dans le fichier src/run.js, on appelle les fonctions :
const EMPTY_DISCOUNT = {
discountApplicationStrategy: DiscountApplicationStrategy.First,
// Un tableau vide signifie l'absence de réduction
discounts: [],
};
export function run(input) {
// Montant de la réduction par bougie du mois éligible
const discount = 2;
const candles = getCandles(input);
const monthlyCandles = getMonthlyCandles(input);
const nbCandles = getTotalQuantity(candles);
const nbMonthlyCandles = getTotalQuantity(monthlyCandles);
return EMPTY_DISCOUNT;
}
Appliquer la formule mathématique
Dans tools.js, nous définissons la formule mathématique :
// Calcule la réduction totale
export function getTotalDiscount(discount, nbCandles, nbMonthlyCandles) {
const groupSize = 3; // Réduction appliquée par groupe de 3 bougies
const discountByGroup = Math.floor(nbCandles / groupSize);
const discountedMonthlyCandles = Math.min(discountByGroup, nbMonthlyCandles);
return discount * discountedMonthlyCandles;
}
Dans run.js, on appelle la fonction :
const EMPTY_DISCOUNT = {
discountApplicationStrategy: DiscountApplicationStrategy.First,
// Un tableau vide signifie l'absence de réduction
discounts: [],
};
export function run(input) {
// Montant de la réduction par bougie du mois éligible
const discount = 2;
const candles = getCandles(input);
const monthlyCandles = getMonthlyCandles(input);
const nbCandles = getTotalQuantity(candles);
const nbMonthlyCandles = getTotalQuantity(monthlyCandles);
const totalDiscount = getTotalDiscount(discount, nbCandles, nbMonthlyCandles);
return EMPTY_DISCOUNT;
}
Attribuer les réductions par ligne
Si totalDiscount est égal à zéro, aucune réduction ne s’applique :
const EMPTY_DISCOUNT = {
discountApplicationStrategy: DiscountApplicationStrategy.First,
// Un tableau vide signifie l'absence de réduction
discounts: [],
};
export function run(input) {
// Montant de la réduction par bougie du mois éligible
const discount = 2;
const candles = getCandles(input);
const monthlyCandles = getMonthlyCandles(input);
const nbCandles = getTotalQuantity(candles);
const nbMonthlyCandles = getTotalQuantity(monthlyCandles);
const totalDiscount = getTotalDiscount(discount, nbCandles, nbMonthlyCandles);
if (totalDiscount === 0) {
return EMPTY_DISCOUNT;
}
}
Les réductions sont appliquées aux bougies du mois éligibles. La distribution des remises est gérée dans la fonction applyDiscounts, définie dans tools.js :
// Distribue la reduction par ligne dans le panier
export function applyDiscounts(discount, totalDiscount, candles) {
const discounts = [];
let totalApplied = 0;
let index = 0;
while (totalApplied < totalDiscount && index < candles.length) {
const candle = candles[index];
const allocated = allocateDiscount(discount, totalDiscount, candle);
const candleDiscount = createDiscount(candle, allocated);
discounts.push(candleDiscount);
totalApplied += allocated;
index++;
}
return discounts;
}
export function allocateDiscount(discount, totalDiscount, candle) {
const allocatedDiscount = discount * candle.quantity;
return Math.min(allocatedDiscount, totalDiscount);
}
export function createDiscount(candle, discount) {
return {
value: {
fixedAmount: {
amount: discount,
},
},
targets: [
{
productVariant: {
id: candle.merchandise.id,
},
},
],
message: "Offre Bougie du mois",
};
}
Il ne reste plus qu’à appeler applyDiscounts dans la fonction run et retourner les réductions appliquées :
export function run(input) {
// Montant de la réduction par bougie du mois éligible
const discount = 2;
const candles = getCandles(input);
const monthlyCandles = getMonthlyCandles(input);
const nbCandles = getTotalQuantity(candles);
const nbMonthlyCandles = getTotalQuantity(monthlyCandles);
const totalDiscount = getTotalDiscount(discount, nbCandles, nbMonthlyCandles);
if (totalDiscount === 0) {
return EMPTY_DISCOUNT;
}
return {
discountApplicationStrategy: DiscountApplicationStrategy.First,
discounts: applyDiscounts(discount, totalDiscount, monthlyCandles),
};
}
Il ne reste plus qu’à déployer la fonction
npm run deploy
Aller plus loin
Testing de l’application
Une étape essentielle consiste à intégrer des tests unitaires dans run.test.js afin de valider le bon fonctionnement de la logique métier. Cela permet de s’assurer que les réductions sont correctement appliquées et de prévenir toute régression.
Rendre la logique plus adaptable à l’évolution métier
L’approche actuelle est rigide et ne prend pas en compte les évolutions possibles de la logique métier. Plusieurs éléments sont actuellement fixes :
- Le montant de la réduction (actuellement fixé à 2 €).
- Le nombre d’articles nécessaires pour qu’une réduction s’applique (groupe de 3 bougies).
- Type de produit ciblé (“Bougie”).
- Tag recherché (“bougie du mois”).
Pour une approche plus flexible, ces paramètres devraient être externalisés dans la configuration de l’application afin d’être modifiables sans toucher au code source.
Vous pouvez retrouvez toutes les informations nécessaires sur cette page.
Proposer une interface utilisateur
Lors de la création de l’application, l’option Build a Remix app aurait dû être privilégiée.
Obtenir le projet
Le code est source du projet est disponible dans ce repo.