Guide technique

Commerce Multi-Canal : L'Architecture d'un Checkout Unifié à Travers Cinq Fournisseurs

Comment construire un checkout unifié à travers plusieurs fournisseurs. Synchronisation bidirectionnelle, proxy de disponibilité en temps réel, scoping des canaux, routage des commandes et gestion des erreurs quand les fournisseurs tombent en plein checkout.

16 mars 202614 min de lectureÉquipe d'Ingénierie Oronts

Multi-Canal, ce n'est pas "se connecter à Amazon"

La plupart des articles sur le commerce multi-canal décrivent comment connecter ton shop aux APIs de marketplaces. C'est de la syndication, pas de l'architecture. Le vrai commerce multi-canal, c'est : plusieurs fournisseurs avec des APIs différentes, des modèles de prix différents, des formats de disponibilité différents et des flux de réservation différents, le tout présenté au client comme un seul checkout fluide.

On a construit une plateforme d'agrégation multi-fournisseurs qui unifie le checkout à travers 5 APIs de fournisseurs. Un panier, cinq fournisseurs, une commande. Cet article couvre l'architecture qui rend tout ça possible. Pour l'évaluation plus large des plateformes commerce, consulte notre guide Vendure en production et notre guide des plateformes e-commerce.

Le Problème du Checkout Unifié

Chaque fournisseur a sa propre API, son propre format de données, son propre modèle de disponibilité et son propre flux de réservation :

AspectFournisseur AFournisseur BFournisseur C
Style d'APIREST v2GraphQLSOAP/XML
TarificationPar personne, dynamiquePrix fixe, paliersPar groupe, négocié
DisponibilitéAPI temps réelFichier de sync quotidienWebhook au changement
RéservationDeux étapes (réserver + confirmer)Une étape (réservation instantanée)Trois étapes (devis + réserver + confirmer)
AnnulationGratuite jusqu'à 24hNon remboursableRemboursement partiel
Format d'IDUUIDNumériquePréfixe alphanumérique

Un checkout unifié doit normaliser tout ça derrière une seule interface. Le client ajoute des articles de différents fournisseurs dans un seul panier et paie une seule fois. Le système route chaque article vers le bon fournisseur, gère le flux de réservation par fournisseur et présente une seule confirmation au client.

Le Pattern Supplier Adapter

Chaque fournisseur reçoit un adapter qui implémente une interface commune :

interface SupplierAdapter {
    search(query: SearchQuery): Promise<Product[]>;
    checkAvailability(productId: string, date: string, persons: PersonConfig[]): Promise<AvailabilityResult>;
    reserve(params: ReservationParams): Promise<Reservation>;
    confirm(reservationId: string, bookerInfo: BookerInfo): Promise<BookingConfirmation>;
    cancel(bookingId: string): Promise<CancellationResult>;
}

// Chaque fournisseur implémente l'interface différemment
class SupplierAAdapter implements SupplierAdapter {
    async checkAvailability(productId: string, date: string, persons: PersonConfig[]) {
        // Fournisseur A : appel API temps réel
        const response = await this.httpClient.get(`/v2/availability/${productId}`, {
            params: { date, adults: persons.filter(p => p.type === 'adult').length },
        });
        return this.normalizeAvailability(response.data);
    }
}

class SupplierBAdapter implements SupplierAdapter {
    async checkAvailability(productId: string, date: string, persons: PersonConfig[]) {
        // Fournisseur B : requête GraphQL
        const { data } = await this.graphqlClient.query({
            query: AVAILABILITY_QUERY,
            variables: { productId, date },
        });
        return this.normalizeAvailability(data.availability);
    }
}

L'adapter normalise la réponse de chaque fournisseur dans un format commun. Le service de checkout travaille avec le format commun, jamais avec les structures de données spécifiques aux fournisseurs.

Proxy de Disponibilité en Temps Réel

Certains fournisseurs proposent des prix en temps réel qui changent à la minute (tarification dynamique, inventaire limité). Les résultats de recherche montrent un prix en cache, mais au moment du checkout le système doit vérifier le prix actuel.

async function getAvailabilityWithFallback(
    productId: string, date: string, persons: PersonConfig[]
): Promise<AvailabilityResult> {
    const supplier = getSupplierForProduct(productId);

    if (supplier.supportsRealTimeAvailability) {
        try {
            // Prix live depuis l'API du fournisseur
            const live = await supplier.adapter.checkAvailability(productId, date, persons);
            await cache.set(`avail:${productId}:${date}`, live, { ttl: 300 });
            return live;
        } catch (error) {
            // API du fournisseur en panne : retourner le prix en cache avec avertissement
            const cached = await cache.get(`avail:${productId}:${date}`);
            if (cached) {
                return { ...cached, stale: true, warning: 'Price may have changed' };
            }
            throw new AvailabilityError('Cannot determine availability');
        }
    }

    // Fournisseur avec disponibilité en batch : retourner depuis l'index
    return searchIndex.getAvailability(productId, date);
}

Le pattern proxy : essayer en live, se rabattre sur le cache, se rabattre sur l'index. Toujours informer le client si le prix est potentiellement obsolète.

Synchronisation Bidirectionnelle : Empêcher les Boucles Infinies

Quand deux systèmes synchronisent des données dans les deux sens (système commerce et système opérations), chaque mise à jour du système A déclenche un sync vers le système B, qui déclenche un sync retour vers A.

// Le suivi de source empêche les boucles infinies
interface SyncMessage {
    entityId: string;
    entityType: string;
    data: any;
    source: string;           // "commerce" | "operations" | "import"
    correlationId: string;
}

async function handleSync(message: SyncMessage) {
    // Ignorer les messages qui viennent de ce système
    if (message.source === THIS_SYSTEM_ID) {
        return; // ACK et passer
    }

    // Traiter la synchronisation
    await updateEntity(message.entityId, message.data);

    // Publier la mise à jour avec NOTRE tag source
    await publishSync({
        ...message,
        source: THIS_SYSTEM_ID,  // Taguer pour que l'autre système l'ignore
    });
}

Chaque message porte un champ source. Quand un système reçoit un message de lui-même (via l'autre système), il l'ignore. La boucle est cassée dès le premier aller-retour.

Pour d'autres patterns event-driven incluant la déduplication et le dead letter handling, consulte notre guide d'architecture event-driven.

Channel Scoping : Visibilité vs Publication vs Disponibilité

Trois concepts qui doivent rester séparés :

ConceptQuestionExemple
VisibilitéQuels fournisseurs ce canal peut-il voir ?Le site allemand voit les fournisseurs A, B, C. L'API partenaire ne voit que le fournisseur A.
PublicationCe produit est-il publié et actif ?Le produit existe mais n'est pas publié (brouillon).
DisponibilitéCe produit peut-il être réservé maintenant ?Le produit est publié mais épuisé pour samedi.
// Le canal détermine la visibilité
const channel = await channelStore.get(tenantId, channelId);
const visibleSupplierIds = channel.supplierIds;

// Filtrer les produits par visibilité du canal
const products = await searchIndex.search({
    query: userQuery,
    filters: {
        supplier_id: { $in: visibleSupplierIds },  // Scoping du canal
        status: 'active',                           // Publication
    },
});

// La disponibilité est vérifiée au moment du checkout (concern séparé)

Un produit peut être visible (dans la liste fournisseurs du canal), publié (statut = actif), mais indisponible (épuisé). Chacun est un filtre différent à un stade différent du flux.

Routage des Commandes

Quand une commande contient des articles de plusieurs fournisseurs, chaque article doit être routé vers le bon fournisseur pour le fulfillment :

async function processMultiSupplierOrder(order: Order): Promise<OrderResult> {
    // Grouper les articles par fournisseur
    const itemsBySupplier = groupBy(order.items, item => item.supplierId);

    // Traiter les articles de chaque fournisseur indépendamment
    const results = await Promise.allSettled(
        Object.entries(itemsBySupplier).map(async ([supplierId, items]) => {
            const adapter = getAdapter(supplierId);

            // Réserver
            const reservation = await adapter.reserve({
                items,
                booker: order.bookerInfo,
                expiresIn: 900, // 15 min de maintien
            });

            // Confirmer
            return adapter.confirm(reservation.id, order.bookerInfo);
        })
    );

    // Gérer les résultats mixtes
    const successful = results.filter(r => r.status === 'fulfilled');
    const failed = results.filter(r => r.status === 'rejected');

    if (failed.length > 0 && successful.length > 0) {
        // Succès partiel : certains fournisseurs réservés, d'autres échoués
        // Annuler les réservations réussies ? Ou confirmer la commande partielle ?
        // C'est une décision business, pas technique.
        return handlePartialSuccess(order, successful, failed);
    }

    return { status: failed.length === 0 ? 'COMPLETED' : 'FAILED', results };
}

Le Problème du Succès Partiel

Que se passe-t-il quand le fournisseur A confirme mais le fournisseur B échoue ? Les options :

StratégieComportementIdéal pour
Tout ou rienAnnuler le fournisseur A si B échoueCommandes haute valeur, événements
Fulfillment partielConfirmer A, notifier le client pour BProduits interchangeables
Retry puis annulerRetenter B 3 fois, annuler A si B échoue définitivementÉquilibre UX et fiabilité

La bonne stratégie dépend du business. Pour des billets d'événements (non fongibles), tout ou rien est généralement correct. Pour des produits interchangeables, le fulfillment partiel est meilleur.

Gestion des Erreurs : Quand les Fournisseurs Tombent en Plein Checkout

Point de défaillanceCe qui s'est passéRéponse
Pendant la rechercheTimeout de l'API fournisseurAfficher les résultats des autres fournisseurs
Pendant la vérification de disponibilitéPrix obsolèteAfficher le prix en cache avec avertissement
Pendant la réservationStock épuiséRetirer l'article du panier, suggérer des alternatives
Pendant la confirmationPaiement refusé par le fournisseurAnnuler la réservation, retenter le paiement ou rembourser
Après la confirmationLe fournisseur envoie une annulationNotifier le client, proposer une nouvelle réservation ou un remboursement
Pendant l'annulationAPI du fournisseur en panneMettre l'annulation en file d'attente pour retry

Chaque erreur doit être gérée explicitement. "Une erreur est survenue" n'est jamais un message d'erreur acceptable pour une transaction commerce.

Pièges Courants

  1. Supposer que tous les fournisseurs ont le même flux de réservation. Les flux en deux étapes, une étape et trois étapes existent tous. L'adapter doit les normaliser.

  2. Ignorer les changements de prix entre la recherche et le checkout. La tarification dynamique signifie que le prix au checkout peut différer du prix de recherche. Valider au moment du checkout.

  3. Pas de suivi de source sur la synchronisation bidirectionnelle. Sans ça, chaque mise à jour boucle infiniment entre les systèmes.

  4. Traiter la visibilité des canaux comme un concern UI. Le scoping des canaux doit être appliqué au niveau de la couche query, pas juste côté frontend. Les consommateurs d'API voient les mêmes filtres.

  5. Pas de stratégie de défaillance partielle. Les commandes multi-fournisseurs vont partiellement échouer. Définis la politique business avant que ça arrive en production.

  6. Appels synchrones aux fournisseurs pendant le checkout. Si un fournisseur est lent, tout le checkout attend. Traite les fournisseurs en parallèle avec des timeouts individuels.

Points Clés

  • Les supplier adapters normalisent la diversité. Chaque fournisseur reçoit un adapter implémentant une interface commune. La logique business ne touche jamais les formats spécifiques aux fournisseurs.

  • La disponibilité en temps réel est un pattern proxy. Essayer en live, se rabattre sur le cache, se rabattre sur l'index. Toujours communiquer la fraîcheur des données au client.

  • Le suivi de source empêche les boucles de sync. Chaque message porte son origine. Les systèmes ignorent les messages venant d'eux-mêmes.

  • Visibilité, publication et disponibilité sont trois concerns séparés. Le canal détermine la visibilité. Le statut du produit détermine la publication. L'inventaire détermine la disponibilité. Ne les confonds pas.

  • La défaillance partielle est une décision business. Tout ou rien vs fulfillment partiel dépend du type de produit. Définis la politique avant de construire le code.

On construit des systèmes de commerce multi-canal dans le cadre de notre pratique e-commerce et logiciel sur mesure. Si tu as besoin d'aide avec l'architecture d'intégration fournisseurs, parle à notre équipe ou demande un devis.

Sujets couverts

commerce multi-canalarchitecture omnicanaleintégration marketplacesynchronisation commercecheckout unifiéagrégation fournisseurssynchronisation bidirectionnelle

Prêt à construire des systèmes IA prêts pour la production ?

Notre équipe est spécialisée dans les systèmes IA prêts pour la production. Discutons de comment nous pouvons aider.

Démarrer une conversation