X alana Y indirimi
Bir üründen 2 alana başka bir üründen 1 adet bedava ya da indirimli veren, en ucuzdan başlayan Shopify indirim Function’ı; test edilmiş JavaScript.
- Function API
- Discount
- Hedef (target)
- cart.lines.discounts.generate.run
- İndirim sınıfı
- PRODUCT
- 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’ı bir “X alana Y” kampanyası yürütür: listelediğiniz ürünlerden her 2 adede karşılık ödül ürünlerinden 1 adet bedava olur, en ucuzundan başlayarak. Adetler, oran ve ürün kimlikleri bir JSON metafield’da durur. Discount Function API’nin cart.lines.discounts.generate.run hedefini ürün sınıfı ve adet sınırlı sepet satırı hedefleriyle kullanır, sürüm 2026-10.
Nasıl kullanılır
- Shopify CLI ile bir uygulama oluşturun ve uzantıyı ekleyin: shopify app generate extension --template discount, dil olarak JavaScript.
- Oluşturulan shopify.extension.toml ve src/ dosyalarını aşağıdakilerle değiştirin.
- Testleri çalıştırın: shopify app function run --input tests/qualifies.input.json.
- shopify app deploy ile deploy edin ve discountAutomaticAppCreate ile discountClasses: [PRODUCT] olan otomatik bir indirim oluşturun.
- İndirimin $app:function-configuration metafield’ına buyProductIds ve getProductIds’i, gerekirse buyQuantity, getQuantity ve percentage’ı yazın.
Kod
shopify.extension.tomlapi_version = "2026-10"
[[extensions]]
name = "Buy X get Y discount"
handle = "buy-x-get-y-discount"
type = "function"
[[extensions.targeting]]
target = "cart.lines.discounts.generate.run"
input_query = "src/cart_lines_discounts_generate_run.graphql"
export = "cart-lines-discounts-generate-run"
[extensions.build]
command = ""
path = "dist/function.wasm"
src/cart_lines_discounts_generate_run.graphqlquery CartLinesDiscountsGenerateRunInput {
cart {
lines {
id
quantity
cost {
amountPerQuantity {
amount
}
}
merchandise {
__typename
... on ProductVariant {
product {
id
}
}
}
}
}
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
*/
/**
* Settings stored as JSON in the discount's metafield. The product lists are
* empty by default, so the discount does nothing until it's configured.
* Keep the two lists separate: a product in both counts only as "buy".
*/
const DEFAULT_CONFIG = {
buyProductIds: /** @type {string[]} */ ([]),
buyQuantity: 2,
getProductIds: /** @type {string[]} */ ([]),
getQuantity: 1,
// 100 makes the Y items free.
percentage: 100,
// Shown at checkout; null builds it from the numbers above ("Buy 2, get 1 free").
message: /** @type {string | null} */ (null),
};
/**
* @param {{buyQuantity: number, getQuantity: number, percentage: number}} config
*/
const defaultMessage = ({ buyQuantity, getQuantity, percentage }) =>
`Buy ${buyQuantity}, get ${getQuantity} ${percentage >= 100 ? "free" : `${percentage}% off`}`;
/** @type {CartLinesDiscountsGenerateRunResult} */
const NO_DISCOUNT = { operations: [] };
/**
* @param {CartLinesDiscountsGenerateRunInput} input
* @returns {CartLinesDiscountsGenerateRunResult}
*/
export function cartLinesDiscountsGenerateRun(input) {
// A Function may only return operations for the classes the discount was created with.
if (!input.discount.discountClasses.includes("PRODUCT")) return NO_DISCOUNT;
const config = { ...DEFAULT_CONFIG, ...input.discount.metafield?.jsonValue };
/** @param {(typeof input.cart.lines)[number]} line */
const productId = (line) => (line.merchandise.__typename === "ProductVariant" ? line.merchandise.product.id : null);
const buyLines = input.cart.lines.filter((line) => config.buyProductIds.includes(productId(line)));
const getLines = input.cart.lines.filter(
(line) => !buyLines.includes(line) && config.getProductIds.includes(productId(line)),
);
const boughtUnits = buyLines.reduce((sum, line) => sum + line.quantity, 0);
let freeUnits = Math.floor(boughtUnits / config.buyQuantity) * config.getQuantity;
if (freeUnits === 0 || getLines.length === 0) return NO_DISCOUNT;
// Discount the cheapest Y units first, as customers expect from "get one free".
const targets = [];
const cheapestFirst = [...getLines].sort(
(a, b) => Number(a.cost.amountPerQuantity.amount) - Number(b.cost.amountPerQuantity.amount),
);
for (const line of cheapestFirst) {
if (freeUnits === 0) break;
const quantity = Math.min(line.quantity, freeUnits);
targets.push({ cartLine: { id: line.id, quantity } });
freeUnits -= quantity;
}
return {
operations: [
{
productDiscountsAdd: {
candidates: [{ message: config.message ?? defaultMessage(config), targets, value: { percentage: { value: String(config.percentage) } } }],
selectionStrategy: "FIRST",
},
},
],
};
}
Test girdisi ve beklenen çıktı
tests/not-enough-bought.input.json{
"cart": {
"lines": [
{ "id": "gid://shopify/CartLine/1", "quantity": 1, "cost": { "amountPerQuantity": { "amount": "30.0" } }, "merchandise": { "__typename": "ProductVariant", "product": { "id": "gid://shopify/Product/1111111111" } } },
{ "id": "gid://shopify/CartLine/2", "quantity": 1, "cost": { "amountPerQuantity": { "amount": "12.0" } }, "merchandise": { "__typename": "ProductVariant", "product": { "id": "gid://shopify/Product/2222222222" } } }
]
},
"discount": {
"discountClasses": ["PRODUCT"],
"metafield": {
"jsonValue": {
"buyProductIds": ["gid://shopify/Product/1111111111"],
"getProductIds": ["gid://shopify/Product/2222222222"]
}
}
}
}
tests/not-enough-bought.output.json{ "operations": [] }
tests/qualifies.input.json{
"cart": {
"lines": [
{ "id": "gid://shopify/CartLine/1", "quantity": 2, "cost": { "amountPerQuantity": { "amount": "30.0" } }, "merchandise": { "__typename": "ProductVariant", "product": { "id": "gid://shopify/Product/1111111111" } } },
{ "id": "gid://shopify/CartLine/2", "quantity": 1, "cost": { "amountPerQuantity": { "amount": "12.0" } }, "merchandise": { "__typename": "ProductVariant", "product": { "id": "gid://shopify/Product/2222222222" } } }
]
},
"discount": {
"discountClasses": ["PRODUCT"],
"metafield": {
"jsonValue": {
"buyProductIds": ["gid://shopify/Product/1111111111"],
"getProductIds": ["gid://shopify/Product/2222222222"]
}
}
}
}
tests/qualifies.output.json{
"operations": [
{
"productDiscountsAdd": {
"candidates": [
{
"message": "Buy 2, get 1 free",
"targets": [{ "cartLine": { "id": "gid://shopify/CartLine/2", "quantity": 1 } }],
"value": { "percentage": { "value": "100" } }
}
],
"selectionStrategy": "FIRST"
}
}
]
}
tests/two-sets-cheapest-first.input.json{
"cart": {
"lines": [
{ "id": "gid://shopify/CartLine/1", "quantity": 5, "cost": { "amountPerQuantity": { "amount": "30.0" } }, "merchandise": { "__typename": "ProductVariant", "product": { "id": "gid://shopify/Product/1111111111" } } },
{ "id": "gid://shopify/CartLine/2", "quantity": 3, "cost": { "amountPerQuantity": { "amount": "15.0" } }, "merchandise": { "__typename": "ProductVariant", "product": { "id": "gid://shopify/Product/2222222222" } } },
{ "id": "gid://shopify/CartLine/3", "quantity": 1, "cost": { "amountPerQuantity": { "amount": "12.0" } }, "merchandise": { "__typename": "ProductVariant", "product": { "id": "gid://shopify/Product/2222222222" } } }
]
},
"discount": {
"discountClasses": ["PRODUCT"],
"metafield": {
"jsonValue": {
"buyProductIds": ["gid://shopify/Product/1111111111"],
"getProductIds": ["gid://shopify/Product/2222222222"]
}
}
}
}
tests/two-sets-cheapest-first.output.json{
"operations": [
{
"productDiscountsAdd": {
"candidates": [
{
"message": "Buy 2, get 1 free",
"targets": [
{ "cartLine": { "id": "gid://shopify/CartLine/3", "quantity": 1 } },
{ "cartLine": { "id": "gid://shopify/CartLine/2", "quantity": 1 } }
],
"value": { "percentage": { "value": "100" } }
}
],
"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
- Ödül ürünü sepette olmalıdır; indirim Function’ı satır ekleyemez. Ürünü tema ya da bir cart transform ile ekleyin.
- Alınan ve kazanılan ürün listelerini ayrı tutun. Bu kodda iki listede birden olan bir ürün yalnızca alınan ürün sayılır.
- Hedefteki adet, indirimi bedava adetlerle sınırlar; fazladan ödül ürünleri normal fiyattan ücretlendirilir.
Sık sorulan sorular
- Shopify’da X alana Y zaten yok mu?
- Basit durumlar için var. Function, yönetim panelindeki ayarların karşılayamadığı kurallar gerektiğinde işe yarar; örneğin birden fazla üründe en ucuzdan başlayan ödül ya da başka bir mantıkla birleştirme.
- Aynı üründe “2 al, 3.’sü bedava” nasıl yapılır?
- Bu kod ayrı listeler bekler. Tek ürün için sayımı grup büyüklüğüne (buyQuantity + getQuantity) çevirin ve her tam grupta getQuantity kadar adede indirim verin.
- Ödül bedava yerine yüzde indirim olabilir mi?
- Evet. percentage’ı ayarlayın; örneğin yarı fiyat için 50. 100 ödülü bedava yapar.
Shopify belgeleri
- 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.
