Metafield ile ayarlanan tek bir indirim Function’ı

Ürün ve sipariş indirimleri her indirimde ayrı bir JSON metafield’dan okunan Shopify indirim Function’ı; tek deploy ile birçok kampanya.

Function API
Discount
Hedef (target)
cart.lines.discounts.generate.run
İndirim sınıfları
PRODUCT, ORDER
Plan
Herkese açık uygulamada her plan; özel uygulamada Shopify Plus
Uygulama yetkisi
write_discounts
API sürümü
2026-10
Son kontrol

Kısaca

Bu indirim Function’ı tüm kampanyasını her indirimdeki bir JSON metafield’dan okur: seçili koleksiyonlardaki ürünlere yüzde indirim, belirli bir tutarın üstündeki siparişlere sabit indirim ya da ikisi birden. Tek bir deploy birçok indirime hizmet eder. Discount Function API’nin cart.lines.discounts.generate.run hedefini kullanır (2026-10) ve her indirim için yalnızca o indirimin sınıflarını döndürür.

Nasıl kullanılır

  1. Shopify CLI 4.9.0 ya da üstüyle bir uygulama oluşturun ve uzantıyı ekleyin: shopify app generate extension --template discount, dil olarak JavaScript.
  2. Oluşturulan shopify.extension.toml ve src/ dosyalarını aşağıdakilerle değiştirin.
  3. Testleri çalıştırın: shopify app function run --input tests/product-and-order.input.json.
  4. Deploy edin, ardından her kampanyayı discountAutomaticAppCreate (kodlu indirim için discountCodeAppCreate) ile oluşturup JSON’unu metafields içinde gönderin.
  5. Her indirime yalnızca kullandığı sınıfları verin; örneğin koleksiyon indirimi için discountClasses: [PRODUCT].

Kod

shopify.extension.toml
api_version = "2026-10"

[[extensions]]
name = "Discount configured by metafield"
handle = "metafield-configured-discount"
type = "function"

  [[extensions.targeting]]
  target = "cart.lines.discounts.generate.run"
  input_query = "src/cart_lines_discounts_generate_run.graphql"
  input_variables = { namespace = "$app", key = "function-configuration" }
  export = "cart-lines-discounts-generate-run"

  [extensions.build]
  command = ""
  path = "dist/function.wasm"
src/cart_lines_discounts_generate_run.graphql
query CartLinesDiscountsGenerateRunInput($collectionIds: [ID!]) {
  cart {
    lines {
      id
      merchandise {
        __typename
        ... on ProductVariant {
          product {
            inAnyCollection(ids: $collectionIds)
          }
        }
      }
    }
  }
  discount {
    discountClasses
    metafield(namespace: "$app", key: "function-configuration") {
      jsonValue
    }
  }
}
src/cart_lines_discounts_generate_run.js
// @ts-check

/**
 * @typedef {import("../generated/api").CartLinesDiscountsGenerateRunInput} CartLinesDiscountsGenerateRunInput
 * @typedef {import("../generated/api").CartLinesDiscountsGenerateRunResult} CartLinesDiscountsGenerateRunResult
 */

/**
 * One Function, many campaigns: each discount created from it carries its own
 * JSON in the $app:function-configuration metafield. `collectionIds` in that
 * JSON also feeds the query variable (see input_variables in the toml).
 * Missing parts switch that part off.
 */
const DEFAULT_CONFIG = {
  // { percentage, message }; applies to products in collectionIds, or to all products when the list is empty.
  product: /** @type {{percentage: number, message?: string} | null} */ (null),
  collectionIds: /** @type {string[]} */ ([]),
  // { amount, minimumSubtotal, message }; a fixed amount off orders above minimumSubtotal (store currency).
  order: /** @type {{amount: number, minimumSubtotal?: number, message?: string} | null} */ (null),
};

/**
 * @param {CartLinesDiscountsGenerateRunInput} input
 * @returns {CartLinesDiscountsGenerateRunResult}
 */
export function cartLinesDiscountsGenerateRun(input) {
  const config = { ...DEFAULT_CONFIG, ...input.discount.metafield?.jsonValue };
  const classes = input.discount.discountClasses;
  const operations = [];

  // A Function may only return operations for the classes the discount was created with.
  if (config.product && classes.includes("PRODUCT")) {
    const targets = input.cart.lines
      .filter(
        (line) =>
          line.merchandise.__typename === "ProductVariant" &&
          (config.collectionIds.length === 0 || line.merchandise.product.inAnyCollection),
      )
      .map((line) => ({ cartLine: { id: line.id } }));
    if (targets.length > 0) {
      operations.push({
        productDiscountsAdd: {
          candidates: [
            {
              message: config.product.message ?? `${config.product.percentage}% off`,
              targets,
              value: { percentage: { value: String(config.product.percentage) } },
            },
          ],
          selectionStrategy: "FIRST",
        },
      });
    }
  }

  if (config.order && classes.includes("ORDER")) {
    // Shopify checks the minimum itself (in the store's currency), so the
    // candidate is offered and only applied when the condition holds.
    const conditions = config.order.minimumSubtotal
      ? [{ orderMinimumSubtotal: { minimumAmount: String(config.order.minimumSubtotal), excludedCartLineIds: [] } }]
      : [];
    operations.push({
      orderDiscountsAdd: {
        candidates: [
          {
            message: config.order.message ?? `${config.order.amount} off your order`,
            conditions,
            targets: [{ orderSubtotal: { excludedCartLineIds: [] } }],
            value: { fixedAmount: { amount: String(config.order.amount) } },
          },
        ],
        selectionStrategy: "FIRST",
      },
    });
  }

  return { operations };
}
Test girdisi ve beklenen çıktı
tests/not-configured.input.json
{
  "cart": { "lines": [
      { "id": "gid://shopify/CartLine/1", "merchandise": { "__typename": "ProductVariant", "product": { "inAnyCollection": true } } },
      { "id": "gid://shopify/CartLine/2", "merchandise": { "__typename": "ProductVariant", "product": { "inAnyCollection": false } } }
    ] },
  "discount": { "discountClasses": ["PRODUCT", "ORDER"], "metafield": null }
}
tests/not-configured.output.json
{ "operations": [] }
tests/order-class-only.input.json
{
  "cart": { "lines": [
      { "id": "gid://shopify/CartLine/1", "merchandise": { "__typename": "ProductVariant", "product": { "inAnyCollection": true } } },
      { "id": "gid://shopify/CartLine/2", "merchandise": { "__typename": "ProductVariant", "product": { "inAnyCollection": false } } }
    ] },
  "discount": {
    "discountClasses": ["ORDER"],
    "metafield": { "jsonValue": {
        "product": { "percentage": 20, "message": "20% off summer collection" },
        "collectionIds": ["gid://shopify/Collection/1234567890"],
        "order": { "amount": 50, "minimumSubtotal": 500, "message": "50 off orders over 500" }
      } }
  }
}
tests/order-class-only.output.json
{
  "operations": [
    {
      "orderDiscountsAdd": {
        "candidates": [
          {
            "message": "50 off orders over 500",
            "conditions": [{ "orderMinimumSubtotal": { "minimumAmount": "500", "excludedCartLineIds": [] } }],
            "targets": [{ "orderSubtotal": { "excludedCartLineIds": [] } }],
            "value": { "fixedAmount": { "amount": "50" } }
          }
        ],
        "selectionStrategy": "FIRST"
      }
    }
  ]
}
tests/product-and-order.input.json
{
  "cart": { "lines": [
      { "id": "gid://shopify/CartLine/1", "merchandise": { "__typename": "ProductVariant", "product": { "inAnyCollection": true } } },
      { "id": "gid://shopify/CartLine/2", "merchandise": { "__typename": "ProductVariant", "product": { "inAnyCollection": false } } }
    ] },
  "discount": {
    "discountClasses": ["PRODUCT", "ORDER"],
    "metafield": { "jsonValue": {
        "product": { "percentage": 20, "message": "20% off summer collection" },
        "collectionIds": ["gid://shopify/Collection/1234567890"],
        "order": { "amount": 50, "minimumSubtotal": 500, "message": "50 off orders over 500" }
      } }
  }
}
tests/product-and-order.output.json
{
  "operations": [
    {
      "productDiscountsAdd": {
        "candidates": [
          {
            "message": "20% off summer collection",
            "targets": [{ "cartLine": { "id": "gid://shopify/CartLine/1" } }],
            "value": { "percentage": { "value": "20" } }
          }
        ],
        "selectionStrategy": "FIRST"
      }
    },
    {
      "orderDiscountsAdd": {
        "candidates": [
          {
            "message": "50 off orders over 500",
            "conditions": [{ "orderMinimumSubtotal": { "minimumAmount": "500", "excludedCartLineIds": [] } }],
            "targets": [{ "orderSubtotal": { "excludedCartLineIds": [] } }],
            "value": { "fixedAmount": { "amount": "50" } }
          }
        ],
        "selectionStrategy": "FIRST"
      }
    }
  ]
}

Lintel tarafından yazıldı. MIT lisansı: kendi mağazanızda da müşteri işlerinizde de ücretsiz kullanabilirsiniz.

Sınırlar ve özel durumlar

  • Sipariş minimumu, Shopify’ın mağaza para biriminde kontrol ettiği bir orderMinimumSubtotal koşulu olarak gönderilir.
  • collectionIds listesi boşsa ürün indirimi tüm ürünlere uygulanır.
  • 10.000 baytı aşan metafield’lar Function’lara gelmez; kampanyaları küçük tutun ya da bölün.

Sık sorulan sorular

İndirim Function’ı neden bir metafield ile ayarlanmalı?
Metafield olmadan her değişiklik kod düzenleyip deploy etmek demektir. Metafield ile mağaza sahibi ya da uygulamanızın yönetim sayfası rakamları indirim bazında değiştirir; Function aynı kalır.
Yapılandırmanın kullandığı bir sınıf indirimde yoksa ne olur?
O kısım atlanır. Function, ürün ya da sipariş işlemi döndürmeden önce discount.discountClasses’ı kontrol eder.
Mağaza sahibi yapılandırmayı nasıl değiştirir?
GraphQL Admin API’deki metafieldsSet ile ya da uygulamanızın kendi ayar sayfasından. Sık değişiyorsa küçük bir form yapın.
  • Function API sürümü 2026-10 için yazıldı ve gösterilen tarihte Shopify’ın başvuru belgeleriyle karşılaştırıldı. Shopify her çeyrekte yeni bir sürüm yayımlar.
  • Function içeren özel (custom) uygulamalar yalnızca Shopify Plus mağazalarında çalışır. Aynı Function herkese açık bir uygulamada, sayfada aksi yazmıyorsa her planda çalışır.
  • Her örnek test verileriyle gelir ve yayımlamadan önce bu testleri çalıştırırız. Kodu değiştirdikten sonra testleri tekrar çalıştırın: shopify app function run.
  • MIT lisansı. Kodu kendi mağazanızda da müşteri işlerinizde de kullanabilirsiniz.

Mağazanız için kurulmasını, test edilmesini ve bakımını isterseniz: Shopify Functions geliştirme.

Geliştirmek istediğiniz bir şey mi var? Gelin, birlikte konuşalım.

Aynı anda sınırlı sayıda proje alıyoruz.

Gencer KarakayaShopify mühendisi

Proje özetinizi bir mühendis okur; size e-postayla yanıt veririz.