Guía técnica

Comercio Multi-Canal: La Arquitectura de un Checkout Unificado a Través de Cinco Proveedores

Cómo construir un checkout unificado a través de múltiples proveedores. Sincronización bidireccional, proxy de disponibilidad en tiempo real, scoping de canales, enrutamiento de pedidos y manejo de errores cuando los proveedores fallan en pleno checkout.

16 de marzo de 202614 min de lecturaEquipo de Ingeniería Oronts

Multi-Canal no es "Conectarse a Amazon"

La mayoría de los artículos sobre comercio multi-canal describen cómo conectar tu tienda a las APIs de marketplaces. Eso es syndication, no arquitectura. El verdadero comercio multi-canal significa: múltiples proveedores con APIs diferentes, modelos de precios diferentes, formatos de disponibilidad diferentes y flujos de reserva diferentes, todo presentado al cliente como un solo checkout fluido.

Construimos una plataforma de agregación multi-proveedor que unifica el checkout a través de 5 APIs de proveedores. Un carrito, cinco proveedores, un pedido. Este artículo cubre la arquitectura que lo hace posible. Para la evaluación más amplia de plataformas de comercio, revisa nuestra guía de Vendure en producción y la guía de plataformas e-commerce.

El Problema del Checkout Unificado

Cada proveedor tiene su propia API, su propio formato de datos, su propio modelo de disponibilidad y su propio flujo de reserva:

AspectoProveedor AProveedor BProveedor C
Estilo de APIREST v2GraphQLSOAP/XML
PreciosPor persona, dinámicoPrecio fijo, nivelesPor grupo, negociado
DisponibilidadAPI en tiempo realArchivo de sync diarioWebhook al cambiar
ReservaDos pasos (reservar + confirmar)Un paso (reserva instantánea)Tres pasos (cotización + reservar + confirmar)
CancelaciónGratis hasta 24hNo reembolsableReembolso parcial
Formato de IDUUIDNuméricoPrefijo alfanumérico

Un checkout unificado debe normalizar todo esto detrás de una sola interfaz. El cliente agrega artículos de diferentes proveedores a un solo carrito y paga una sola vez. El sistema enruta cada artículo al proveedor correcto, maneja el flujo de reserva por proveedor y presenta una sola confirmación al cliente.

El Patrón Supplier Adapter

Cada proveedor recibe un adapter que implementa una interfaz común:

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

// Cada proveedor implementa la interfaz de forma diferente
class SupplierAAdapter implements SupplierAdapter {
    async checkAvailability(productId: string, date: string, persons: PersonConfig[]) {
        // Proveedor A: llamada API en tiempo real
        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[]) {
        // Proveedor B: consulta GraphQL
        const { data } = await this.graphqlClient.query({
            query: AVAILABILITY_QUERY,
            variables: { productId, date },
        });
        return this.normalizeAvailability(data.availability);
    }
}

El adapter normaliza la respuesta de cada proveedor a un formato común. El servicio de checkout trabaja con el formato común, nunca con estructuras de datos específicas del proveedor.

Proxy de Disponibilidad en Tiempo Real

Algunos proveedores ofrecen precios en tiempo real que cambian por minuto (precios dinámicos, inventario limitado). Los resultados de búsqueda muestran un precio en caché, pero al momento del checkout el sistema debe verificar el precio actual.

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

    if (supplier.supportsRealTimeAvailability) {
        try {
            // Precio en vivo desde la API del proveedor
            const live = await supplier.adapter.checkAvailability(productId, date, persons);
            await cache.set(`avail:${productId}:${date}`, live, { ttl: 300 });
            return live;
        } catch (error) {
            // API del proveedor caída: devolver precio en caché con advertencia
            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');
        }
    }

    // Proveedor con disponibilidad en batch: devolver desde el índice
    return searchIndex.getAvailability(productId, date);
}

El patrón proxy: intentar en vivo, caer al caché, caer al índice. Siempre informar al cliente si el precio podría estar desactualizado.

Sincronización Bidireccional: Prevenir Bucles Infinitos

Cuando dos sistemas sincronizan datos bidireccionalmente (sistema de comercio y sistema de operaciones), cada actualización del sistema A dispara un sync al sistema B, que a su vez dispara un sync de vuelta a A.

// El tracking de origen previene bucles infinitos
interface SyncMessage {
    entityId: string;
    entityType: string;
    data: any;
    source: string;           // "commerce" | "operations" | "import"
    correlationId: string;
}

async function handleSync(message: SyncMessage) {
    // Ignorar mensajes que se originaron en este sistema
    if (message.source === THIS_SYSTEM_ID) {
        return; // ACK y saltar
    }

    // Procesar la sincronización
    await updateEntity(message.entityId, message.data);

    // Publicar actualización con NUESTRO tag de origen
    await publishSync({
        ...message,
        source: THIS_SYSTEM_ID,  // Etiquetar para que el otro sistema lo ignore
    });
}

Cada mensaje lleva un campo source. Cuando un sistema recibe un mensaje de sí mismo (a través del otro sistema), lo ignora. El bucle se rompe en el primer viaje de ida y vuelta.

Para más patrones event-driven incluyendo deduplicación y dead letter handling, revisa nuestra guía de arquitectura event-driven.

Channel Scoping: Visibilidad vs Publicación vs Disponibilidad

Tres conceptos que deben mantenerse separados:

ConceptoPreguntaEjemplo
VisibilidadQué proveedores puede ver este canal?El sitio alemán ve proveedores A, B, C. La API de socios solo ve proveedor A.
PublicaciónEste producto está publicado y activo?El producto existe pero no está publicado (borrador).
DisponibilidadSe puede reservar este producto ahora?El producto está publicado pero agotado para el sábado.
// El canal determina la visibilidad
const channel = await channelStore.get(tenantId, channelId);
const visibleSupplierIds = channel.supplierIds;

// Filtrar productos por visibilidad del canal
const products = await searchIndex.search({
    query: userQuery,
    filters: {
        supplier_id: { $in: visibleSupplierIds },  // Scoping del canal
        status: 'active',                           // Publicación
    },
});

// La disponibilidad se verifica al momento del checkout (concern separado)

Un producto puede ser visible (en la lista de proveedores del canal), publicado (estado = activo), pero no disponible (agotado). Cada uno es un filtro diferente en una etapa diferente del flujo.

Enrutamiento de Pedidos

Cuando un pedido contiene artículos de múltiples proveedores, cada artículo debe enrutarse al proveedor correcto para el fulfillment:

async function processMultiSupplierOrder(order: Order): Promise<OrderResult> {
    // Agrupar artículos por proveedor
    const itemsBySupplier = groupBy(order.items, item => item.supplierId);

    // Procesar los artículos de cada proveedor independientemente
    const results = await Promise.allSettled(
        Object.entries(itemsBySupplier).map(async ([supplierId, items]) => {
            const adapter = getAdapter(supplierId);

            // Reservar
            const reservation = await adapter.reserve({
                items,
                booker: order.bookerInfo,
                expiresIn: 900, // 15 min de retención
            });

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

    // Manejar resultados mixtos
    const successful = results.filter(r => r.status === 'fulfilled');
    const failed = results.filter(r => r.status === 'rejected');

    if (failed.length > 0 && successful.length > 0) {
        // Éxito parcial: algunos proveedores reservados, otros fallaron
        // Cancelar las reservas exitosas? O confirmar el pedido parcial?
        // Esta es una decisión de negocio, no técnica.
        return handlePartialSuccess(order, successful, failed);
    }

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

El Problema del Éxito Parcial

Qué pasa cuando el proveedor A confirma pero el proveedor B falla? Las opciones:

EstrategiaComportamientoIdeal para
Todo o nadaCancelar proveedor A si B fallaPedidos de alto valor, eventos
Fulfillment parcialConfirmar A, notificar al cliente sobre BProductos intercambiables
Reintentar y luego cancelarReintentar B 3 veces, cancelar A si B falla definitivamenteBalance entre UX y confiabilidad

La estrategia correcta depende del negocio. Para boletos de eventos (no fungibles), todo o nada es generalmente lo correcto. Para productos intercambiables, el fulfillment parcial es mejor.

Manejo de Errores: Cuando los Proveedores Fallan en Pleno Checkout

Punto de fallaQué pasóRespuesta
Durante la búsquedaTimeout de API del proveedorMostrar resultados de otros proveedores
Durante verificación de disponibilidadPrecio desactualizadoMostrar precio en caché con advertencia
Durante la reservaInventario agotadoEliminar artículo del carrito, sugerir alternativas
Durante la confirmaciónPago rechazado por el proveedorCancelar reserva, reintentar pago o reembolsar
Después de la confirmaciónEl proveedor envía cancelaciónNotificar al cliente, ofrecer re-reserva o reembolso
Durante la cancelaciónAPI del proveedor caídaPoner cancelación en cola para reintento

Cada error debe manejarse explícitamente. "Algo salió mal" nunca es un mensaje de error aceptable para una transacción de comercio.

Errores Comunes

  1. Asumir que todos los proveedores tienen el mismo flujo de reserva. Flujos de dos pasos, un paso y tres pasos, todos existen. El adapter debe normalizarlos.

  2. Ignorar los cambios de precio entre búsqueda y checkout. Los precios dinámicos significan que el precio en checkout puede diferir del precio de búsqueda. Valida al momento del checkout.

  3. Sin tracking de origen en la sincronización bidireccional. Sin eso, cada actualización genera un bucle infinito entre sistemas.

  4. Tratar la visibilidad de canales como un concern de UI. El scoping de canales debe aplicarse en la capa de query, no solo en el frontend. Los consumidores de API ven los mismos filtros.

  5. Sin estrategia de falla parcial. Los pedidos multi-proveedor van a fallar parcialmente. Define la política de negocio antes de que ocurra en producción.

  6. Llamadas sincrónicas a proveedores durante el checkout. Si un proveedor es lento, todo el checkout espera. Procesa los proveedores en paralelo con timeouts individuales.

Puntos Clave

  • Los supplier adapters normalizan la diversidad. Cada proveedor recibe un adapter que implementa una interfaz común. La lógica de negocio nunca toca formatos específicos del proveedor.

  • La disponibilidad en tiempo real es un patrón proxy. Intentar en vivo, caer al caché, caer al índice. Siempre comunicar la frescura de los datos al cliente.

  • El tracking de origen previene bucles de sync. Cada mensaje lleva su origen. Los sistemas ignoran mensajes de sí mismos.

  • Visibilidad, publicación y disponibilidad son tres concerns separados. El canal determina la visibilidad. El estado del producto determina la publicación. El inventario determina la disponibilidad. No los mezcles.

  • La falla parcial es una decisión de negocio. Todo o nada vs fulfillment parcial depende del tipo de producto. Define la política antes de construir el código.

Construimos sistemas de comercio multi-canal como parte de nuestra práctica de e-commerce y software a medida. Si necesitas ayuda con la arquitectura de integración de proveedores, habla con nuestro equipo o solicita una cotización.

Temas cubiertos

comercio multi-canalarquitectura omnicanalintegración de marketplacesincronización de comerciocheckout unificadoagregación de proveedoressincronización bidireccional

¿Listo para construir sistemas de IA listos para producción?

Nuestro equipo se especializa en sistemas de IA listos para producción. Hablemos de cómo podemos ayudar.

Iniciar una conversación