Разработка плагина синхронизации товаров для Tec.Delivery
Методология создания интеграций с WMS, ERP, POS и учетными системами, далее WMS.
Данная инструкция описывает рекомендуемую архитектуру, порядок разработки, правила сопоставления данных и процесс интеграции с Merchant Product API для создания собственных плагинов синхронизации категорий, товаров, остатков и изображений в системе Tec.Delivery.
Необходимые данные
Перед началом разработки необходимо получить доступы и техническую информацию как по системе Tec.Delivery, так и для WMS.
Архитектура плагина
Рекомендуется разделять плагин на независимые модули, отвечающие за планирование запусков, получение данных, преобразование данных, загрузку данных в Tec.Delivery и логирование.
| Модуль | Назначение | Пример |
|---|---|---|
| Планировщик | Контроль периодичности запуска | Cron каждые 5 минут |
| Store API Client | Получение данных из внешней системы | get_products_store() |
| Tec API Client | Получение данных из Tec.Delivery | get_products_tec() |
| Mapper | Преобразование данных | getProducts() |
| Uploader | Отправка данных в API | put_products() |
| Logger | Логирование работы | integration.log |
Типы синхронизации
| Тип | Назначение | Рекомендуемый период |
|---|---|---|
| Полная синхронизация | Проверка всего каталога | 1–2 раза в сутки |
| Синхронизация изменений | Обновление измененных товаров | Каждые 15–30 минут |
| Синхронизация остатков | Обновление остатков | Каждые 5–10 минут |
Полная синхронизация выполняется ночью, синхронизация изменений — каждые 30 минут, остатки обновляются каждые 5 минут.
Работа с Merchant Product API
Для загрузки товаров и категорий используется метод put-products Merchant Product API.
| Параметр | Описание | Пример |
|---|---|---|
| api | Группа API | product |
| method | Метод API | put-products |
| token | Токен магазина | merchant_token |
| language | Исходный язык данных | RU |
| translate | Автоматический перевод | true |
| webhook | URL для уведомления о завершении обработки | https://example.com/webhook |
Сопоставление категорий
| Поле внешней системы | Поле Tec.Delivery | Пример |
|---|---|---|
| id | external_id | 1001 |
| parent | external_parent_id | 100 |
| name | name | Напитки |
| modified_at | external_key | 2026-06-01 10:00:00 |
Сопоставление товаров
| Поле внешней системы | Поле Tec.Delivery | Пример |
|---|---|---|
| id | external_id | SKU123 |
| parent | external_parent_id | 1001 |
| name | name | Coca-Cola 500 мл |
| description | description | Газированный напиток |
| price | price | 199 |
| store_balance | store_balance | 150 |
| modified_at | external_key | 2026-06-01 10:00:00 |
| image_modified_at | external_data | 2026-06-01 10:15:00 |
Поле external_key используется для определения изменения товара, а external_data — для определения изменения изображения.
Синхронизация изображений
| Параметр | Описание | Пример |
|---|---|---|
| url | URL изображения | https://site.com/image.jpg |
| resize | Изменение размера | false |
| bgcolor | Цвет фона | #FFFFFF |
| scale | Коэффициент масштабирования | 0.8 |
Пример реализации плагина на PHP
$tecApiUrl = "https://api.tec.delivery/common/merchant/1.0/";
$limit = 100;
$settings = [
"tec_token" => "YOUR_TEC_TOKEN",
"api_url" => "https://example.com/api/",
"method_get_product" => "products",
"method_get_balance" => "balance",
"webhook" => "https://example.com/webhook",
"auth" => false, // или "login:password"
"language" => "RU",
"translate" => true,
];
echo "Start sync\n";
$put = [];
$put = array_merge($put, sync_categories($settings));
$put = array_merge($put, sync_products($settings));
put_products($settings, $put);
echo "Done\n";
function sync_categories(array $settings): array
{
$tecCat = get_products_tec([
"api" => "product",
"method" => "get-categories",
"fields" => [
"product_id",
"name",
"external_id",
"enable",
"external_parent_id",
"external_key"
],
"filter" => [
"enable" => ["eq" => true]
]
], $settings);
foreach ($tecCat as $key => $cat) {
$tecCat[$key]["exist"] = false;
}
$storeCat = get_products_store([
"type" => "category"
], $settings);
$put = [];
foreach ($storeCat as $cat) {
if (empty($cat["id"])) {
continue;
}
$index = findIndexByField($tecCat, "external_id", $cat["id"]);
if ($index !== false) {
$tecCat[$index]["exist"] = true;
if (($tecCat[$index]["external_key"] ?? null) === ($cat["modified_at"] ?? null)) {
continue;
}
}
$put[] = [
"external_id" => $cat["id"],
"external_parent_id" => $cat["parent"] ?? 0,
"name" => $cat["name"],
"external_key" => $cat["modified_at"] ?? null,
"is_category" => true,
"enable" => true,
];
}
foreach ($tecCat as $cat) {
if ($cat["exist"] === false) {
$put[] = [
"product_id" => $cat["product_id"],
"enable" => false,
];
}
}
return $put;
}
function sync_products(array $settings): array
{
$tecProducts = get_products_tec([
"api" => "product",
"method" => "get-all",
"fields" => [
"product_id",
"name",
"external_id",
"external_key",
"external_data",
"enable"
],
"filter" => [
"enable" => ["eq" => true],
"is_category" => ["eq" => false]
]
], $settings);
foreach ($tecProducts as $key => $product) {
$tecProducts[$key]["exist"] = false;
}
$storeProducts = get_products_store([
"type" => "product"
], $settings);
$put = [];
foreach ($storeProducts as $product) {
if (empty($product["id"])) {
continue;
}
$index = findIndexByField($tecProducts, "external_id", $product["id"]);
if ($index === false) {
$put[] = prepare_product($product, true);
continue;
}
$tecProducts[$index]["exist"] = true;
$changed =
($tecProducts[$index]["external_key"] ?? null) !== ($product["modified_at"] ?? null);
$imageChanged =
($tecProducts[$index]["external_data"] ?? null) !== ($product["image_modified_at"] ?? null);
if ($changed || $imageChanged) {
$put[] = prepare_product($product, $imageChanged);
}
}
foreach ($tecProducts as $product) {
if ($product["exist"] === false) {
$put[] = [
"product_id" => $product["product_id"],
"enable" => false,
"store_balance" => 0,
];
}
}
return $put;
}
function prepare_product(array $product, bool $withImage = false): array
{
$item = [
"external_id" => $product["id"],
"external_parent_id" => $product["parent"] ?? 0,
"external_key" => $product["modified_at"] ?? null,
"external_data" => $product["image_modified_at"] ?? null,
"name" => $product["name"] ?? "",
"description" => $product["description"] ?? "",
"price" => isset($product["price"]) ? (float)$product["price"] : 0,
"store_balance" => isset($product["store_balance"]) ? (int)$product["store_balance"] : 0,
"enable" => true,
"is_category" => false,
];
if ($withImage && !empty($product["image_url"])) {
$item["image"] = [
"url" => $product["image_url"],
"resize" => false,
"bgcolor" => "#FFFFFF",
"scale" => 0.8,
"remove_background" => false,
"hash" => [
"field" => "external_data",
"value" => $product["image_modified_at"] ?? ""
]
];
}
return $item;
}
function put_products(array $settings, array $data): void
{
global $tecApiUrl, $limit;
if (empty($data)) {
echo "Nothing to update\n";
return;
}
$chunks = array_chunk($data, $limit);
$total = count($chunks);
foreach ($chunks as $index => $chunk) {
echo "Put products: " . ($index + 1) . " / {$total}\n";
$request = [
"api" => "product",
"method" => "put-products",
"token" => $settings["tec_token"],
"webhook" => $settings["webhook"],
"language" => $settings["language"] ?? "RU",
"translate" => $settings["translate"] ?? true,
"payload" => $chunk
];
get_curl_response($tecApiUrl, $request);
}
}
function get_products_tec(array $request, array $settings): array
{
global $tecApiUrl, $limit;
$offset = 0;
$result = [];
while ($offset !== false) {
echo "Get Tec products offset: {$offset}\n";
$url = $tecApiUrl . "?token={$settings["tec_token"]}&limit={$limit}&offset={$offset}";
$response = get_curl_response($url, $request);
$data = $response["payload"]["data"] ?? [];
$offset = $response["payload"]["offset"] ?? false;
if (!is_array($data)) {
throw new Exception("Invalid Tec API response");
}
$result = array_merge($result, $data);
}
return $result;
}
function get_products_store(array $params, array $settings, string $method = "method_get_product"): array
{
global $limit;
$offset = 0;
$count = 1;
$result = [];
$query = http_build_query($params);
while ($offset < $count) {
echo "Get store products offset: {$offset}\n";
$url = $settings["api_url"]
. $settings[$method]
. "?limit={$limit}&offset={$offset}";
if ($query !== "") {
$url .= "&" . $query;
}
$response = get_curl_response($url, false, $settings["auth"] ?? false);
$count = (int)($response["count"] ?? 0);
$data = $response["data"] ?? [];
if (!is_array($data)) {
throw new Exception("Invalid store API response");
}
$result = array_merge($result, $data);
$offset += $limit;
}
return $result;
}
function get_curl_response(string $url, array|false $raw = false, string|false $basicAuth = false): array
{
$ch = curl_init($url);
$headers = [
"Accept: application/json;charset=utf-8",
"Accept-Encoding: gzip, deflate"
];
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_ENCODING, "");
if ($raw !== false) {
$json = json_encode($raw, JSON_UNESCAPED_UNICODE);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
$headers[] = "Content-Type: application/json";
$headers[] = "Content-Length: " . strlen($json);
}
if ($basicAuth) {
curl_setopt($ch, CURLOPT_USERPWD, $basicAuth);
}
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_CONNECTTIMEOUT => 60,
CURLOPT_TIMEOUT => 60,
]);
$response = curl_exec($ch);
$http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
throw new Exception("cURL error: {$error}");
}
curl_close($ch);
if ($http < 200 || $http >= 300) {
throw new Exception("HTTP error {$http}: {$response}");
}
$decoded = json_decode($response, true);
if (!is_array($decoded)) {
throw new Exception("Invalid JSON response: {$response}");
}
return $decoded;
}
function findIndexByField(array $array, string $field, mixed $value): int|false
{
foreach ($array as $index => $item) {
if (isset($item[$field]) && $item[$field] == $value) {
return $index;
}
}
return false;
}
Проверка работы синхронизации
| Тест | Ожидаемый результат | Пример |
|---|---|---|
| Создание категории | Категория появилась в каталоге | Новая категория импортирована |
| Создание товара | Товар появился в каталоге | Импортирован новый SKU |
| Изменение товара | Изменения применены | Обновлена цена |
| Изменение остатков | Остаток обновлен | 150 → 75 |
| Удаление товара | Товар отключен | enable = false |
Рекомендации по разработке
Final Checklist
- Получен Merchant Token.
- Реализировано получение категорий.
- Реализировано получение товаров.
- Реализировано получение остатков.
- Настроена синхронизация изображений.
- Реализована инкрементальная синхронизация.
- Настроено логирование ошибок.
- Настроены повторные попытки запросов.
- Настроен webhook обработки результатов.
- Проведено тестирование на продуктивных данных.