# Des remises qui ne s’appliquent pas : déboguer la tarification dynamique

> Une remise « à partir de 5 unités » fonctionne en test puis disparaît en production. Le coupable n'est pas le code de la règle, mais la façon dont WooCommerce agrège le panier.

- Auteur : Clément Hadrot
- Publié le : 2022-05-11
- Mis à jour le : 2022-05-11
- Catégorie : E-commerce
- URL : https://wpmoderne.dev.wordpress-developpement.fr/ecommerce/debogueur-tarification-dynamique-remises-woocommerce/

## L’essentiel

- Le panier fragmente les lignes ajoutées séparément
- woocommerce_before_calculate_totals ne s'exécute pas une seule fois
- Toujours agréger par ID produit, pas par ligne de panier

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 );
        }
    }
}
```

> L'essentiel à retenir : Le panier fragmente les lignes ajoutées séparément ; woocommerce_before_calculate_totals ne s'exécute pas une seule fois ; Toujours agréger par ID produit, pas par ligne de panier

## 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` (ou `variation_id` pour 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é.
