Налаштування Shopify Functions для користувацької логіки знижок/доставки
Shopify Functions — механізм запуску користувацького коду безпосередньо в інфраструктурі Shopify. На відміну від додатків з webhook, Functions виконуються синхронно у процесі чекауту: Shopify викликає функцію, отримує відповідь та застосовує результат.
Де працюють Functions
| API функцій | Призначення |
|---|---|
cart_transform |
Змінення рядків кошика перед чекаутом (бандли, модифікації) |
discounts |
Користувацькі знижки (фіксовані, відсоток, BXGY) |
payment_customization |
Приховування/перейменування способів оплати |
shipping_discount |
Знижка на доставку |
delivery_customization |
Приховування/перейменування/сортування методів доставки |
fulfillment_constraints |
Обмеження на розбивку за складами |
order_routing |
Маршрутизація замовлень за складами |
Functions пишуться на Rust (офіційна мова, компілюється в WebAssembly) або JavaScript/TypeScript (через Javy runtime). Rust переважніший для складної логіки — швидше та економніше за лімітами.
Установка та ініціалізація
# Functions створюються всередині Shopify App
shopify app generate extension --template discount --name custom-volume-discount
cd extensions/custom-volume-discount
Структура extension:
extensions/custom-volume-discount/
├── src/
│ └── run.ts # (або run.rs для Rust)
├── input.graphql # Описання вхідних даних
├── shopify.extension.toml
└── package.json
Приклад: скидка за обсяг замовлення (JavaScript)
input.graphql — описує, які дані Shopify передасть функції:
# input.graphql
query RunInput {
cart {
lines {
id
quantity
merchandise {
... on ProductVariant {
id
product {
id
tags
metafield(namespace: "custom", key: "volume_discount_tier") {
value
}
}
}
}
}
}
discountNode {
metafield(namespace: "custom", key: "tiers") {
value
}
}
}
src/run.ts — логіка функції:
import type { RunInput, FunctionRunResult } from "../generated/api";
interface DiscountTier {
quantity: number;
percentage: number;
}
export function run(input: RunInput): FunctionRunResult {
// Читаємо конфіг скидок з метаполя discount node
const tiersJson = input.discountNode?.metafield?.value;
const tiers: DiscountTier[] = tiersJson ? JSON.parse(tiersJson) : [];
if (!tiers.length) return { discounts: [], discountApplicationStrategy: "FIRST" };
const discounts = [];
for (const line of input.cart.lines) {
const variant = line.merchandise;
if (variant.__typename !== "ProductVariant") continue;
const product = variant.product;
// Застосовуємо скидку тільки до товарів з тегом 'volume-eligible'
if (!product.tags.includes("volume-eligible")) continue;
// Знаходимо підходящий tier за кількістю
const applicableTier = tiers
.filter(t => line.quantity >= t.quantity)
.sort((a, b) => b.quantity - a.quantity)[0];
if (!applicableTier) continue;
discounts.push({
targets: [{ cartLine: { id: line.id } }],
value: { percentage: { value: String(applicableTier.percentage) } },
message: `Знижка ${applicableTier.percentage}% за кількість (від ${applicableTier.quantity} шт.)`,
});
}
return {
discounts,
discountApplicationStrategy: "FIRST",
};
}
Приклад: користувацька логіка доставки (Rust)
Сховати методи доставки залежно від вмісту кошика:
// src/run.rs
use shopify_function::prelude::*;
use shopify_function::Result;
#[shopify_function_target(query_path = "src/run.graphql", schema_path = "schema.graphql")]
fn run(input: input::ResponseData) -> Result<output::FunctionRunResult> {
let cart_weight_grams: i64 = input.cart.lines.iter()
.map(|line| {
let variant = &line.merchandise;
let weight = variant.weight.unwrap_or(0.0);
(weight * 1000.0 * line.quantity as f64) as i64
})
.sum();
let operations = input.cart.delivery_groups.iter()
.flat_map(|group| group.delivery_options.iter().map(|opt| {
// Приховати «Експрес-доставку» якщо посилка >20кг
let hide = opt.title.contains("Експрес") && cart_weight_grams > 20_000;
output::DeliveryCustomizationOperation {
hide: if hide {
Some(output::HideOperation { delivery_option_handle: opt.handle.clone() })
} else {
None
},
move_: None,
rename: None,
}
}))
.collect();
Ok(output::FunctionRunResult { operations })
}
Конфігурація функції в TOML
# shopify.extension.toml
api_version = "2025-01"
[[extensions]]
name = "custom-volume-discount"
handle = "custom-volume-discount"
type = "function"
[extensions.targeting]
module = "./src/run.ts"
[[extensions.targeting]]
target = "purchase.product-discount.run"
[extensions.build]
command = "npm run build"
path = "dist/function.wasm"
Активація через Shopify Admin
Functions не працюють автоматично після деплою — потрібно створити Discount (або Customization) в Admin та вибрати функцію як джерело логіки:
// Програмне створення скидки через Admin GraphQL API
const CREATE_DISCOUNT = `
mutation discountAutomaticAppCreate($automaticAppDiscount: DiscountAutomaticAppInput!) {
discountAutomaticAppCreate(automaticAppDiscount: $automaticAppDiscount) {
automaticAppDiscount {
discountId
title
startsAt
}
userErrors { field message }
}
}
`;
await adminClient.query({
data: {
query: CREATE_DISCOUNT,
variables: {
automaticAppDiscount: {
title: "Обсяжна знижка",
functionId: "gid://shopify/ShopifyFunction/abc123",
startsAt: new Date().toISOString(),
metafields: [{
namespace: "custom",
key: "tiers",
type: "json",
value: JSON.stringify([
{ quantity: 5, percentage: 5 },
{ quantity: 10, percentage: 10 },
{ quantity: 20, percentage: 15 }
])
}]
}
}
}
});
Обмеження Functions
- Ліміт часу виконання: 5 мс для Rust/WASM, 10 мс для JS
- Ліміт пам'яті: 10 МБ
-
Немає мережевих запитів: функція не може робити HTTP-виклики — всі дані повинні прийти через
input.graphql - Немає доступу до БД: складну конфігурацію передаємо через Metafields у запиті вхідних даних
- Checkout UI Extensions: візуальний UI для чекауту — окремий механізм (не Functions)
Через ліміти на час виконання потрібно максимально спрощувати всю бізнес-логіку. Важкі обчислення перекладаються на підготовчий етап — результат кешується в Metafields та передається у функцію як готові дані.
Тестування
# Запуск тестів функції локально
npm run test # або cargo test
# Деплой extension до додатку
shopify app deploy
# Перегляд логів виконання (через Shopify Partners)
shopify app logs
Строки
Простої функції знижки (відсоток за тегом або колекцією): 2–4 дні. Складна багаторівнева функція з конфігурацією через метаполя та Admin UI для управління правилами: 1–2 тижні. Функція доставки з геологікою та інтеграцією зовнішніх тарифів (через метаполя): 1–2 тижні.







