Aller au contenu principal

Exemples - Calcul promotions

· 7 minutes de lecture
Mohammed Boukada

Dans ce billet, nous allons voir ensemble comment implémenter un cas d'usage simple avec RuleShake.

Pour les impatients, voici le lien vers cet exemple : https://demo.ruleshake.com/discount

Besoin

Prenons un site e-commerce modeste vendant une douzaine d'articles (vraiment très modeste !) pour lequel nous voulons mettre en place un système de promotion.

Nous avons besoin de modéliser un référentiel d'articles ou pour chaque article définir son libellé, sa catégorie, son prix et une image.

Nous souhaitons mettre en plus 4 promotions :

  • 0,03 € de remise pour chaque lot de 3 articles de la même catégorie
  • 0,05 € de remise pour chaque pot de Nutella
  • 0,10 € de remise pour chaque lot de 5 articles (quelque soit leurs catégories)
  • 0,50 € de remise si le montant du panier (en soustrayant les autres remises) dépasse 10 €

Analyse

D'un point de vue RuleShake, on va pouvoir identifier deux éléments dans ce besoin :

  • Un dataset représentant le référentiel d'articles qui sera défini dans RuleShake Referential
  • Une collection permettant le calcul des promotions qui sera définie dans RuleShake Catalog puis publiée pour être exécutée dans RuleShake Runner

Implémentation

Référentiel de produits

Commençons donc par définir le dataset référentiel d'articles dans RuleShake Referential :

On voit ici que nous avons créé un dataset qui a pour code ARTICLES et qui est composé de 4 propriétés :

  • LABEL : le libellé de l'article
  • CATEGORY : la catégorie de l'article
  • IMAGE : lien vers une image représentant l'article
  • PRICE : le prix de l'article

En plus de ces propriétés, chaque entrée (ou record) dans ce dataset devra renseigner un code unique. Pour cet exemple, nous allons renseigner le code barre (EAN13) de l'article :

Nous avons fini le paramétrage dans RuleShake Referential pour la création du référentiel des produits. Nous allons maintenant créer la collection qui permet le calcul des promotions.

Calcul de la promotion

Nous allons maintenant créer une collection dans RuleShake Catalog que nous allons appeler DISCOUNT et dans laquelle on va définir deux variables :

  • ARTICLE : une variable d'entrée qui représente un article ajouté dans le panier. Cette variable est multiple, elle peut donc recevoir plusieurs valeurs. Elle est de type Record, elle est donc liée à un dataset défini dans RuleShake Referential.
  • DISCOUNTS : une variable calculée de type Composite. Cela veut dire que cette variable n'a pas de valeur directe, mais contient des sous-variables.

Regardons maintenant la définition de la variable ARTICLE :

Comme mentionné précédemment, les variables de type Record sont forcément liées à un dataset défini dans RuleShake Referential. Dans cet exemple, ARTICLE est lié au dataset ARTICLES que nous avons défini au début. La variable ARTICLE est définie comme multiple et pouvant avoir jusqu'à 10 000 valeurs.

Lors de l'appel de RuleShake Runner pour l'évaluation de la collection DISCOUNT, pour chaque instance de la variable ARTICLE on donnera le code d'un record du dataset ARTICLES, exemple :

{
"type": "record",
"reference": "ARTICLE[0]",
"value": "3017620425035"
}

Pour les variables de type Record, RuleShake Runner va interroger RuleShake Referential pour résoudre ces variables et construire une variable composite avec le même nom et ayant comme sous-variables les propriétés du dataset.

Dans la définition de la variable ARTICLE, on visualise la liste des sous-variables qui seront résolues au runtime :

Ces sous-variables peuvent être utilisées dans les formules de calcul des autres variables calculées.

Maintenant, intéressons-nous à la variable DISCOUNTS qui porte les promotions :

Nous avons ici modélisé les 4 promotions demandées sous forme de variables calculées de type Nombre. Dans chacune de ces variables, on va définir le calcul de la promotion concernée.

Prenons l'exemple de la promotion NUTELLA :

Dans la définition de la valeur de cette variable, nous avons laissé le champ statique Valeur à vide et nous avons défini une formule de calcul. Cette formule est écrite dans le langage mvel.

Nous filtrerons les instances de la variable ARTICLE pour ne garder que celles qui ont la sous-variable LABEL valant Nutella. Ensuite, si la liste résultante est vide, on retourne 0 sinon on multiplie la taille de cette liste par 0.05.

Pour les autres variables promotions :

PromotionFormule
SAME_CATEGORY
groups = MultipleHelper.groupBy(ARTICLE, "CATEGORY");
count = 0;
for (group : groups.entrySet()) {
  if (group.value.size() > 2) {
    count = count + NumberHelper.floorDiv(group.value.size(), 3);
  }
}
return count * 0.03;
NB_ARTICLES
NumberHelper.floorDiv(MultipleHelper.count(ARTICLE), 5) * 0.1
TOTAL_AMOUNT
discount = DISCOUNTS.NB_ARTICLES.value + DISCOUNTS.SAME_CATEGORY.value + DISCOUNTS.NUTELLA.value;
totalAmount = NumberHelper.sum(MultipleHelper.extract(ARTICLE, "PRICE"));
if (totalAmount - discount > 10) {
  return 0.5;
} else {
  return 0;
}

Utilisation

Maintenant que nous avons terminé le paramétrage de la collection DISCOUNT, on peut commencer à l'évaluer avec des variables représentants les articles.

Une API exposée par RuleShake Runner permet d'évaluer une collection en lui passant en entrée des inputs assignant des valeurs aux variables de nature Entrée

Exemple de requête :

POST ruleshake-runner/api/v1/evaluations

{
"requestTime": "2023-06-11T00:00",
"collectionCode": "DISCOUNT",
"inputs": [
{
"reference": "ARTICLE[0]",
"value": "3017620425035",
"type": "record"
},
{
"reference": "ARTICLE[1]",
"value": "3083680973939",
"type": "record"
},
{
"reference": "ARTICLE[2]",
"value": "3083680973939",
"type": "record"
},
{
"reference": "ARTICLE[3]",
"value": "3083681085860",
"type": "record"
},
{
"reference": "ARTICLE[4]",
"value": "3700009252567",
"type": "record"
}
]
}

En réponse, nous obtenons le résultat suivant (payload épuré des champs techniques pour améliorer la lecture) :

Cliquer pour développer
{
"reference": {
"code": "DISCOUNT",
"version": 1
},
"variables": [
{
"runtimeReference": "ARTICLE[0]",
"definitionReference": "ARTICLE",
"type": "RECORD",
"value": "3017620425035",
"datasetCode": "ARTICLES",
"subVariables": [
{
"runtimeReference": "ARTICLE[0]/IMAGE",
"definitionReference": "ARTICLE/IMAGE",
"type": "STRING",
"value": "https://images.openfoodfacts.org/images/products/301/762/042/5035/front_fr.427.400.jpg"
},
{
"runtimeReference": "ARTICLE[0]/PRICE",
"definitionReference": "ARTICLE/PRICE",
"type": "NUMBER",
"value": 3.49
},
{
"runtimeReference": "ARTICLE[0]/CATEGORY",
"definitionReference": "ARTICLE/CATEGORY",
"type": "STRING",
"value": "Sweet groceries"
},
{
"runtimeReference": "ARTICLE[0]/LABEL",
"definitionReference": "ARTICLE/LABEL",
"type": "STRING",
"value": "Nutella"
}
]
},
{
"runtimeReference": "ARTICLE[1]",
"definitionReference": "ARTICLE",
"type": "RECORD",
"value": "3083680973939",
"datasetCode": "ARTICLES",
"subVariables": [
{
"runtimeReference": "ARTICLE[1]/IMAGE",
"definitionReference": "ARTICLE/IMAGE",
"type": "STRING",
"value": "https://images.openfoodfacts.org/images/products/308/368/097/3939/front_fr.79.400.jpg"
},
{
"runtimeReference": "ARTICLE[1]/PRICE",
"definitionReference": "ARTICLE/PRICE",
"type": "NUMBER",
"value": 2.89
},
{
"runtimeReference": "ARTICLE[1]/CATEGORY",
"definitionReference": "ARTICLE/CATEGORY",
"type": "STRING",
"value": "Canned"
},
{
"runtimeReference": "ARTICLE[1]/LABEL",
"definitionReference": "ARTICLE/LABEL",
"type": "STRING",
"value": "Ratatouille Cassegrain"
}
]
},
{
"runtimeReference": "ARTICLE[2]",
"definitionReference": "ARTICLE",
"type": "RECORD",
"value": "3083680973939",
"datasetCode": "ARTICLES",
"subVariables": [
{
"runtimeReference": "ARTICLE[2]/IMAGE",
"definitionReference": "ARTICLE/IMAGE",
"type": "STRING",
"value": "https://images.openfoodfacts.org/images/products/308/368/097/3939/front_fr.79.400.jpg"
},
{
"runtimeReference": "ARTICLE[2]/PRICE",
"definitionReference": "ARTICLE/PRICE",
"type": "NUMBER",
"value": 2.89
},
{
"runtimeReference": "ARTICLE[2]/CATEGORY",
"definitionReference": "ARTICLE/CATEGORY",
"type": "STRING",
"value": "Canned"
},
{
"runtimeReference": "ARTICLE[2]/LABEL",
"definitionReference": "ARTICLE/LABEL",
"type": "STRING",
"value": "Ratatouille Cassegrain"
}
]
},
{
"runtimeReference": "ARTICLE[3]",
"definitionReference": "ARTICLE",
"type": "RECORD",
"value": "3083681085860",
"datasetCode": "ARTICLES",
"subVariables": [
{
"runtimeReference": "ARTICLE[3]/IMAGE",
"definitionReference": "ARTICLE/IMAGE",
"type": "STRING",
"value": "https://images.openfoodfacts.org/images/products/308/368/108/5860/front_fr.23.400.jpg"
},
{
"runtimeReference": "ARTICLE[3]/PRICE",
"definitionReference": "ARTICLE/PRICE",
"type": "NUMBER",
"value": 1.8
},
{
"runtimeReference": "ARTICLE[3]/CATEGORY",
"definitionReference": "ARTICLE/CATEGORY",
"type": "STRING",
"value": "Canned"
},
{
"runtimeReference": "ARTICLE[3]/LABEL",
"definitionReference": "ARTICLE/LABEL",
"type": "STRING",
"value": "Cooked chickpeas"
}
]
},
{
"runtimeReference": "ARTICLE[4]",
"definitionReference": "ARTICLE",
"type": "RECORD",
"value": "3700009252567",
"datasetCode": "ARTICLES",
"subVariables": [
{
"runtimeReference": "ARTICLE[4]/IMAGE",
"definitionReference": "ARTICLE/IMAGE",
"type": "STRING",
"value": "https://images.openfoodfacts.org/images/products/370/000/925/2567/front_fr.49.400.jpg"
},
{
"runtimeReference": "ARTICLE[4]/PRICE",
"definitionReference": "ARTICLE/PRICE",
"type": "NUMBER",
"value": 6.15
},
{
"runtimeReference": "ARTICLE[4]/CATEGORY",
"definitionReference": "ARTICLE/CATEGORY",
"type": "STRING",
"value": "Caterer"
},
{
"runtimeReference": "ARTICLE[4]/LABEL",
"definitionReference": "ARTICLE/LABEL",
"type": "STRING",
"value": "Salad Bowl Caesar Mix"
}
]
},
{
"runtimeReference": "DISCOUNTS",
"definitionReference": "DISCOUNTS",
"type": "COMPOSITE",
"value": [
{
"runtimeReference": "DISCOUNTS/NB_ARTICLES",
"definitionReference": "DISCOUNTS/NB_ARTICLES",
"type": "NUMBER",
"value": 0.1,
"properties": {
"DESC": "0,10€ discount for each set of 5 articles"
}
},
{
"runtimeReference": "DISCOUNTS/NUTELLA",
"definitionReference": "DISCOUNTS/NUTELLA",
"type": "NUMBER",
"value": 0.05,
"properties": {
"DESC": "0,05€ for each nutella jar"
}
},
{
"runtimeReference": "DISCOUNTS/TOTAL_AMOUNT",
"definitionReference": "DISCOUNTS/TOTAL_AMOUNT",
"type": "NUMBER",
"value": 0.5,
"properties": {
"DESC": "0,50€ discount if total amount (discounts deducted) is more than 10€"
}
},
{
"runtimeReference": "DISCOUNTS/SAME_CATEGORY",
"definitionReference": "DISCOUNTS/SAME_CATEGORY",
"type": "NUMBER",
"value": 0.03,
"properties": {
"DESC": "0,03€ discount for each set of 3 products of the same category"
}
}
]
}
]
}

Alors c'est sûr qu'une meilleure présentation que du JSON est possible ! (une IHM par exemple 😮, peut être même en Vue.js 😎). Si vous avez résisté au clic du lien au début de ce billet, vous avez maintenant mérité de le faire :

https://demo.ruleshake.com/discount