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:
- Lee el carrito vía
onGetCartal montarse y tras cualquierneuroon:cart-update. - Muta el carrito vía
onAddToCart,onRemoveFromCart,onUpdateQuantitycuando el usuario interactúa con CTAs del widget. - Envía un
cartSnapshotcon cadaPOST /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:
- Verifica que no haya operaciones del propio widget en vuelo (evita pisar estado optimista).
- Aplica un debounce de 300 ms (algunos hosts emiten múltiples eventos seguidos:
added_to_cart,wc_fragments_refreshed). - Llama a
cart.onGetCarty 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.type | Significado | Confirmación |
|---|---|---|
ADD_SUGGESTION | Añadir un producto sugerido | El widget muestra una card premium con confirmationPrompt y CTA. |
REMOVE | Quitar item por itemKey | Confirmación inline. |
UPDATE_QUANTITY | Cambiar quantity | Confirmación inline. |
CLEAR | Vaciar carrito | Confirmación destructiva. |
SET_DESTINATION_COUNTRY | Persistir 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
- Configuración —
CartConfigen contexto. - Eventos del widget —
neuroon:cart-updatey otros. - Reference → Modelos de datos — schemas completos.