# Architecture d’une synchronisation ERP-WooCommerce en file d’attente asynchrone

> Connecter WooCommerce à un ERP en appel direct tient rarement la charge. Voici comment structurer une file d'attente asynchrone qui absorbe les pannes du système tiers.

- Auteur : Clément Hadrot
- Publié le : 2022-06-23
- Mis à jour le : 2022-06-23
- Catégorie : E-commerce
- URL : https://wpmoderne.dev.wordpress-developpement.fr/ecommerce/architecture-synchronisation-erp-woocommerce-asynchrone/

## L’essentiel

- Une file découplée par Action Scheduler, jamais d'appel bloquant
- Une clé d'idempotence par événement pour éviter les doublons
- Une table de rejeu pour les échecs définitifs

Le client gère un réseau de douze magasins, avec un ERP central qui pilote les stocks, les tarifs et la facturation. La direction voulait que chaque commande WooCommerce remonte instantanément dans l'ERP, et que chaque mouvement de stock décidé côté ERP redescende sur la boutique en ligne. Le premier prototype, livré par une autre agence, appelait l'API de l'ERP directement depuis le hook `woocommerce_checkout_order_processed`. Résultat : dès que l'ERP ralentissait, ce qui arrivait chaque lundi matin lors de la synchronisation comptable, les clients voyaient leur tunnel de commande se bloquer plusieurs secondes, parfois expirer.

Ce constat a motivé une reprise complète de l'architecture, avec un principe simple posé dès le départ : aucun appel réseau vers l'ERP ne doit jamais bloquer une requête HTTP destinée à un visiteur. Tout passe par une file d'attente asynchrone, avec des tentatives, des délais et un chemin de secours pour les cas qui échouent malgré tout.

## Pourquoi l'appel direct ne tient pas la charge

Un appel synchrone à un système tiers introduit trois risques cumulés : la latence du système distant devient la latence perçue par le client final, une panne de l'ERP devient une panne de la boutique, et un timeout mal configuré peut créer une commande WooCommerce sans confirmation ERP, ou l'inverse. Dans un contexte de douze points de vente avec une volumétrie de plusieurs centaines de commandes par jour, ce couplage fort n'était pas soutenable dès le premier pic saisonnier.

## Vue d'ensemble de l'architecture retenue

La solution s'appuie sur Action Scheduler, la bibliothèque de tâches asynchrones déjà intégrée à WooCommerce depuis la version 3.0, plutôt que sur une file externe type RabbitMQ, jugée disproportionnée pour ce volume et plus coûteuse à maintenir pour une équipe habituée à l'écosystème WordPress.

```
Commande WooCommerce validée
        │
        ▼
  Hook woocommerce_order_status_processing
        │
        ▼
  Création d'un événement en file
  (as_schedule_single_action)
        │
        ▼
  Worker Action Scheduler
        │
        ├── Succès ──────────────► Commande marquée « synchronisée ERP »
        │
        └── Échec (timeout, 5xx)
                │
                ▼
        Nouvelle tentative (backoff exponentiel)
                │
                ▼
        Après 3 échecs → table de rejeu manuelle
                │
                ▼
        Alerte Slack à l'équipe support
```

> L'essentiel à retenir : Une file découplée par Action Scheduler, jamais d'appel bloquant ; Une clé d'idempotence par événement pour éviter les doublons ; Une table de rejeu pour les échecs définitifs

## Le rôle de la clé d'idempotence

Le point le plus délicat n'était pas de programmer la tâche, mais d'éviter qu'une commande ne soit envoyée deux fois à l'ERP en cas de rejeu après un timeout ambigu, où l'ERP a peut-être bien reçu la commande sans que WooCommerce reçoive la réponse. Chaque événement porte donc une clé d'idempotence, construite à partir de l'identifiant de commande et d'un horodatage de tentative, transmise à l'ERP dans un en-tête dédié :

```
function erp_planifier_synchronisation( $order_id ) {
    $cle = 'wc-order-' . $order_id . '-v1';

    if ( get_post_meta( $order_id, '_erp_sync_queued', true ) ) {
        return;
    }

    as_schedule_single_action(
        time(),
        'erp_synchroniser_commande',
        array( 'order_id' => $order_id, 'idempotency_key' => $cle ),
        'erp-sync'
    );

    update_post_meta( $order_id, '_erp_sync_queued', $cle );
}
add_action( 'woocommerce_order_status_processing', 'erp_planifier_synchronisation' );
```

L'ERP, côté serveur, rejette toute requête portant une clé d'idempotence déjà traitée avec succès, ce qui rend les rejeux sans risque même en cas de doute sur l'issue d'une tentative précédente.

## Gérer les échecs sans perdre d'événement

Trois tentatives espacées par un délai croissant (une minute, puis quinze minutes, puis deux heures) couvrent la grande majorité des pannes transitoires de l'ERP. Au-delà, l'événement est déplacé vers une table dédiée aux échecs définitifs, consultée quotidiennement par l'équipe support, avec une alerte automatique envoyée sur un canal Slack interne dès qu'un événement y atterrit :

- Un identifiant de commande, la clé d'idempotence et le dernier message d'erreur reçu de l'ERP.
- Un bouton de rejeu manuel depuis l'administration, qui reprogramme l'événement dans Action Scheduler.
- Un compteur de tentatives conservé, pour distinguer un incident isolé d'une panne prolongée de l'ERP.

> Une file d'attente sans chemin d'échec visible n'est qu'un problème différé : le jour où l'ERP tombe une demi-journée, il faut pouvoir dire exactement quelles commandes attendent encore, pas les redécouvrir une semaine plus tard.

## Bilan après un an de production

Depuis la mise en place de cette architecture, aucun incident côté ERP n'a plus jamais ralenti le tunnel de commande, et le taux de synchronisation en première tentative dépasse 96 % sur les douze derniers mois. Le principal apprentissage tient en une phrase : découpler ne suffit pas si la file n'a pas de sortie de secours clairement instrumentée ; c'est cette table de rejeu, presque anecdotique dans le schéma initial, qui a fini par être la pièce la plus consultée par l'équipe support au quotidien.
