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

# عروض البيع الإضافي في Upcart

> استخدم استراتيجية (Strategy) لاختيار المنتجات المعروضة في وحدة عروض البيع الإضافي في Upcart بشكل ديناميكي.

<div id="overview">
  ## نظرة عامة
</div>

Upcart تطبيق منفصل عن Aftersell، لذا فإن الاستراتيجيات ليست مدمجة في وحدة Upsells كما هي في تدفقات ما بعد الشراء وصفحة الدفع في Aftersell. بدلاً من ذلك، تربط بين التطبيقين بسكريبت صغير يستدعي Strategies API مباشرة ويُغذي النتيجة إلى وحدة Upsells الحالية في Upcart عبر واجهة برمجة التطبيقات العامة الخاصة بـ Upcart.

السكريبت **جاهز للاستخدام** - الصقه مرة واحدة في HTML المخصص في Upcart، واستبدل قيمتين (مفتاح Strategy API الخاص بك ومعرّف الاستراتيجية)، وستبدأ وحدة Upsells في عرض أي منتجات تعيدها الاستراتيجية.

***

<div id="what-youll-need">
  ## ما ستحتاج إليه
</div>

1. **مفتاح Strategy API الخاص بك.** في Aftersell، انتقل إلى **Settings → Product Strategy**، وفي بطاقة **Security Token**، انسخ الرمز الخاص بك (هذا هو مفتاح Strategy API).
2. **معرّف الاستراتيجية.** افتح الاستراتيجية التي تريد تشغيلها في محرر الاستراتيجيات في Aftersell وانسخ معرّفها.
3. **تفعيل وحدة Upsells في Upcart.** يتجاوز السكريبت قائمة المنتجات المعروضة في كتلة البيع الإضافي الحالية، لذا يجب أن تكون الوحدة مفعّلة حتى يظهر أي شيء.

***

<div id="adding-the-script">
  ## إضافة السكريبت
</div>

في Upcart، انتقل إلى **Settings → Custom HTML → Scripts (before load)** والصق السكريبت أدناه. استبدل `STRATEGY_ID` و`STRATEGY_API_KEY` بالقيم من Aftersell، ثم احفظ.

```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>
  مفتاح Strategy API الخاص بك يفوّض الاستدعاءات مقابل استراتيجيات متجرك. يضع السكريبت أعلاه المفتاح في كود على جانب العميل، وهي الطريقة العملية الوحيدة لاستدعاء واجهة برمجة التطبيقات من درج السلة (cart drawer). تعامل مع المفتاح كما تتعامل مع أي بيانات اعتماد عامة أخرى لواجهة المتجر، وقم بتدويره من **Settings → Product Strategy** في Aftersell إذا تم كشفه بأي طريقة لم تكن مقصودة.
</Warning>

***

<div id="what-the-strategy-sees">
  ## ما تراه الاستراتيجية
</div>

نظرًا لأن هذا يعمل من سلة واجهة المتجر، فإن السياق مجموعة فرعية مختصرة مما هو متاح في الأسطح الأصلية لـ Aftersell:

<div id="product-context">
  #### سياق المنتج
</div>

يتم إرسال العناصر الموجودة حاليًا في سلة Upcart كمنتجات إدخال. المشغّلات مثل **نوع المنتج** و**المورّد** و**معرّف المنتج (handle)** و**عنوان المنتج** وأي مشغّلات **معرّف المنتج / معرّف المتغير** تُقيَّم جميعها مقابل هذه العناصر.

<div id="cart-context">
  #### سياق السلة
</div>

* **الإجمالي الفرعي (Subtotal)** - الإجمالي الفرعي للسلة بوحدات العملة الرئيسية للمتجر (مثل الدولارات). قيمة `total_price` في Upcart بالوحدات الصغرى (السنتات)، لذا يقسم السكريبت على 100 لمطابقة الوحدات التي تستخدمها بقية Strategies API - والوحدات التي كُتبت بها قواعد `cart_subtotal` الخاصة بك.
* **عدد العناصر (Item count)** - إجمالي الكمية عبر جميع البنود.
* **عدد البنود (Line count)** - عدد البنود المميزة.

<div id="session-context">
  #### سياق الجلسة
</div>

* **رمز العملة (Currency code)** - يُؤخذ من `window.Shopify.currency.active`.

<Warning>
  **مشغّلات العميل ومشغّلات UTM لن تتطابق.** لا يرسل السكريبت الافتراضي وسوم العملاء أو عدد الطلبات أو الموقع أو معلمات UTM - لذا فإن أي قاعدة تستخدم هذه المشغّلات لن تُفعَّل أبدًا. استخدم مشغّلات المنتج والسلة والعملة، أو Catch all، لضمان إعادة شيء ما دائمًا.
</Warning>

***

<div id="what-happens-when-the-strategy-returns">
  ## ماذا يحدث عندما تُعيد الاستراتيجية نتيجة
</div>

المنتجات التي تعيدها الاستراتيجية **تحل تمامًا محل** القائمة التي كان Upcart سيعرضها في وحدة Upsells. يتم تجاوز قائمة البيع الإضافي المحددة من التاجر طوال مدة تلك السلة - ولا يتم دمجها.

يتم تحويل كل منتج من منتجات الاستراتيجية إلى شكل المنتج المتوقع في Upcart (المتغيرات والصور والخيارات وحقول التعريف وما إلى ذلك) بحيث يُعرض داخل كتلة البيع الإضافي تمامًا مثل أي منتج آخر.

***

<div id="when-no-product-is-returned">
  ## عندما لا يُعاد أي منتج
</div>

إذا لم تُعِد الاستراتيجية أي منتجات، تُعرض وحدة Upsells **فارغة** - ولا تظهر أي عروض بيع إضافي.

لتجنب ذلك، قم بإعداد **Catch all** في الاستراتيجية بحيث يكون هناك دائمًا منتج احتياطي يُعاد. راجع صفحة [بناء الاستراتيجيات](/ar/aftersell/strategies_building_in_app) لمعرفة كيفية إعداد Catch all.

***

<div id="re-evaluation-on-cart-changes">
  ## إعادة التقييم عند تغيّر السلة
</div>

على عكس عروض البيع الإضافي في صفحة الدفع، فإن تنفيذ Upcart **يعيد تقييم الاستراتيجية في كل مرة تتغير فيها السلة** - عند إضافة عناصر أو إزالتها أو تحديث الكميات. يشترك السكريبت في حدث `cartUpdated` الخاص بـ Upcart، ويرسل السلة الجديدة إلى Strategies API، ويحدّث درج السلة بقائمة البيع الإضافي الجديدة.

يتخطى فحص توقيع السلة الاستدعاءات الزائدة إذا لم تتغير البنود والكميات فعليًا، لذا فإن أحداث السلة المتتالية التي لا تغيّر السلة جوهريًا لن تستدعي واجهة برمجة التطبيقات مجددًا.

***

<div id="tips-for-upcart-strategies">
  ## نصائح لاستراتيجيات Upcart
</div>

* **صمم حول السلة.** مشغّلات شكل السلة والمنتج هي أقوى الإشارات المتاحة لك هنا. لا يرسل السكريبت الافتراضي سجل العميل ولا الاستهداف المعتمد على UTM.
* **استخدم Catch all كشبكة أمان.** بدونه، لن تعرض وحدة Upsells شيئًا كلما لم تتطابق أي قاعدة.
* **صديق للتخزين المؤقت بشكل افتراضي.** يمنع حارس توقيع السلة إعادة استدعاء واجهة برمجة التطبيقات إذا لم تتغير السلة جوهريًا - وهو أمر جيد للمتسوقين الذين يفتحون السلة ويغلقونها دون تعديلها.
