Розробка кастомних API Extensions для commercetools
API Extensions — механізм синхронних хуків у commercetools. При певних операціях з ресурсами платформа робить HTTP-запит до вашого сервісу перед або після збереження, очікуючи відповідь протягом 2 секунд. Це єдиний спосіб додати бізнес-логіку прямо в lifecycle ресурсів без форкування платформи.
Типи тригерів
| Тригер | Ресурс | Коли спрацьовує |
|---|---|---|
Create |
Cart, Order, Customer, Payment | На POST |
Update |
Cart, Order, Customer, Payment | На POST /ID |
Delete |
Customer, Shopping List | На DELETE /ID |
Найбільш затребувані: Cart-Update (пересчет цін, валідація промокодів), Order-Create (отправка в ERP), Payment-Update (синхронізація статусів оплати).
Реєстрація Extension
const extension = await apiRoot.extensions().post({
body: {
key: "cart-validation-extension",
destination: {
type: "HTTP",
url: "https://extensions.your-service.com/cart-validate",
authentication: {
type: "AuthorizationHeader",
headerValue: `Bearer ${process.env.EXTENSION_SECRET}`,
},
},
triggers: [
{
resourceTypeId: "cart",
actions: ["Create", "Update"],
},
],
timeoutInMs: 2000,
},
}).execute();
Extension з типом AWSLambda або GoogleCloudFunction викликається прямо без HTTP — платформа підписує запит через IAM.
Структура Extension сервісу
// extensions/src/cart-handler.ts
import express from "express";
import { ExtensionInput, CartUpdateAction } from "@commercetools/platform-sdk";
const app = express();
app.use(express.json());
app.post("/cart-validate", async (req, res) => {
const input: ExtensionInput = req.body;
const { action, resource } = input;
const cart = resource.obj; // повний об'єкт кошика
const actions: CartUpdateAction[] = [];
const errors: ExtensionError[] = [];
// Приклад: мінімальна сума замовлення
if (action === "Update" && cart.totalPrice.centAmount < 50000) {
errors.push({
code: "InvalidInput",
message: "Мінімальна сума замовлення — 500 ₽",
extensionExtraInfo: { field: "totalPrice" },
});
}
// Приклад: автоматично застосувати VIP знижку
if (cart.customerGroup?.id === "vip-group-id") {
const alreadyHasVipDiscount = cart.discountCodes?.some(
(dc) => dc.discountCode.id === "vip-discount-id"
);
if (!alreadyHasVipDiscount) {
actions.push({
action: "addDiscountCode",
code: "VIP10",
});
}
}
if (errors.length > 0) {
return res.status(400).json({ errors });
}
res.json({ actions });
});
Відповідь з actions — commercetools застосує їх атомарно до ресурсу. Відповідь з errors — операція відхиляється, клієнт отримує 400.
Extension для пересчета цін
Сценарій: ціни беруться з зовнішнього ERP, не з commercetools Price.
app.post("/cart-reprice", async (req, res) => {
const { resource } = req.body as ExtensionInput;
const cart = resource.obj;
const actions: CartUpdateAction[] = [];
// Отримати актуальні ціни з ERP
const skus = cart.lineItems.map((li) => li.variant.sku).filter(Boolean);
const erpPrices = await fetchErpPrices(skus, cart.customerId);
for (const lineItem of cart.lineItems) {
const erpPrice = erpPrices[lineItem.variant.sku!];
if (!erpPrice) continue;
const currentCentAmount = lineItem.price.value.centAmount;
const erpCentAmount = Math.round(erpPrice * 100);
if (currentCentAmount !== erpCentAmount) {
actions.push({
action: "setLineItemPrice",
lineItemId: lineItem.id,
externalPrice: {
value: {
centAmount: erpCentAmount,
currencyCode: cart.totalPrice.currencyCode,
},
},
});
}
}
res.json({ actions });
});
Extension для Order-Create: відправка в ERP
app.post("/order-created", async (req, res) => {
const { resource } = req.body as ExtensionInput;
const order = resource.obj;
try {
const erpOrderId = await sendToErp({
commercetoolsOrderId: order.id,
orderNumber: order.orderNumber,
customer: order.customerEmail,
lines: order.lineItems.map((li) => ({
sku: li.variant.sku,
quantity: li.quantity,
price: li.price.value.centAmount,
})),
shippingAddress: order.shippingAddress,
});
// Зберегти ERP ID як custom field замовлення
res.json({
actions: [
{
action: "setCustomField",
name: "erpOrderId",
value: erpOrderId,
},
],
});
} catch (err) {
// Не відхиляємо створення замовлення при помилці ERP —
// краще записати в чергу й обробити асинхронно
await enqueueForRetry({ orderId: order.id, error: err });
res.json({ actions: [] });
}
});
Розгортання Extension-сервісу
Extension повинен відповідати за 2000 мс. Рекомендовані варіанти розгортання:
- AWS Lambda + API Gateway — холодний старт ≤ 200мс з provisioned concurrency
- Google Cloud Run — min-instances=1 виключає холодний старт
- Kubernetes — якщо вже є кластер
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/
EXPOSE 3000
CMD ["node", "dist/server.js"]
Моніторинг та відладка
commercetools логує всі Extension-виклики в Merchant Center → Developer → Extension Logs (зберігає 7 днів). Кожен лог містить:
- Тіло запиту (payload)
- HTTP-статус відповіді Extension
- Тіло відповіді
- Час виконання
При timeout або 5xx від Extension операція відхиляється — це жорстке поведення, яке не можна змінити. Тому Extension-сервіс повинен бути стабільнішим за основний API.
Часові рамки розробки
| Extension | Складність | Часова рамка |
|---|---|---|
| Валідація кошика | Низька | 1–2 дні |
| Пересчет цін з ERP | Висока | 3–5 днів |
| Order → ERP інтеграція | Середня | 2–4 дні |
| Синхронізація Payment статусів | Середня | 2–3 дні |
| Кастомні знижки + промокоди | Висока | 4–6 днів |







