Un client exploite une marketplace de pièces détachées automobiles où un même panier peut contenir des références de trois fournisseurs différents, chacun expédiant depuis son propre entrepôt. Le problème n’était pas de choisir une extension multi-vendeurs du marché — ce choix avait déjà été fait en amont pour un autre besoin — mais de résoudre un problème plus précis : comment scinder une commande entre plusieurs fournisseurs sans forcer le client à payer plusieurs fois, et sans casser le tunnel de paiement au moment critique de la validation.
Le piège à éviter absolument
La première tentation, sur ce type de projet, consiste à créer les sous-commandes fournisseurs avant le paiement, chacune avec son propre appel à la passerelle. C’est une erreur qui multiplie les points d’échec : si un seul des paiements échoue, le client se retrouve avec un état incohérent, parfois facturé partiellement. La bonne approche consiste à ne jamais dupliquer l’appel de paiement : un seul paiement s’effectue sur la commande parente, et la scission en sous-commandes intervient après confirmation du paiement.
Étape 1 : laisser le tunnel de paiement inchangé

Le tunnel de paiement standard de WooCommerce traite la commande complète, avec la totalité du panier, exactement comme sur une boutique classique à un seul vendeur. Aucune modification n’est nécessaire à ce stade : la passerelle ne voit qu’une commande, un montant, un client.
Étape 2 : scinder après confirmation, jamais avant
Une fois le paiement confirmé, le hook woocommerce_payment_complete déclenche la création des sous-commandes fournisseurs, en regroupant les lignes d’articles par fournisseur :
add_action( 'woocommerce_payment_complete', 'marketplace_scinder_commande_par_fournisseur' );
function marketplace_scinder_commande_par_fournisseur( $order_id ) {
$order = wc_get_order( $order_id );
if ( $order->get_meta( '_deja_scindee' ) ) {
return;
}
$lignes_par_fournisseur = array();
foreach ( $order->get_items() as $item ) {
$product = $item->get_product();
$fournisseur = $product ? $product->get_meta( '_fournisseur_id' ) : '';
if ( ! $fournisseur ) {
continue;
}
$lignes_par_fournisseur[ $fournisseur ][] = $item;
}
foreach ( $lignes_par_fournisseur as $fournisseur_id => $items ) {
marketplace_creer_sous_commande( $order, $fournisseur_id, $items );
}
$order->update_meta_data( '_deja_scindee', 'oui' );
$order->save();
}
Étape 3 : créer la sous-commande, sans repaiement
La sous-commande fournisseur est créée avec wc_create_order(), liée à la commande parente par une méta-donnée, et directement placée au statut « en cours de préparation » puisque le paiement global a déjà été validé :
function marketplace_creer_sous_commande( $order_parente, $fournisseur_id, $items ) {
$sous_commande = wc_create_order( array(
'customer_id' => $order_parente->get_customer_id(),
) );
foreach ( $items as $item ) {
$sous_commande->add_item( clone $item );
}
$sous_commande->set_address( $order_parente->get_address( 'shipping' ), 'shipping' );
$sous_commande->update_meta_data( '_commande_parente_id', $order_parente->get_id() );
$sous_commande->update_meta_data( '_fournisseur_id', $fournisseur_id );
$sous_commande->calculate_totals( false );
$sous_commande->set_status( 'processing' );
$sous_commande->save();
return $sous_commande;
}
Le paramètre false passé à calculate_totals() évite de recalculer les frais de port et les taxes sur la sous-commande, déjà réglés au niveau de la commande parente : la sous-commande sert avant tout d’unité de suivi de préparation et d’expédition pour le fournisseur, pas d’un second document de facturation client.
Étape 4 : répartir le montant, sans nouveau paiement
La part de chaque fournisseur se calcule à partir du montant réellement encaissé sur la commande parente, jamais en déclenchant un nouveau prélèvement. Cette répartition alimente ensuite le module de reversement, qui vire à chaque fournisseur sa part après déduction de la commission de la marketplace :
function marketplace_calculer_part_fournisseur( $sous_commande, $order_parente ) {
$montant_total_parent = (float) $order_parente->get_total();
$montant_sous_commande = (float) $sous_commande->get_total();
$commission = 0.12;
return round( $montant_sous_commande * ( 1 - $commission ), 2 );
}
Ce qui s’est cassé la première fois
- Un premier essai déclenchait la scission sur
woocommerce_order_status_changed, qui se déclenche parfois plusieurs fois pour une même commande : la protection par méta-donnée_deja_scindeea été ajoutée après ce constat. - Le clonage naïf des objets
WC_Order_Itemconservait initialement l’identifiant original, ce qui provoquait des conflits d’affichage entre commande parente et sous-commande dans certains rapports. - Le remboursement partiel d’une sous-commande fournisseur ne se répercutait pas automatiquement sur la commande parente, un cas qui a nécessité un hook additionnel dédié.
Sur ce type de marketplace, la règle qui a le mieux résisté à l’usage est simple à énoncer mais facile à oublier sous pression de planning : la commande parente reste la seule source de vérité sur le paiement, les sous-commandes ne sont que des vues de travail dérivées.
En résumé
Scinder une commande entre plusieurs fournisseurs sans casser le paiement repose sur un principe simple mais non négociable : un seul paiement, déclenché une seule fois, et une scission qui n’intervient qu’après confirmation, jamais avant. Cette contrainte, qui peut sembler restrictive au départ, évite en réalité la quasi-totalité des incidents rencontrés sur ce type de projet marketplace multi-fournisseurs.