Cada post anterior de esta serie daba por sentado algo que ya no puedo dar por sentado: que controlamos todo el sistema. El post sobre el motor de reservas trataba de reservar una plaza sin competir contigo mismo. El post sobre disponibilidad en tiempo real trataba de mantener los números consistentes en cada pantalla que un cliente pudiera estar mirando. El post sobre multi-tenancy trataba de aislar escuelas que comparten infraestructura. Todo eso vive dentro de los límites propios de Lueira — nuestra base de datos, nuestra API, nuestras reglas.
Los gestores de canal no tienen ese lujo. Lueira permite que una escuela conecte sus actividades a marketplaces de actividades de terceros "de forma rápida y eficiente" — como dice literalmente la descripción de la función en la web — de modo que el inventario de una escuela de esquí aparezca a la venta en una plataforma externa, no solo en su propio storefront de Lueira. Esa frase es sencilla. Lo que implica por debajo, no lo es.
1. Lo que promete de verdad un gestor de canal
El discurso hacia una escuela es directo: conecta una vez, vende en todas partes. Tus cinco plazas de clase de esquí del martes por la mañana son las mismas cinco plazas, ya reserve un cliente a través de tu propia web o a través de un marketplace que agrega actividades de una docena de operadores del valle. Para que esa promesa se sostenga, Lueira tiene que hablar con la API de cada marketplace, traducir nuestro modelo interno de actividades, plazas y precios a la forma que espere ese marketplace, y mantener a ambos lados honestos sobre cuántas plazas quedan realmente. Ese es el 20% fácil de la funcionalidad — un adaptador por marketplace. El 80% difícil es lo que pasa después de la primera sincronización, en cada cambio a partir de entonces.
2. Disponibilidad que no controlas
El problema de disponibilidad en tiempo real que resolvimos antes daba por hecho una única fuente de verdad: nuestra propia base de datos, nuestras propias escrituras, nuestros propios locks. Un gestor de canal rompe esa suposición a propósito. Ahora, las fuentes de verdad que importan para la reserva de un cliente incluyen sistemas que no gestionamos en absoluto.
Cada marketplace es distinto. Algunos envían webhooks en el instante en que algo cambia en su lado; otros esperan que hagamos polling, limitado por sus rate limits. Unos actualizan casi al instante; otros encolan cambios durante picos de carga. Y cada uno falla de forma distinta: una API que sencillamente está caída durante una hora, una suscripción de webhook que deja de entregar en silencio, un marketplace que confirma una petición y luego nunca llega a aplicarla.
3. Gestionar el doble booking que sigue ocurriendo
Una reserva hecha en un marketplace externo tiene que reducir la disponibilidad en todos los demás sitios — el propio storefront de Lueira y cualquier otro marketplace conectado — prácticamente al instante. No podemos simplemente consultar nuestro propio estado en vivo, porque el estado que acaba de cambiar vive en los servidores de otra persona. Hay una ventana en la que nuestros números están desactualizados en todas partes excepto en el marketplace que acaba de vender la plaza.
func handleMarketplaceBooking(w http.ResponseWriter, r *http.Request) {
var evt MarketplaceBookingEvent
if err := json.NewDecoder(r.Body).Decode(&evt); err != nil {
http.Error(w, "bad payload", http.StatusBadRequest)
return
}
// Toda reserva de marketplace lleva un ID externo. Esa es nuestra
// clave de dedupe — los webhooks pueden llegar duplicados, desordenados,
// o después de que ya hayamos recogido la misma reserva vía polling.
if existing, _ := store.FindByExternalRef(evt.MarketplaceID, evt.ExternalBookingID); existing != nil {
w.WriteHeader(http.StatusOK) // ya procesado, confirmamos y seguimos
return
}
slot, err := availability.Reserve(evt.ActivityID, evt.SlotID, evt.Spots)
if err != nil {
// Alguien más ya se quedó con la plaza. Este es el flujo de disculpa.
notifyOversell(evt)
w.WriteHeader(http.StatusConflict)
return
}
store.RecordExternalBooking(evt.MarketplaceID, evt.ExternalBookingID, slot)
channels.PushAvailabilityUpdate(evt.ActivityID, slot) // propaga a cada otro canal
w.WriteHeader(http.StatusOK)
}
Cuando esa rama de conflicto salta de verdad, el cliente de alguien recibe un mensaje de disculpa, un reembolso y, si es posible, una plaza alternativa. No hemos encontrado la forma de hacer que esa rama sea imposible — solo formas de hacerla rara y de gestionarla con elegancia en lugar de en silencio.
4. Mantener la fuente de verdad en un solo sitio
La decisión de diseño que evita que esto se convierta en un caos es negarse a tratar los marketplaces externos como igualmente autoritativos. El propio motor de disponibilidad de Lueira sigue siendo la única fuente de verdad. Cada integración de canal es una capa de sincronización sobre él, no un igual.
Sobre eso necesitábamos además una capa de reconciliación, porque los webhooks no son un mecanismo de entrega fiable — llegan duplicados, desordenados o no llegan nunca. Cada notificación de reserva entrante necesita un ID externo estable con el que hacer dedupe, más trabajos de reconciliación periódicos que comparen nuestra visión con la del marketplace.
Hemos sido honestos con nosotros mismos en que esto no cierra la brecha del todo. Un doble booking sigue ocurriendo de vez en cuando. Lo que construimos no es una garantía de que no pueda pasar — es una política de qué pasa cuando pasa. Los gestores de canal son, debajo del lenguaje de marketing, un problema de sistemas distribuidos disfrazado de industria de viajes.
