Un client ajoute un article éligible à une remise fidélité de dix pour cent, et découvre au moment du paiement que sa remise atteint en réalité vingt pour cent. Ni une erreur de configuration, ni un bug d’affichage : le hook woocommerce_before_calculate_totals s’est exécuté deux fois pour un seul recalcul de panier, et la fonction personnalisée qui applique la remise n’avait aucune protection contre cette répétition.
Ce piège touche particulièrement les développeurs qui découvrent WooCommerce sans avoir lu attentivement la documentation de ce hook précis, pourtant l’un des plus utilisés pour personnaliser le prix des articles du panier avant calcul des totaux.
Un hook déclenché plus souvent qu’on ne l’imagine
woocommerce_before_calculate_totals se déclenche à chaque appel de WC_Cart::calculate_totals(), et ce calcul, comme évoqué ailleurs, peut lui-même être invoqué plusieurs fois au cours d’une même page : une fois lors du chargement initial du panier, une fois lors d’une mise à jour AJAX du mini-panier, une fois encore lors du rafraîchissement des fragments de page. Rien dans WooCommerce ne garantit qu’un code accroché à ce hook ne s’exécute qu’une seule fois par requête utilisateur.
Une fonction qui modifie le prix d’un article de panier sans vérifier si cette modification a déjà eu lieu applique donc sa logique à chaque déclenchement du hook, sur un prix potentiellement déjà modifié par le passage précédent, d’où l’effet de cumul observé par le client.
Le code fautif, en apparence anodin
add_action( 'woocommerce_before_calculate_totals', function( $cart ) {
foreach ( $cart->get_cart() as $item ) {
if ( in_array( 'fidelite', $item['product']->get_tag_ids(), true ) ) {
$prix_actuel = $item['data']->get_price();
$item['data']->set_price( $prix_actuel * 0.9 );
}
}
});
À première lecture, ce code semble correct : il applique dix pour cent de remise sur chaque article portant l’étiquette « fidélité ». Le problème apparaît dès que le hook se déclenche une seconde fois dans la même requête : le prix déjà réduit de dix pour cent se voit réduit une nouvelle fois du même pourcentage, ce qui ne donne pas vingt pour cent exactement, mais un cumul multiplicatif tout aussi problématique et invisible à l’œil nu sur la fiche produit.

Pourquoi l’erreur passe inaperçue en test rapide
En test manuel superficiel, un développeur qui recharge une seule fois la page du panier ne voit qu’un seul déclenchement du hook et valide la remise comme correcte. C’est seulement en observant le comportement après une mise à jour de quantité en AJAX, qui redéclenche le calcul sans recharger la page complète, que le doublon devient visible dans le total affiché.
Le correctif : un indicateur qui garantit un seul passage
La solution consiste à retenir, pour chaque article du panier identifié par sa clé, si la remise a déjà été appliquée durant le cycle de calcul en cours, avant de modifier à nouveau son prix. Un indicateur statique par requête, ou une vérification basée sur le prix d’origine plutôt que sur le prix courant, résout le problème.
add_action( 'woocommerce_before_calculate_totals', function( $cart ) {
foreach ( $cart->get_cart() as $item ) {
if ( in_array( 'fidelite', $item['product']->get_tag_ids(), true ) ) {
$prix_original = $item['data']->get_regular_price();
$item['data']->set_price( $prix_original * 0.9 );
}
}
}, 20 );
Ici, la remise s’appuie systématiquement sur get_regular_price(), un prix de référence stable, plutôt que sur get_price(), susceptible d’avoir déjà été modifié par un passage précédent du même hook. Peu importe le nombre de fois où le hook se déclenche dans la requête : le résultat final reste identique, calculé depuis la même base à chaque fois.
- Ne jamais baser une remise sur le prix courant potentiellement déjà modifié
- Toujours recalculer depuis une valeur de référence stable comme le prix normal du produit
- Tester le comportement après une mise à jour AJAX du panier, pas seulement au chargement initial
Un hook qui modifie un prix doit toujours partir d’une valeur de référence fixe : sans cela, chaque exécution supplémentaire du hook devient une exécution supplémentaire de la remise.
Prévention pour la suite
Toute fonction accrochée à ce hook mérite d’être relue avec cette question précise en tête : que se passe-t-il si cette fonction s’exécute deux, trois ou quatre fois dans la même requête ? Une fonction idempotente, dont le résultat final ne dépend pas du nombre d’exécutions, élimine ce type d’incident avant même qu’il n’atteigne la production.
Ce que cet article ne couvre pas
La création de règles de tarification dynamique plus élaborées, avec des paliers selon la quantité ou des remises croisées entre catégories de produits, dépasse le cadre de cet article, centré sur le piège précis du cumul involontaire d’une remise simple.
Notre verdict
Ce genre de bug est redoutable précisément parce qu’il ne provoque aucune erreur PHP, aucun message dans les journaux, rien qu’un chiffre légèrement faux que seul un client attentif finit par remarquer. La discipline à adopter est simple : partir toujours d’un prix de référence stable dans toute fonction accrochée à ce hook, sans exception.