> ## 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.

# Upsell in Upcart

> Usa una Strategy per scegliere dinamicamente i prodotti mostrati nel modulo Upsells di Upcart.

<div id="overview">
  ## Panoramica
</div>

Upcart è un'app separata da Aftersell, quindi le Strategy non sono integrate nel modulo Upsells come lo sono nei flussi post-acquisto e di checkout di Aftersell. Puoi però fare da ponte tra le due app con un piccolo script che chiama direttamente la Strategies API e passa il risultato al modulo Upsells esistente di Upcart tramite l'API pubblica di Upcart.

Lo script è **pronto all'uso**: incollalo una volta nell'HTML personalizzato di Upcart, sostituisci due valori (la tua Strategy API key e lo Strategy ID) e il modulo Upsells inizierà a mostrare i prodotti restituiti dalla Strategy.

***

<div id="what-youll-need">
  ## Di cosa avrai bisogno
</div>

1. **La tua Strategy API key.** In Aftersell, vai su **Settings → Product Strategy** e, nella scheda **Security Token**, copia il tuo token (questa è la tua Strategy API key).
2. **Lo Strategy ID.** Apri la Strategy che vuoi eseguire nell'editor delle Strategy di Aftersell e copia il suo ID.
3. **Il modulo Upsells abilitato in Upcart.** Lo script sovrascrive l'elenco dei prodotti mostrati nel blocco di upsell esistente, quindi il modulo deve essere attivo perché venga visualizzato qualcosa.

***

<div id="adding-the-script">
  ## Aggiungere lo script
</div>

In Upcart, vai su **Settings → Custom HTML → Scripts (before load)** e incolla lo script qui sotto. Sostituisci `STRATEGY_ID` e `STRATEGY_API_KEY` con i valori di Aftersell, poi salva.

```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>
  La tua Strategy API key autorizza le chiamate verso le Strategy del tuo negozio. Lo script qui sopra la inserisce nel codice lato client, che è l'unico modo pratico per invocare l'API dal cart drawer. Tratta la chiave come qualsiasi altra credenziale pubblica della vetrina e ruotala da Aftersell **Settings → Product Strategy** se dovesse mai essere esposta in un modo non voluto.
</Warning>

***

<div id="what-the-strategy-sees">
  ## Cosa vede la Strategy
</div>

Poiché questo viene eseguito dal carrello della vetrina, il contesto è un sottoinsieme ridotto di quanto disponibile sulle superfici native di Aftersell:

<div id="product-context">
  #### Contesto prodotto
</div>

Le voci attualmente nel carrello Upcart vengono inviate come prodotti di input. Trigger come **product type**, **vendor**, **product handle**, **product title** e qualsiasi trigger su **product ID / variant ID** vengono tutti valutati rispetto a questi articoli.

<div id="cart-context">
  #### Contesto carrello
</div>

* **Subtotal** - il subtotale del carrello nelle unità principali della valuta del negozio (es. dollari). Il `total_price` di Upcart è in unità minori (centesimi), quindi lo script divide per 100 per allinearsi alle unità usate dal resto della Strategies API - e alle unità in cui sono scritte le tue regole `cart_subtotal`.
* **Item count** - la quantità totale su tutte le righe.
* **Line count** - il numero di voci distinte.

<div id="session-context">
  #### Contesto sessione
</div>

* **Codice valuta** - ricavato da `window.Shopify.currency.active`.

<Warning>
  **I trigger su cliente e UTM non troveranno corrispondenza.** Lo script predefinito non invia tag cliente, numero di ordini, posizione o parametri UTM, quindi qualsiasi regola che usa questi trigger non si attiverà mai. Usa trigger su prodotto, carrello e valuta, oppure un Catch all, per assicurarti che venga sempre restituito qualcosa.
</Warning>

***

<div id="what-happens-when-the-strategy-returns">
  ## Cosa succede quando la Strategy restituisce un risultato
</div>

I prodotti restituiti dalla Strategy **sostituiscono** completamente l'elenco che Upcart mostrerebbe altrimenti nel modulo Upsells. L'elenco di upsell definito dal merchant viene sovrascritto per la durata di quel carrello: non viene unito.

Ogni prodotto della Strategy viene mappato nella forma di prodotto attesa da Upcart (varianti, immagini, opzioni, metafield, ecc.) in modo da essere visualizzato all'interno del blocco di upsell esattamente come qualsiasi altro prodotto.

***

<div id="when-no-product-is-returned">
  ## Quando nessun prodotto viene restituito
</div>

Se la Strategy non restituisce alcun prodotto, il modulo Upsells viene visualizzato **vuoto**: nessun upsell viene mostrato.

Per evitarlo, configura un **Catch all** nella Strategy in modo che ci sia sempre un prodotto di riserva da restituire. Consulta la pagina [Costruire le Strategy](/it/aftersell/strategies_building_in_app) per scoprire come impostare un Catch all.

***

<div id="re-evaluation-on-cart-changes">
  ## Rivalutazione alle modifiche del carrello
</div>

A differenza degli upsell al checkout, l'implementazione Upcart **rivaluta la Strategy ogni volta che il carrello cambia**: articoli aggiunti, rimossi o con quantità aggiornata. Lo script si iscrive all'evento `cartUpdated` di Upcart, invia il nuovo carrello alla Strategies API e aggiorna il cart drawer con il nuovo elenco di upsell.

Un controllo sulla firma del carrello salta le chiamate ridondanti se le voci e le quantità non sono effettivamente cambiate, quindi eventi del carrello consecutivi che non lo modificano in modo sostanziale non richiameranno l'API.

***

<div id="tips-for-upcart-strategies">
  ## Suggerimenti per le Strategy in Upcart
</div>

* **Progetta intorno al carrello.** I trigger sulla composizione del carrello e sui prodotti sono i segnali più forti che hai qui. La cronologia del cliente e il targeting basato su UTM non vengono inviati dallo script predefinito.
* **Usa il Catch all come rete di sicurezza.** Senza di esso, il modulo Upsells non mostrerà nulla quando nessuna regola corrisponde.
* **Cache-friendly per impostazione predefinita.** La protezione basata sulla firma del carrello evita di richiamare l'API se il carrello non è cambiato in modo sostanziale: ottimo per gli acquirenti che aprono e chiudono il carrello senza modificarlo.
