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 todo el control sobre el estado del carrito en tu side.

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

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
}

subtotal, total y price 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.

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'
},
},
})

cartSnapshot enviado al backend

Cuando cart.enabled = true y hay items, el widget incluye un snapshot en el body de POST /api/widget/search:

interface CartSnapshot {
currency: string
totalItems: number
subtotal: number // numérico (no formateado)
total: number
formattedSubtotal?: string
formattedTotal?: string
items: CartSnapshotItem[]
truncated: boolean
hash: string
updatedAt: string
shipping?: CartShippingInfo
}

Acciones del agente sobre el carrito

El backend puede pedir al widget que mute el carrito vía SearchResponse.cartAction:

cartAction.typeSignificadoConfirmación
ADD_SUGGESTIONAñadir un producto sugeridoEl widget muestra una card premium con confirmationPrompt y CTA.
REMOVEQuitar item por itemKeyConfirmación inline.
UPDATE_QUANTITYCambiar quantityConfirmación inline.
CLEARVaciar carritoConfirmación destructiva.
SET_DESTINATION_COUNTRYPersistir país (envíos)Silencioso, sin confirmación. Llama a onSetDestinationCountry.

El widget siempre confirma con el usuario antes de ejecutar ADD/REMOVE/UPDATE/CLEAR. Es el host quien lo hace efectivo.

Próximas lecturas