Casos de uso
Give Feedback

Sincronizar pedidos

Updated on March 26, 2022

Consulta esta guía si ya viste la introducción.

El caso de uso más común de uso de la API de Handy es para llevar los pedidos de Handy a tu sistema ERP o algún otro sistema administrativo.

Esto lo haces con un polling a la API al endpoint GET https://app.handy.la/api/v2/salesOrder

La recomendación es que hagas el polling cada 10 minutos. Puedes lograr esto con los parámetros start y end

curl -H 'Authorization: Bearer API_TOKEN' 'https://app.handy.la/api/v2/salesOrder?start=01/05/2020%2012:00:00&end=01/05/2020%2013:00:00'

Recuerda que probablemente tienes que codificar los campos de fechas.

Obtendrás un resultado como este:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
    "pagination": {
        "totalCount": 6,
        "totalPages": 1,
        "currentPage": "https://app.handy.la/api/v2/salesOrder?page=1",
        "nextPage": null,
        "prevPage": null,
        "firstPage": "https://app.handy.la/api/v2/salesOrder?page=1",
        "lastPage": "https://app.handy.la/api/v2/salesOrder?page=1"
    },
    "salesOrders": [
        {
        "id": 7391919,
        "mobileDateCreated": "2020-05-01T17:43:04Z",
        "customer": {
            "code": "A4",
            "zone": {
                "description": "Guadalajara",
                "id": 980,
                "enabled": true
            },
            "description": "Stark Industries",
            "id": 4,
            "enabled": false
        },
        "createdBy": {
            "name": "Bernardo Bravo",
            "id": 622,
            "enabled": true,
            "username": "user@gmail.com"
        },
        "items": [
            {
            "isReturn": false,
            "product": {
                "code": "ACME-02-006",
                "quantity": 1.5,
                "satUnitCode": "",
                "description": "Monociclo Propulsado Acme",
                "satProductCode": "",
                "enabled": true,
                "price": 1500.123456,
                "hasBeenSold": false,
                "details": null,
                "applyDiscounts": true,
                "id": 2032143,
                "family": {
                    "description": "Mecánicos",
                    "id": 45033,
                    "enabled": true
                },
                "category": null,
                "barcode": null,
                "taxType": 0
            },
            "total": 10500.864192,
            "quantity": 7.0,
            "comments": null,
            "originalPrice": 0.0,
            "isReward": false,
            "price": 1500.123456,
            "discount": null,
            "id": 25223135,
            "promoIds": null,
            "promoNames": null
            },
        ],
        "type": {
            "description": "Cambio físico",
            "id": 12028,
            "enabled": true
        },
        "exported": false,
        "deleted": false,
        "sellerComment": null,
        "scheduledDateForDelivery": "2020-05-02T17:43:04Z",
        "latitude": 20.646848911292956,
        "longitude": -103.45223890705688,
        "accuracy": 0.0,
        "tookInPlace": true,
        "priceList": null,
        "totalSales": 11700.864192,
        "billable": false,
        "billed": false,
        "routeSalePaymentType": null,
        "dateCreated": "2020-05-02T02:00:10Z",
        "promoIds": null,
        "promoNames": null,
        "networkSignalQuality": -1,
        "creationSource": "MOBILE",
        "dateDeleted": null,
        "deletedBy": null,
        "editedFrom": null,
        "isEdited": false
        },
    ]
}

Ahora tendrías que guardar el momento en el que hiciste el polling para usar el campo end como start de la siguiente consulta.


Devoluciones

Si tienes devoluciones, lo podrás saber con el campo isReturn en el item. En ambos casos, el campo quantity es positivo pero sabes el signo de la devolución con el campo isReturn. El tampo total (cantidad en dinero) para los items, sí tendrá contiene el signo.


Código ejemplo

A continuación te compartimos un ejemplo en JavaScript que te ayuda con todos los aspectos a considerar de la API:

  • Autenticación
  • Paginación
  • Límites de uso (rate limit)
  • Polling cada tiempo que determines
  • Detectar pedidos borrados
  • Guardar el ID del pedido del ERP en Handy por futuras referencias
  • Manejo de errores

Lo primero que tienes que hacer es configurar tu token de Handy con una variable de entorno:

HANDY_BEARER_TOKEN=API_TOKEN

También puedes crear un archivo .env y colocar la variable dentro.

Ahora necesitas crear un nuevo proyecto de Node, e instalar las dependencias:

npm i cron dotenv moment node-fetch

Lo puedes correr con:

node index.js

// https://crontab.guru/#*/10_*_*_*_*
// Default: every 10 minutes
const cronExpression = "*/10 * * * *";

const businessLogic = async function (salesOrders, deleted) {
  console.log(
    `${
      deleted ? "Fetched deleted orders" : "Fetched created orders"
    }. Received ${salesOrders.length} sales orders.`
  );

  for (const salesOrder of salesOrders) {
    if (deleted) {
      // A sales order was deleted in Handy
      // If you want, you can delete the externalId from your system.
    } else {
      // *****
      // Implement your business logic here:
      // ✅ Save sales order in your ERP or system.
      // *****
      // Once you saved the sales order in your system,
      // you can save the externalId on the sales order with the Handy API
      // for future reference if the sales order is deleted in Handy.
      // Then you can easily find the corresponding sales order in your system
      // and delete it too.
      // Uncomment line below to save externalId on sales order∫:
      // await saveExternalIdOnSalesOrder(salesOrder.id, yourExternalId);
    }
  }
};

// -----------------------------------------------------------------------------

require("dotenv").config();
const fetch = require("node-fetch");
const CronJob = require("cron").CronJob;
const fs = require("fs");
const dateFormat = "DD/MM/YYYY HH:mm:ss";
const moment = require("moment");

const jobFunction = async function () {
  await fetchSalesOrders(false);
  await fetchSalesOrders(true);
};

const fetchSalesOrders = async (deleted) => {
  let lastTime = {};
  const filePath = deleted ? "./last_time_deleted.json" : "./last_time.json";

  if (fs.existsSync(filePath)) {
    lastTime.start = JSON.parse(fs.readFileSync(filePath));
    lastTime.end = moment().format(dateFormat);
  } else {
    // Defaults
    lastTime.start = moment().format(dateFormat);
    lastTime.end = lastTime.start;
  }

  const url = `https://app.handy.la/api/v2/salesOrder?start=${lastTime.start}&end=${lastTime.end}&deleted=${deleted}`;
  let response = await queryHandyAPI(url);

  if (!response) {
    console.log("No response from Handy API");
    return;
  }

  let salesOrders = response.salesOrders;

  while (response && response.pagination && response.pagination.nextPage) {
    response = await queryHandyAPI(response.pagination.nextPage);
    if (response) salesOrders.push(...response.salesOrders);
  }

  try {
    await businessLogic(salesOrders, deleted);
  } catch (e) {
    console.error("Error implementing business logic", e);
  }

  fs.writeFileSync(filePath, JSON.stringify(lastTime.end));
};

const queryHandyAPI = async function (url) {
  const response = await fetch(url, {
    method: "GET",
    headers: {
      Authorization: `Bearer ${process.env.HANDY_BEARER_TOKEN}`,
      "Content-Type": "application/json",
    },
  });

  if (response.status === 429) {
    console.log("Rate limit reached. Waiting for 60 seconds.");
    await sleep(60000);
    return queryHandyAPI(url);
  }

  if (response.status === 200) {
    return response.json();
  } else {
    console.log("Error: " + response.status);
    return null;
  }
};

const saveExternalIdOnSalesOrder = async function (salesOrderId, externalId) {
  const url = `https://app.handy.la/api/v2/salesOrder/${salesOrderId}`;
  const response = await fetch(url, {
    method: "PUT",
    headers: {
      Authorization: `Bearer ${process.env.HANDY_BEARER_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ externalId }),
  });

  if (response.status === 200) {
    console.log(
      `Saved externalId ${externalId} on sales order ${salesOrderId}`
    );
  } else {
    console.log(
      `Error saving externalId ${externalId} on sales order ${salesOrderId}`
    );
  }
};

const sleep = function (ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
};

new CronJob(
  cronExpression,
  jobFunction,
  null,
  true,
  "America/Mexico_City",
  this,
  true
);
console.log("Started cron job");