Настройка 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 недели.







