TEC.DELIVERY
enesilru

Разработка плагина синхронизации товаров для Tec.Delivery

Методология создания интеграций с WMS, ERP, POS и учетными системами, далее WMS.

Данная инструкция описывает рекомендуемую архитектуру, порядок разработки, правила сопоставления данных и процесс интеграции с Merchant Product API для создания собственных плагинов синхронизации категорий, товаров, остатков и изображений в системе Tec.Delivery.

Каждый плагин синхронизации должен использовать стабильные внешние идентификаторы и поддерживать инкрементальное обновление данных для минимизации нагрузки на API и сокращения времени синхронизации.
Подготовка

Необходимые данные

Перед началом разработки необходимо получить доступы и техническую информацию как по системе Tec.Delivery, так и для WMS.

Полная дркументация: Product Management API

Разработка плагина

Архитектура плагина

Рекомендуется разделять плагин на независимые модули, отвечающие за планирование запусков, получение данных, преобразование данных, загрузку данных в 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

API → Product → put-products

Для загрузки товаров и категорий используется метод 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
Изображение должно отправляться только при изменении значения image_modified_at.

Пример реализации плагина на 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
Важные замечания

Рекомендации по разработке

Всегда используйте стабильные значения external_id. Изменение идентификаторов приведет к созданию дубликатов товаров.
Для больших каталогов обязательно используйте постраничную загрузку через limit и offset.
Реализуйте повторные попытки запросов при ошибках HTTP 429 и 5xx.
Отправляйте данные пакетами не более 100 товаров за один запрос.
Final Checklist

Final Checklist

  • Получен Merchant Token.
  • Реализировано получение категорий.
  • Реализировано получение товаров.
  • Реализировано получение остатков.
  • Настроена синхронизация изображений.
  • Реализована инкрементальная синхронизация.
  • Настроено логирование ошибок.
  • Настроены повторные попытки запросов.
  • Настроен webhook обработки результатов.
  • Проведено тестирование на продуктивных данных.