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.
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.
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 |
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
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 |
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 |
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;
}
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 |
Recomendaciones de desarrollo
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.