La boutique vend du matériel d’électricien par lot, avec une règle maison : à partir de cinq unités d’une même référence, 10 % de remise s’applique automatiquement, sans coupon à saisir. La règle avait été testée, validée, mise en production. Trois semaines plus tard, un client signale qu’il a ajouté six fois la même vis dans son panier, une par une depuis la fiche produit, et que la remise n’apparaît jamais.
En reproduisant le scénario en environnement de recette, la remise s’applique parfaitement… quand le produit est ajouté une seule fois avec une quantité de six. Le problème n’était donc pas dans le calcul du pourcentage, mais dans la façon dont le panier de WooCommerce représente une même référence ajoutée à plusieurs reprises.
Symptôme : une remise qui dépend de la façon d’ajouter au panier
Le code de la règle, accroché sur woocommerce_before_calculate_totals, ressemblait à ceci :
add_action( 'woocommerce_before_calculate_totals', 'tp_remise_par_quantite' );
function tp_remise_par_quantite( $cart ) {
foreach ( $cart->get_cart() as $cart_item ) {
if ( $cart_item['quantity'] >= 5 ) {
$prix = $cart_item['data']->get_regular_price();
$cart_item['data']->set_price( $prix * 0.9 );
}
}
}
La condition $cart_item['quantity'] >= 5 ne teste que la quantité de la ligne de panier en cours, jamais le total cumulé pour un même produit réparti sur plusieurs lignes. Or WooCommerce crée une nouvelle entrée dans $cart->cart_contents à chaque appel à WC()->cart->add_to_cart() tant que les métadonnées associées diffèrent, ce qui arrive par exemple avec des champs personnalisés issus d’un plugin de personnalisation de produit, ou tout simplement quand JavaScript envoie deux requêtes AJAX très rapprochées côté fiche produit.
Diagnostic : agréger les lignes par identifiant produit
Un simple var_dump( $cart->get_cart() ) déposé temporairement dans le hook a confirmé l’hypothèse : deux clés de panier différentes, correspondant au même product_id, avec une quantité de trois chacune. Le total réel était bien de six, mais aucune des deux lignes ne dépassait individuellement le seuil de cinq.
La correction consiste à construire d’abord un total par produit, avant d’appliquer la remise ligne par ligne :
add_action( 'woocommerce_before_calculate_totals', 'tp_remise_par_quantite_v2', 20 );
function tp_remise_par_quantite_v2( $cart ) {
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
return;
}
$totaux_par_produit = array();
foreach ( $cart->get_cart() as $cart_item ) {
$id = $cart_item['product_id'];
$totaux_par_produit[ $id ] = ( $totaux_par_produit[ $id ] ?? 0 ) + $cart_item['quantity'];
}
foreach ( $cart->get_cart() as $cart_item ) {
$id = $cart_item['product_id'];
if ( $totaux_par_produit[ $id ] >= 5 ) {
$prix = $cart_item['data']->get_regular_price();
$cart_item['data']->set_price( $prix * 0.9 );
}
}
}

Correctif : fixer aussi la priorité et le garde-fou admin
Deux détails, invisibles au premier abord, ont aussi leur importance. D’abord la priorité 20 passée à add_action : sur ce projet, une extension de gestion de stock accrochait également woocommerce_before_calculate_totals à la priorité par défaut de 10, et modifiait l’ordre des lignes de panier après le calcul de la remise, ce qui provoquait un calcul sur un état de panier partiellement obsolète. Passer la règle maison après cette extension a stabilisé le résultat.
Ensuite, le garde-fou is_admin() && ! defined( 'DOING_AJAX' ) évite que la fonction s’exécute dans le contexte de l’administration, où get_regular_price() peut renvoyer une valeur différente selon le contexte de traduction ou de devise actif, faussant le calcul lors de la prévisualisation d’une commande créée manuellement par un opérateur du service client.
Prévention : ne jamais raisonner « ligne de panier » quand la règle porte sur le produit
Cette confusion entre ligne de panier et produit revient régulièrement dans les développements de règles de tarification maison. Quelques réflexes limitent le risque :
- Toujours agréger par
product_id(ouvariation_idpour une règle par variation) avant d’appliquer un seuil de quantité. - Tester systématiquement le scénario « ajout du même produit en plusieurs fois », pas seulement l’ajout en une fois avec une quantité modifiée sur la fiche produit.
- Vérifier la présence d’autres extensions accrochées au même hook via
global $wp_filter; print_r( $wp_filter['woocommerce_before_calculate_totals'] );, pour repérer un conflit de priorité avant qu’il ne devienne un ticket support.
Une règle de tarification qui fonctionne « en test » mérite toujours un second test avec le geste réel du client final, pas seulement le raccourci pratique du développeur qui règle la quantité en un clic.
En résumé
Le bug n’était ni dans le taux de remise ni dans le hook choisi, mais dans une hypothèse implicite sur la structure du panier. WooCommerce ne fusionne pas automatiquement les lignes identiques dès que la moindre métadonnée diffère, et une règle de tarification dynamique doit toujours raisonner au niveau du produit agrégé plutôt qu’au niveau de la ligne. Un correctif de quelques lignes a suffi une fois le vrai problème identifié, mais l’identifier a demandé de reproduire le geste exact du client plutôt que le raccourci du développeur pressé.