TEC.DELIVERY
enesilru

Desarrollo de un Plugin de Sincronización de Productos para Tec.Delivery

Metodología para crear integraciones con sistemas WMS, ERP, POS y sistemas contables, en adelante denominados WMS.

Esta guía describe la arquitectura recomendada, el proceso de desarrollo, las reglas de mapeo de datos y el procedimiento de integración con Merchant Product API para crear plugins personalizados de sincronización de categorías, productos, inventario e imágenes en la plataforma Tec.Delivery.

Cada plugin de sincronización debe utilizar identificadores externos estables y admitir actualizaciones incrementales para minimizar la carga sobre la API y reducir el tiempo de sincronización.
Preparación

Información requerida

Antes de comenzar el desarrollo, es necesario obtener las credenciales de acceso y la información técnica tanto de la plataforma Tec.Delivery como del sistema WMS.

Documentación completa: Product Management API

Desarrollo del plugin

Arquitectura del plugin

Se recomienda dividir el plugin en módulos independientes responsables de la planificación de ejecuciones, obtención de datos, transformación de datos, carga de datos en Tec.Delivery y registro de eventos.

Módulo Propósito Ejemplo
Programador Controla la frecuencia de ejecución Cron cada 5 minutos
Store API Client Obtiene datos del sistema externo get_products_store()
Tec API Client Obtiene datos de Tec.Delivery get_products_tec()
Mapper Transforma los datos getProducts()
Uploader Envía datos a la API put_products()
Logger Registra la actividad del plugin integration.log

Tipos de sincronización

Tipo Propósito Frecuencia recomendada
Sincronización completa Verifica todo el catálogo 1–2 veces al día
Sincronización de cambios Actualiza productos modificados Cada 15–30 minutos
Sincronización de inventario Actualiza existencias Cada 5–10 minutos
Ejemplo

La sincronización completa se ejecuta durante la noche, los cambios de productos se sincronizan cada 30 minutos y las existencias se actualizan cada 5 minutos.

Trabajo con Merchant Product API

API → Product → put-products

El método put-products de Merchant Product API se utiliza para cargar productos y categorías.

Parámetro Descripción Ejemplo
api Grupo de API product
method Método de API put-products
token Token del comercio merchant_token
language Idioma de origen de los datos ES
translate Traducción automática true
webhook URL para notificar la finalización del procesamiento https://example.com/webhook

Mapeo de categorías

Campo del sistema externo Campo de Tec.Delivery Ejemplo
id external_id 1001
parent external_parent_id 100
name name Bebidas
modified_at external_key 2026-06-01 10:00:00

Mapeo de productos

Campo del sistema externo Campo de Tec.Delivery Ejemplo
id external_id SKU123
parent external_parent_id 1001
name name Coca-Cola 500 ml
description description Bebida gaseosa
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
Ejemplo

El campo external_key se utiliza para detectar cambios en los productos, mientras que external_data se utiliza para detectar cambios en las imágenes.

Sincronización de imágenes

Parámetro Descripción Ejemplo
url URL de la imagen https://site.com/image.jpg
resize Redimensionar imagen false
bgcolor Color de fondo #FFFFFF
scale Factor de escala 0.8
Las imágenes solo deben enviarse cuando cambie el valor de 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" => "ES",
    "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"] ?? "ES",
            "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;
}

        
Pruebas

Verificación de la sincronización

Prueba Resultado esperado Ejemplo
Creación de categoría La categoría aparece en el catálogo Nueva categoría importada
Creación de producto El producto aparece en el catálogo Nuevo SKU importado
Modificación de producto Los cambios se aplican correctamente Precio actualizado
Cambio de inventario El inventario se actualiza 150 → 75
Eliminación de producto El producto se desactiva enable = false
Notas importantes

Recomendaciones de desarrollo

Utilice siempre valores estables para external_id. Cambiar los identificadores provocará la creación de productos duplicados.
Para catálogos grandes, utilice siempre carga paginada mediante limit y offset.
Implemente reintentos automáticos para errores HTTP 429 y 5xx.
Envíe los datos en lotes de no más de 100 productos por solicitud.
Lista de verificación final

Lista de verificación final

  • Se obtuvo el Merchant Token.
  • Se implementó la obtención de categorías.
  • Se implementó la obtención de productos.
  • Se implementó la obtención de inventario.
  • Se configuró la sincronización de imágenes.
  • Se implementó la sincronización incremental.
  • Se configuró el registro de errores.
  • Se configuró la lógica de reintentos.
  • Se configuró el webhook para procesar resultados.
  • Se completaron las pruebas con datos de producción.