> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aftersell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Upsells de Upcart

> Usa una Strategy para elegir dinámicamente los productos mostrados en el módulo de upsells de Upcart.

<div id="overview">
  ## Descripción general
</div>

Upcart es una aplicación independiente de Aftersell, por lo que las Strategies no están integradas en el módulo Upsells como lo están en los flujos post-compra y de checkout de Aftersell. En su lugar, conectas las dos aplicaciones con un pequeño script que llama directamente a la API de Strategies e introduce el resultado en el módulo Upsells existente de Upcart mediante la API pública de Upcart.

El script es **listo para usar**: pégalo una vez en el HTML personalizado de Upcart, reemplaza dos valores (tu clave de API de Strategy y el ID de la Strategy) y el módulo Upsells empezará a mostrar los productos que devuelva la Strategy.

***

<div id="what-youll-need">
  ## Qué necesitarás
</div>

1. **Tu clave de API de Strategy.** En Aftersell, ve a **Settings → Product Strategy** y, en la tarjeta **Security Token**, copia tu token (esta es tu clave de API de Strategy).
2. **El ID de la Strategy.** Abre la Strategy que quieres ejecutar en el editor de Strategies de Aftersell y copia su ID.
3. **El módulo Upsells habilitado en Upcart.** El script sobrescribe la lista de productos mostrados en el bloque de upsell existente, por lo que el módulo debe estar activado para que se renderice algo.

***

<div id="adding-the-script">
  ## Añadir el script
</div>

En Upcart, ve a **Settings → Custom HTML → Scripts (before load)** y pega el script a continuación. Reemplaza `STRATEGY_ID` y `STRATEGY_API_KEY` con los valores de Aftersell y luego guarda.

```html theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
<script>
  const STRATEGY_ID = "YOUR_STRATEGY_ID";
  const STRATEGY_API_KEY = "YOUR_STRATEGY_API_KEY";
  const STRATEGY_BACKEND_URL = "https://start.aftersell.app";

  let cartToken = null;
  const fetchCartToken = async () => {
    const res = await fetch("/cart.js");
    const c = await res.json();
    cartToken = c.token;
  };

  const mapCartItemToContext = (cartItem) => ({
    productId: "gid://shopify/Product/" + cartItem.productId.toString(),
    variantId: "gid://shopify/ProductVariant/" + cartItem.variantId.toString(),
    tags: [],
    title: cartItem.title,
    vendor: cartItem.vendor,
    productType: cartItem.productType,
    handle: cartItem.handle,
    quantity: cartItem.quantity,
    price: cartItem.originalPrice / 100,
  });

  // --- StrategyProduct -> Upcart Product conversion -----------------------

  const gidToNumericId = (gid) => Number(String(gid).split("/").pop());
  const priceStringToCents = (price) =>
    price == null ? null : Math.round(parseFloat(price) * 100);

  const strategyMetafieldsToProductMetafields = (metafields = []) => {
    const grouped = {};
    for (const { namespace, key, value } of metafields) {
      grouped[namespace] = grouped[namespace] || {};
      grouped[namespace][key] = value;
    }
    return { product: grouped };
  };

  const deriveProductOptions = (variants = []) => {
    const byName = new Map();
    for (const variant of variants) {
      (variant.selectedOptions ?? []).forEach((opt, idx) => {
        if (!byName.has(opt.name)) {
          byName.set(opt.name, { name: opt.name, position: idx + 1, values: [] });
        }
        const entry = byName.get(opt.name);
        if (!entry.values.includes(opt.value)) entry.values.push(opt.value);
      });
    }
    return [...byName.values()];
  };

  const mapStrategyVariantToProductVariant = (variant) => {
    const selected = variant.selectedOptions ?? [];
    const optionValues = selected.map((o) => o.value);
    return {
      id: gidToNumericId(variant.variantId),
      title: variant.title,
      option1: optionValues[0] ?? null,
      option2: optionValues[1] ?? null,
      option3: optionValues[2] ?? null,
      sku: variant.sku ?? "",
      requires_shipping: true,
      taxable: true,
      featured_image: null,
      available: variant.availableForSale,
      name: variant.title,
      public_title: variant.title,
      options: optionValues,
      price: priceStringToCents(variant.price) ?? 0,
      weight: 0,
      compare_at_price: priceStringToCents(variant.compareAtPrice),
      inventory_management: "",
      barcode: null,
      requires_selling_plan: false,
      selling_plan_allocations: [],
    };
  };

  const mapStrategyProductToProduct = (product) => {
    const variants = (product.variants ?? []).map(mapStrategyVariantToProductVariant);
    const variantPrices = variants.map((v) => v.price);
    const priceMin = variantPrices.length ? Math.min(...variantPrices) : (priceStringToCents(product.price) ?? 0);
    const priceMax = variantPrices.length ? Math.max(...variantPrices) : (priceStringToCents(product.price) ?? 0);

    const variantCompareAtPrices = variants
      .map((v) => v.compare_at_price)
      .filter((p) => p != null);
    const compareAtMin = variantCompareAtPrices.length ? Math.min(...variantCompareAtPrices) : 0;
    const compareAtMax = variantCompareAtPrices.length ? Math.max(...variantCompareAtPrices) : 0;

    const images = (product.images ?? [])
      .slice()
      .sort((a, b) => a.position - b.position)
      .map((img) => img.src);

    return {
      id: gidToNumericId(product.productId),
      title: product.title,
      handle: product.handle,
      description: product.description ?? "",
      published_at: "",
      created_at: "",
      vendor: product.vendor ?? "",
      type: product.productType ?? "",
      tags: [...(product.tags ?? [])],
      price: priceStringToCents(product.price) ?? 0,
      price_min: priceMin,
      price_max: priceMax,
      available: product.availableForSale,
      price_varies: priceMin !== priceMax,
      compare_at_price: priceStringToCents(product.compareAtPrice),
      compare_at_price_min: compareAtMin,
      compare_at_price_max: compareAtMax,
      compare_at_price_varies: compareAtMin !== compareAtMax,
      variants,
      images,
      featured_image: images[0] ?? "",
      options: deriveProductOptions(product.variants),
      url: product.url ?? "",
      media: [],
      requires_selling_plan: false,
      selling_plan_groups: [],
      metafields: strategyMetafieldsToProductMetafields(product.metafields),
    };
  };

  // -----------------------------------------------------------------------

  let replacedUpsells = null;
  let lastFetchedCartSignature = null;

  const cartSignature = (cart) =>
    JSON.stringify(cart.items.map((i) => [i.variantId, i.quantity]));

  const runStrategyEvaluation = async () => {
    if (!STRATEGY_ID || !STRATEGY_API_KEY) return;

    await fetchCartToken();
    if (!cartToken) return;

    const cart = window.upcartGetCart();
    if (!cart) return;

    const signature = cartSignature(cart);
    if (signature === lastFetchedCartSignature) return;
    lastFetchedCartSignature = signature;

    const cartContext = {
      subtotal: cart.total_price / 100,
      itemCount: cart.items.reduce((acc, item) => acc + item.quantity, 0),
      lineCount: cart.items.length,
    };

    const products = cart.items.map(mapCartItemToContext);

    const res = await fetch(STRATEGY_BACKEND_URL + "/api/public/strategy/evaluate", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Strategy-Api-Key": STRATEGY_API_KEY,
      },
      body: JSON.stringify({
        shopDomain: window.Shopify.shop,
        strategyId: STRATEGY_ID,
        context: {
          products,
          cartToken,
          cart: cartContext.itemCount > 0 ? cartContext : undefined,
          session: { currencyCode: window.Shopify.currency.active },
        },
      }),
    });
    replacedUpsells = await res.json();

    if (typeof window.upcartRefreshCart === "function") {
      window.upcartRefreshCart();
    }
  };

  window.upcartSubscribeCartUpdated(runStrategyEvaluation);

  const waitForUpcartCart = (timeoutMs = 10000) =>
    new Promise((resolve) => {
      const start = Date.now();
      const check = () => {
        if (window.upcartGetCart()) return resolve(true);
        if (Date.now() - start > timeoutMs) return resolve(false);
        setTimeout(check, 100);
      };
      check();
    });

  waitForUpcartCart().then((ready) => {
    if (ready) runStrategyEvaluation();
  });

  window.upcartModifyListOfUpsells = () => {
    if (!replacedUpsells || !Array.isArray(replacedUpsells.products)) return;
    try {
      return replacedUpsells.products.map(mapStrategyProductToProduct);
    } catch (err) {
      console.error("upcartModifyListOfUpsells mapping failed", err);
      return;
    }
  };
</script>
```

<Warning>
  Tu clave de API de Strategy autoriza llamadas a las Strategies de tu tienda. El script anterior la coloca en código del lado del cliente, que es la única forma práctica de invocar la API desde el cajón del carrito. Trata la clave como cualquier otra credencial pública de la tienda y rótala desde **Settings → Product Strategy** en Aftersell si alguna vez queda expuesta de una forma que no pretendías.
</Warning>

***

<div id="what-the-strategy-sees">
  ## Qué ve la Strategy
</div>

Como esto se ejecuta desde el carrito de la tienda, el contexto es un subconjunto reducido de lo que está disponible en las superficies nativas de Aftersell:

<div id="product-context">
  #### Contexto de producto
</div>

Las líneas de pedido actualmente en el carrito de Upcart se envían como productos de entrada. Los activadores como **tipo de producto**, **proveedor**, **handle de producto**, **título de producto** y cualquier activador de **ID de producto / ID de variante** se evalúan contra estos artículos.

<div id="cart-context">
  #### Contexto del carrito
</div>

* **Subtotal**: subtotal del carrito en las unidades principales de la moneda de la tienda (p. ej., dólares). El `total_price` de Upcart está en unidades menores (centavos), por lo que el script divide entre 100 para coincidir con las unidades que usa el resto de la API de Strategies, y con las unidades en las que están escritas tus reglas de `cart_subtotal`.
* **Item count**: cantidad total en todas las líneas.
* **Line count**: número de líneas de pedido distintas.

<div id="session-context">
  #### Contexto de sesión
</div>

* **Código de moneda**: tomado de `window.Shopify.currency.active`.

<Warning>
  **Los activadores de cliente y los activadores UTM no coincidirán.** El script predeterminado no envía etiquetas de cliente, número de pedidos, ubicación ni parámetros UTM, por lo que cualquier regla que use esos activadores nunca se disparará. Usa activadores de producto, carrito y moneda, o un Catch all, para asegurarte de que siempre se devuelva algo.
</Warning>

***

<div id="what-happens-when-the-strategy-returns">
  ## Qué pasa cuando la Strategy devuelve un resultado
</div>

Los productos devueltos por la Strategy **reemplazan** por completo la lista que Upcart mostraría de otro modo en el módulo Upsells. La lista de upsells definida por el comerciante se sobrescribe mientras dure ese carrito; no se fusiona.

Cada producto de la Strategy se mapea a la forma de producto que Upcart espera (variantes, imágenes, opciones, metacampos, etc.) para que se renderice dentro del bloque de upsell exactamente como cualquier otro producto.

***

<div id="when-no-product-is-returned">
  ## Cuando no se devuelve ningún producto
</div>

Si la Strategy no devuelve productos, el módulo Upsells se renderiza **vacío**: no se muestran upsells.

Para evitarlo, configura un **Catch all** en la Strategy de modo que siempre haya un producto de respaldo para devolver. Consulta la página [Crear Strategies](/es/aftersell/strategies_building_in_app) para saber cómo configurar un Catch all.

***

<div id="re-evaluation-on-cart-changes">
  ## Reevaluación con los cambios del carrito
</div>

A diferencia de los upsells del checkout, la implementación de Upcart **reevalúa la Strategy cada vez que el carrito cambia**: artículos añadidos, eliminados o con cantidad actualizada. El script se suscribe al evento `cartUpdated` de Upcart, envía el nuevo carrito a la API de Strategies y actualiza el cajón del carrito con la nueva lista de upsells.

Una comprobación de firma del carrito omite llamadas redundantes si las líneas de pedido y las cantidades realmente no han cambiado, de modo que los eventos de carrito consecutivos que no cambian materialmente el carrito no volverán a llamar a la API.

***

<div id="tips-for-upcart-strategies">
  ## Consejos para Strategies de Upcart
</div>

* **Diseña en torno al carrito.** Los activadores de forma del carrito y de producto son las señales más sólidas que tienes aquí. El historial del cliente y la segmentación basada en UTM no se envían con el script predeterminado.
* **Usa el Catch all como red de seguridad.** Sin uno, el módulo Upsells no mostrará nada cuando ninguna regla coincida.
* **Amigable con la caché de forma predeterminada.** La protección de firma del carrito evita volver a llamar a la API si el carrito no ha cambiado materialmente, ideal para los compradores que abren y cierran el carrito sin editarlo.
