Saltar al contenido principal

Integración de carrito

El widget no toca el carrito del host directamente. En su lugar, define un contrato de callbacks que tu integración implementa contra el carrito real (WooCommerce, custom, headless). Esto mantiene el widget agnóstico de plataforma y deja todo el control sobre el estado del carrito en tu lado.

Cuando cart.enabled = true, el widget:

  1. Lee el carrito vía onGetCart() al montarse y tras cualquier neuroon:cart-update.
  2. Muta el carrito vía onAddToCart, onRemoveFromCart, onUpdateQuantity cuando el usuario interactúa con CTAs del widget.
  3. Envía un cartSnapshot con cada POST /api/widget/search, lo que activa la rama cart-aware del agente conversacional (cross-sell, recordar destinos, etc.).

Contrato CartConfig

interface CartConfig {
enabled: boolean
initialCount?: number
onGetCart: () => Promise<CartState>
onAddToCart: (productId: string, quantity: number, externalProductId?: string) => Promise<CartOperationResult>
onRemoveFromCart: (itemKey: string) => Promise<CartOperationResult>
onUpdateQuantity: (itemKey: string, quantity: number) => Promise<CartOperationResult>
onCheckout: () => void
canAddToCart?: (product: Product) => boolean
externalUpdateEvent?: string // default 'neuroon:cart-update'
onSetDestinationCountry?: (countryCode: string) => Promise<CartOperationResult | void>
}

Tipos completos en widget/src/types/cart.ts.

CartState

interface CartState {
items: CartItem[]
totalItems: number
subtotal: string // formateado por el host: "232,48 €"
total: string // formateado por el host
currency: string // ISO 4217: "EUR", "USD"
shipping?: CartShippingInfo
}

interface CartItem {
key: string // identificador único del item en el carrito
id: string // productId Neuroon
externalId?: string // host-platform id (e.g. WC post_id)
name: string
price: string // formateado por el host
quantity: number
image?: string
url?: string
variant?: string // "Talla: 42, Color: Negro"
maxQuantity?: number
}

interface CartShippingInfo {
currentAmount: number
thresholdMet: boolean
needsCountryResolution?: boolean
freeShippingThreshold?: number
delta?: number // max(0, threshold - currentAmount)
destinationCountry?: string // ISO-3166-1 alpha-2 (uppercase)
destinationLabel?: string // p.ej. "España"
formattedThreshold?: string // p.ej. "50,00 €"
formattedDelta?: string
availableCountries?: { code: string; label: string; threshold?: number }[]
}

subtotal, total, price y los formatted* de shipping son strings ya formateados por el host. Esto evita disputas de redondeo y currency formatting entre regiones.

CartOperationResult

type CartOperationResult =
| { success: true; cart: CartState; notice?: string }
| { success: false; cart?: CartState; error?: string }

Si tu callback devuelve success: true, el widget actualiza su estado interno con la cart retornada. Si devuelve success: false, el widget muestra el error en una toast y no muta su estado.

Puente neuroon:cart-update

El host debe emitir neuroon:cart-update después de cualquier mutación del carrito fuera del widget (mini-cart, página de carrito, otro plugin):

window.dispatchEvent(new CustomEvent('neuroon:cart-update'))

Al recibirlo, el widget:

  1. Verifica que no haya operaciones del propio widget en vuelo (evita pisar estado optimista).
  2. Aplica un debounce de 300 ms (algunos hosts emiten múltiples eventos seguidos: added_to_cart, wc_fragments_refreshed).
  3. Llama a cart.onGetCart() y actualiza su estado interno.

Lógica en widget/src/context/CartContext.tsx. El nombre del evento es configurable vía cart.externalUpdateEvent.

Ejemplo: WooCommerce con jQuery

<script>
(function() {
function notifyNeuroon() {
window.dispatchEvent(new CustomEvent('neuroon:cart-update'))
}
if (window.jQuery) {
jQuery(document.body).on(
'added_to_cart removed_from_cart wc_fragments_refreshed updated_cart_totals',
notifyNeuroon
)
}
})()
</script>

El plugin oficial de Neuroon para WordPress ya implementa este puente; sólo lo necesitas si tienes una integración custom.

Ejemplo: implementación de CartConfig

window.NeuroonWidget.init({
container: '#neuroon-search',
token: 'WIDGET_TOKEN',
cart: {
enabled: true,
initialCount: window.MyShop.cartCount, // del cookie/SSR
async onGetCart() {
const r = await fetch('/api/cart', { credentials: 'include' })
const data = await r.json()
return {
items: data.items.map(i => ({
key: i.line_item_key,
id: i.product_id,
externalId: String(i.post_id),
name: i.title,
price: i.formatted_price,
quantity: i.qty,
image: i.thumbnail,
url: i.permalink,
variant: i.variant_label,
maxQuantity: i.stock_qty,
})),
totalItems: data.total_items,
subtotal: data.formatted_subtotal,
total: data.formatted_total,
currency: data.currency,
}
},
async onAddToCart(productId, quantity, externalProductId) {
const r = await fetch('/api/cart/add', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
productId: externalProductId || productId,
quantity,
}),
})
if (!r.ok) return { success: false, error: 'No se pudo añadir' }
return { success: true, cart: await this.onGetCart() }
},
async onRemoveFromCart(itemKey) {
await fetch(`/api/cart/${itemKey}`, { method: 'DELETE', credentials: 'include' })
return { success: true, cart: await this.onGetCart() }
},
async onUpdateQuantity(itemKey, quantity) {
await fetch(`/api/cart/${itemKey}`, {
method: 'PATCH',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ quantity }),
})
return { success: true, cart: await this.onGetCart() }
},
onCheckout() {
window.location.href = '/checkout'
},
},
})

Lo que ocurre por debajo

Cuando cart.enabled = true, el widget envía automáticamente un snapshot del carrito al backend en cada búsqueda. Tú no construyes ese snapshot: sale del CartState que devuelves en cart.onGetCart(). El backend lo usa para personalizar resultados (cross-sell, recordar destinos de envío, etc.).

Si el asistente decide que el usuario podría querer añadir/quitar/actualizar un producto, el widget llama a tus callbacks (onAddToCart, onRemoveFromCart, onUpdateQuantity, onCheckout, onSetDestinationCountry?) — siempre tras confirmación visible al usuario para mutaciones destructivas. Tu único trabajo es implementar esos callbacks contra tu carrito real.

Próximas lecturas