TEC.DELIVERY
enesilru

פיתוח תוסף סנכרון מוצרים עבור Tec.Delivery

מתודולוגיה ליצירת אינטגרציות עם מערכות WMS, ERP, POS ומערכות הנהלת חשבונות, להלן WMS.

מדריך זה מתאר את הארכיטקטורה המומלצת, תהליך הפיתוח, כללי מיפוי הנתונים ותהליך האינטגרציה עם Merchant Product API לצורך בניית תוספי סנכרון מותאמים אישית עבור קטגוריות, מוצרים, רמות מלאי ותמונות בפלטפורמת Tec.Delivery.

כל תוסף סנכרון חייב להשתמש במזהים חיצוניים יציבים ולתמוך בעדכוני נתונים מצטברים כדי לצמצם את העומס על ה-API ולהקטין את זמן הסנכרון.
הכנה

מידע נדרש

לפני תחילת הפיתוח יש לקבל פרטי גישה ומידע טכני הן עבור פלטפורמת Tec.Delivery והן עבור מערכת ה-WMS.

תיעוד מלא: Product Management API

פיתוח התוסף

ארכיטקטורת התוסף

מומלץ לחלק את התוסף למודולים עצמאיים האחראים על תזמון, שליפת נתונים, המרת נתונים, העלאת נתונים ל-Tec.Delivery ורישום לוגים.

מודול מטרה דוגמה
Scheduler בקרת תדירות ההפעלה 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 שפת נתוני המקור EN
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 ml
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 כתובת תמונה 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" => "IL",
    "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"] ?? "IL",
            "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 יציבים. שינוי מזהים יגרום ליצירת מוצרים כפולים.
בקטלוגים גדולים יש להשתמש תמיד בטעינה מדורגת (pagination) באמצעות limit ו-offset.
יש לממש מנגנון ניסיונות חוזרים עבור שגיאות HTTP 429 ו-5xx.
יש לשלוח נתונים במנות של לא יותר מ-100 מוצרים לבקשה.
רשימת בדיקה סופית

רשימת בדיקה סופית

  • טוקן הסוחר התקבל.
  • מימוש שליפת קטגוריות הושלם.
  • מימוש שליפת מוצרים הושלם.
  • מימוש שליפת מלאי הושלם.
  • סנכרון תמונות הוגדר.
  • סנכרון מצטבר מומש.
  • רישום שגיאות הוגדר.
  • מנגנון ניסיונות חוזרים הוגדר.
  • Webhook לעיבוד תוצאות הוגדר.
  • הבדיקות הושלמו באמצעות נתוני ייצור.