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

> Utilisez une Strategy pour choisir dynamiquement les produits affichés dans le module Upsells d'Upcart.

<div id="overview">
  ## Aperçu
</div>

Upcart est une application distincte d'Aftersell, donc les Strategies ne sont pas intégrées nativement au module Upsells comme elles le sont dans les flux post-achat et checkout d'Aftersell. À la place, vous reliez les deux applications avec un petit script qui appelle directement l'API Strategies et alimente le résultat dans le module Upsells existant d'Upcart via l'API publique d'Upcart.

Le script est **prêt à l'emploi** — collez-le une fois dans le HTML personnalisé d'Upcart, remplacez deux valeurs (votre clé API Strategy et le Strategy ID), et le module Upsells commencera à afficher les produits renvoyés par la Strategy.

***

<div id="what-youll-need">
  ## Ce dont vous aurez besoin
</div>

1. **Votre clé API Strategy.** Dans Aftersell, accédez à **Settings → Product Strategy** et, dans la carte **Security Token**, copiez votre jeton (c'est votre clé API Strategy).
2. **Le Strategy ID.** Ouvrez la Strategy que vous souhaitez exécuter dans l'éditeur de Strategy d'Aftersell et copiez son ID.
3. **Le module Upsells activé dans Upcart.** Le script remplace la liste des produits affichés dans le bloc d'upsell existant, le module doit donc être activé pour que quelque chose s'affiche.

***

<div id="adding-the-script">
  ## Ajouter le script
</div>

Dans Upcart, accédez à **Settings → Custom HTML → Scripts (before load)** et collez le script ci-dessous. Remplacez `STRATEGY_ID` et `STRATEGY_API_KEY` par les valeurs provenant d'Aftersell, puis enregistrez.

```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>
  Votre clé API Strategy autorise les appels vers les Strategies de votre boutique. Le script ci-dessus la place dans du code côté client, ce qui est le seul moyen pratique d'invoquer l'API depuis le tiroir de panier. Traitez la clé comme n'importe quel autre identifiant public de boutique et régénérez-la depuis Aftersell **Settings → Product Strategy** si elle est un jour exposée d'une manière non souhaitée.
</Warning>

***

<div id="what-the-strategy-sees">
  ## Ce que voit la Strategy
</div>

Comme cela s'exécute depuis le panier de la boutique, le contexte est un sous-ensemble réduit de ce qui est disponible sur les surfaces natives d'Aftersell :

<div id="product-context">
  #### Contexte produit
</div>

Les lignes d'articles actuellement dans le panier Upcart sont envoyées comme produits d'entrée. Les déclencheurs comme le **type de produit**, le **fournisseur**, le **handle de produit**, le **titre de produit** et tout déclencheur d'**ID de produit / ID de variante** sont tous évalués par rapport à ces articles.

<div id="cart-context">
  #### Contexte panier
</div>

* **Subtotal** — sous-total du panier dans les unités monétaires principales de la boutique (par ex. en dollars). Le `total_price` d'Upcart est en unités mineures (centimes), donc le script divise par 100 pour correspondre aux unités utilisées par le reste de l'API Strategies — et aux unités dans lesquelles vos règles `cart_subtotal` sont rédigées.
* **Item count** — quantité totale sur toutes les lignes.
* **Line count** — nombre de lignes d'articles distinctes.

<div id="session-context">
  #### Contexte session
</div>

* **Code de devise** — extrait de `window.Shopify.currency.active`.

<Warning>
  **Les déclencheurs client et les déclencheurs UTM ne correspondront pas.** Le script par défaut n'envoie pas les tags client, le nombre de commandes, la localisation ni les paramètres UTM — toute règle utilisant ces déclencheurs ne se déclenchera donc jamais. Utilisez des déclencheurs de produit, de panier et de devise, ou un Catch all, pour garantir qu'un résultat soit toujours renvoyé.
</Warning>

***

<div id="what-happens-when-the-strategy-returns">
  ## Ce qui se passe quand la Strategy renvoie un résultat
</div>

Les produits renvoyés par la Strategy **remplacent** entièrement la liste qu'Upcart afficherait autrement dans le module Upsells. La liste d'upsells définie par le marchand est remplacée pour la durée de ce panier — elle n'est pas fusionnée.

Chaque produit de la Strategy est converti dans le format de produit attendu par Upcart (variantes, images, options, métachamps, etc.) afin qu'il s'affiche dans le bloc d'upsell exactement comme n'importe quel autre produit.

***

<div id="when-no-product-is-returned">
  ## Quand aucun produit n'est renvoyé
</div>

Si la Strategy ne renvoie aucun produit, le module Upsells s'affiche **vide** — aucun upsell n'est montré.

Pour éviter cela, configurez un **Catch all** dans la Strategy afin qu'il y ait toujours un produit de repli à renvoyer. Consultez la page [Construire des Strategies](/fr/aftersell/strategies_building_in_app) pour savoir comment configurer un Catch all.

***

<div id="re-evaluation-on-cart-changes">
  ## Réévaluation lors des changements de panier
</div>

Contrairement aux upsells de checkout, l'implémentation Upcart **réévalue la Strategy à chaque changement du panier** — articles ajoutés, retirés ou quantités modifiées. Le script s'abonne à l'événement `cartUpdated` d'Upcart, envoie le nouveau panier à l'API Strategies et actualise le tiroir de panier avec la nouvelle liste d'upsells.

Une vérification de signature de panier ignore les appels redondants si les lignes d'articles et les quantités n'ont pas réellement changé, de sorte que des événements de panier consécutifs sans changement significatif ne solliciteront pas à nouveau l'API.

***

<div id="tips-for-upcart-strategies">
  ## Conseils pour les Strategies Upcart
</div>

* **Concevez autour du panier.** Les déclencheurs de composition du panier et de produit sont les signaux les plus forts dont vous disposez ici. L'historique client et le ciblage basé sur les UTM ne sont pas envoyés par le script par défaut.
* **Utilisez le Catch all comme filet de sécurité.** Sans lui, le module Upsells n'affichera rien dès qu'aucune règle ne correspond.
* **Économe en appels par défaut.** La protection par signature de panier évite de resolliciter l'API si le panier n'a pas réellement changé — idéal pour les acheteurs qui ouvrent et ferment le panier sans le modifier.
