Guía de uso — APIs del Agente Vendedor Transaccional (`server/api/externalAccess`)

Complemento de APIs del Agente Vendedor Transaccional (que trae el código completo).
Este documento explica para qué sirve cada endpoint y cómo se usa: método, ruta, parámetros y forma de la respuesta. No repite el código, solo el comportamiento.

Última revisión contra el código: 2026-10-12 (rama fixes). Si existe versión v2 de un
endpoint, la v1 ya no se usa; aquí sólo se documenta la v2 (ver
Endpoints obsoletos).

Todas las rutas cuelgan de /apiCrm/externalAccess (en esta guía se omite ese prefijo).

Autenticación (aplica a todos los endpoints)

Todas las rutas exigen un header:

api_access_token: <id del documento externalAccess en Mongo>

validateToken resuelve ese token a un crm (conexión Firebird/MSSQL del cliente) y a
externalAccess.alias (el usuario Terra con el que se opera). Errores posibles:
"El token proporcionado no es válido", "El token está deshabilitado",
"Se ha superado la vigencia del token" (vigente hasta el final del día de valid_until),
"La conexión a la base de datos no se pudo recuperar".

En los endpoints GET de imagen/galería, en vez del header se puede mandar ?token=... en
el query string (útil para usarlos directo como <img src="..."> en HTML).

Formato de errores

  • Errores de validación y de negocio → HTTP 200 con:
    { "error": true, "message": "texto del error" }
    
  • Excepciones inesperadas (error de SQL, conexión, etc.) → también HTTP 200 con
    { "error": true, "message": "<stack completo del error>" }.
  • Excepción: createDocto responde HTTP 500 cuando el ERP rechaza el documento o hay
    timeout del envío (ver ese endpoint).

El bot siempre debe revisar error === true; no basta con el código HTTP.


Índice

  1. Carrito y pedido
  2. Catálogo
  3. Clientes y direcciones
  4. Consultas para el contacto (saldo, existencias, tracking)
  5. PDF de cotización
  6. Multimedia
  7. Utilidades / depuración
  8. Endpoints obsoletos (no usar)
  9. Puntos a cuidar

Carrito y pedido

El “carrito” es una cotización de Terra (DCDOCTOS_PDV, TIPO_DOCTO = 'C'). Su id es el
DOCTO_PV_ID. Sólo se puede modificar mientras su estatus sea N (Normal/pendiente).

Flujo típico:
getIdCart → (createCart o cart/:id/add|update|remove) → getCart → cart/:id/observations
→ createDocto (envía al ERP) → statusCart / getPDF.

Obtener carritos abiertos de un cliente

POST /accessToken/api/Daiko/v2/getIdCart/:id

A pesar del nombre, :id es el CLIENTE_ID, no un id de carrito. Busca ese cliente
(o prospecto si no existe como cliente) y devuelve sus datos junto con los carritos que
tenga abiertos (cotizaciones con ESTATUS = 'N').

  • Path: id — CLIENTE_ID (o PROSPECTO_ID).
  • Body: sin parámetros.
  • Respuesta 200:
    {
      "NOMBRE_CLIENTE": "...",
      "MONEDA_ID": 1,
      "CARRITOS_ID": [316, 317],
      "CARRITOS": [{ "CARRITO_ID": 316, "FOLIO": "COT001234" }]
    }
    
    Si no tiene carritos abiertos: "CARRITOS_ID": null y "CARRITOS": [].
  • Errores: "No existe cliente. Proporciona uno válido" si no se encuentra ni como
    cliente ni como prospecto.

Crear carrito (nueva cotización)

POST /accessToken/api/Daiko/v2/createCart/:id

Crea una cotización nueva (tipo C, estatus N) para el cliente :id, con los productos
indicados.

  • Path: id — CLIENTE_ID (también funciona con PROSPECTO_ID).
  • Body:
    Campo Tipo Requerido Notas
    almacen_id int Sí
    vendedor_id int Sí
    moneda_id int Sí
    productos array Sí [{ articulo_id, unidades }]. unidades default 1 si se omite.
    dir_consig_id int No Dirección de envío (de getAddresses/createAddress). Si no se manda, se usa la dirección de facturación/principal del cliente.
  • Valida que todos los articulo_id existan antes de crear nada.
  • Lo que calcula el servidor (no lo manda Daiko):
    • Precio de cada partida: lista de precios del cliente, precio del ERP (SAE/MyBusiness) y
      políticas de precio.
    • Descuento de cada partida: si el cliente tiene descuento propio (DESCUENTO_CLIENTE),
      ese aplica a todo; si no, la política de descuento por artículo (GET_DSCTO_ART). Si
      Daiko manda un descuento, se ignora.
    • El descuento del cliente también se guarda en el encabezado (en SAE se envía como
      descuento total del pedido).
    • Impuestos según el artículo y la configuración del cliente.
    • Folio con la serie del usuario del token; condición de pago predeterminada; uso de CFDI
      del cliente; fecha de hoy.
  • Respuesta 200:
    {
      "error": false, "created": true,
      "cliente_id": "316", "carrito_creado": 1026408, "folio": "COT001234",
      "createOrders": [{ "message": "Artículo 12846 agregado al carrito exitosamente." }]
    }
    
  • Errores: "No se encontró el artículo X", "El Folio: X ya existe.", errores de
    configuración de folios del usuario, parámetros faltantes.

Obtener carrito

POST /accessToken/api/Daiko/v2/getCart/:id

  • Path: id — DOCTO_PV_ID (id del carrito/cotización).
  • Body: sin parámetros.
  • Respuesta 200:
    {
      "importeCarrito": {
        "ID_CARRITO": 1026408, "ESTATUS": "N", "TIPO_DOCTO": "C",
        "IMPORTE_NETO": 1000, "TOTAL_IMPUESTOS": 160, "TOTAL_RETENCIONES": 0,
        "TOTAL_CARRITO": 1160, "NOMBRE_CLIENTE": "...", "FOLIO": "COT001234",
        "FECHA": "...", "VENDEDOR_ID": 12, "DSCTO_EXTRA": 0, "MONEDA_ID": 1,
        "CLIENTE_ID": 316, "OBSERVACIONES_ENC": "", "OBSERVACIONES": ""
      },
      "Carrito": [
        { "ARTICULO_ID": 12846, "NOMBRE": "...", "UNIDADES": 2, "PRECIO_UNITARIO": 500,
          "PCTJE_IMPUESTO": 16, "MONTO_IMPUESTO": 160, "COSTO_ENVIO": 0, "ESTATUS": "A",
          "DESCUENTO": 0, "POSICION": 1, "PARTIDA": "1",
          "PRECIO_UNITARIO_IMPTO": 580, "PRECIOTOTALARTICULOS": 1160 }
      ]
    }
    
    • PCTJE_IMPUESTO ya viene neto de retenciones (impuesto − retención).
    • DESCUENTO es porcentaje; PRECIOTOTALARTICULOS = (precio × unidades − descuento) + impuesto.
    • TOTAL_CARRITO = IMPORTE_NETO + TOTAL_IMPUESTOS − TOTAL_RETENCIONES.
  • Errores: "Carrito no existe."

Agregar / actualizar / eliminar artículo del carrito

POST /accessToken/api/Daiko/v2/cart/:id/:action

  • Path: id — DOCTO_PV_ID del carrito. action — uno de add, update, remove.

  • Body (una partida):

    Campo Requerido Notas
    articulo_id Sí Numérico (no como texto, ver Puntos a cuidar).
    unidades Sí, salvo en remove Numérico. En update debe ser > 0.

    También acepta varias partidas a la vez: { "productos": [ {...}, {...} ] }.
    La partida se localiza por articulo_id; no hace falta mandar DOCTO_PV_DET_ID.

  • Comportamiento por acción:

    • add: si el artículo ya está en el carrito, suma las unidades a las que ya tenía; si no
      está, lo agrega al final.
    • update: reemplaza las unidades de la partida. Error si el artículo no está en el carrito.
    • remove: quita la partida y recorre las posiciones. Error si el artículo no está en el carrito.
    • Si tras la operación unidades <= 0, la partida se quita.
    • En add/update se recalculan precio y descuento de las partidas tocadas, con las mismas
      reglas que createCart; después se recalculan los totales del carrito.
  • Respuesta 200: { "error": false, "created": true, "message": "Carrito actualizado." }

  • Errores: "Parámetro 'x' no válido." (acción inválida),
    "Artículo id X no existe en el carrito.", "El parámetro unidades no es válido.",
    "Carrito no existe.", "La cotización se encuentra <estatus>." (cualquier estatus distinto de N).

Actualizar observaciones del carrito

POST /accessToken/api/Daiko/v2/cart/:id/observations

Guarda el texto en OBSERVACIONES_ENC (observaciones del encabezado). No valida el estatus del carrito.

  • Path: id — DOCTO_PV_ID.
  • Body: { "observaciones": "texto libre" } (requerido; puede ser cadena vacía para borrar).
  • Respuesta 200: { "success": true, "message": "Observaciones actualizadas correctamente" }
  • Errores: "No existe el carrito indicado."

Estatus del carrito

POST /accessToken/api/Daiko/v2/statusCart/:id

Consulta de solo lectura.

  • Path: id — DOCTO_PV_ID.
  • Respuesta 200:
    { "status": "OK", "data": [{ "TIPO_DOCTO": "Cotizacion", "ESTATUS_ORDEN": "Pendiente",
      "CLIENTE_ID": 316, "DOCTO_PV_ID": 1026408, "ESTATUS_PAGO": "" }] }
    
    data es un arreglo de un elemento. TIPO_DOCTO se traduce a texto
    (Cotizacion/Pedido/Remision/Facturado); ESTATUS_ORDEN usa el catálogo
    Utils.statusQuotations en forma femenina (ej. “Pendiente”, “Ganada”, “Cancelada”).
  • Errores: "No existe Órden."

Cambiar estatus del carrito

POST /accessToken/api/Daiko/v2/setStatusCart/:id

  • Path: id — DOCTO_PV_ID.
  • Body: { "estatus": "N" } — debe ser un código válido de Utils.statusQuotations.
  • No valida el estatus actual (puede reabrir un carrito cancelado o ganado).
  • Respuesta 200: { "status": "OK", "data": "1026408" }
  • Errores: "No existe Órden.", "El estatus no es válido."

Eliminar carrito

POST /accessToken/api/Daiko/v2/deleteCart/:id

En realidad cancela (no borra el registro): pone ESTATUS = 'C' y registra usuario/fecha
de cancelación.

  • Path: id — DOCTO_PV_ID.
  • Respuesta 200: { "status": "OK", "data": "1026408" }
  • Errores: "Carrito no existe.", "El Carrito se encuentra ganado." (ya se convirtió en
    pedido), "El Carrito se encuentra cancelado." (ya estaba cancelado).

Generar pedido desde una cotización

POST /accessToken/api/Daiko/v2/createDocto/:id

El endpoint más pesado: toma la cotización :id y la envía al ERP. El documento destino
depende de MODO_USO: Microsip → Pedido (P), SAE → Pedido, ContPAQ → documento 2 (Pedido),
MyBusiness POS → Pedido (PE).

  • Path: id — DOCTO_PV_ID de la cotización ya armada.

  • Body:

    Campo Requerido Notas
    cliente_id Sí Con celular, valida que el contacto tenga el permiso GENERAR_PEDIDOS.
    celular Sí Celular del contacto que pide el pedido (CONTACTO_PERMISOS).
    confirm_prices No Mandar true en el reintento si el usuario aceptó los cambios de precio.
    confirm_components No Mandar true en el reintento si el usuario aceptó componentes de kit con precio $0.
  • Validaciones, en orden:

    1. Permiso GENERAR_PEDIDOS del contacto.
    2. Que el cliente exista. Si es prospecto y el CRM es MyBusiness POS (MODO_USO = 4) con el
      parámetro CONVERTIR_PROSPECTO_PEDIDO = 'S', lo convierte a cliente automáticamente.
    3. Cliente no suspendido, sin saldo vencido y dentro de su límite de crédito.
    4. SAE/ContPAQ: uso de CFDI y forma de pago SAT (de la cotización o, si faltan, del cliente).
    5. Artículos y componentes catalogados.
    6. Existencias, sólo si el usuario del token tiene la preferencia SURTIR_SOLO_EXISTENCIAS.
    7. Precios vigentes, sólo si tiene la preferencia VALIDAR_PRECIOS.
    8. Componentes de kits con precio 0.
    9. ContPAQ: características de los artículos.
  • Respuesta 200 (éxito): el resultado que regresa el ERP (incluye FOLIO_DESTINO,
    TIPO_DOCTO_DESTINO, COMPLETAR, entre otros).

  • Respuestas de “necesita confirmación” (HTTP 200; no son error definitivo, hay que reintentar):

    • { "error": true, "confirm_prices": "Los siguientes articulos cambiaron de precio:\n...\n¿Desea actualizar los precios y continuar con el envío?" }
    • { "error": true, "confirm_components": "Las siguientes partidas contienen componentes con precio 0:\n...\n¿Deseas continuar con el envío?" }

    En ambos casos se reintenta el mismo POST agregando el flag correspondiente en true. Si
    se acepta confirm_prices, el pedido se envía con los precios actuales.

  • Errores (HTTP 200, { error: true, message }): permiso no autorizado, cliente/prospecto no
    encontrado, cliente suspendido/con saldo vencido/sin crédito, falta uso de CFDI o forma de pago
    SAT, artículo no catalogado, sin existencias, timeout de preprocesamiento (60 s).

  • Errores del ERP (HTTP 500): si el ERP rechaza el documento o no responde en 90 s, la
    respuesta es HTTP 500 con el texto del error como cuerpo (no viene en { error, message }).

  • Si el bot corta la conexión por su propio timeout, el servidor deja de procesar; conviene
    consultar statusCart antes de reintentar para no duplicar el pedido.


Catálogo

Buscar/listar productos

POST /accessToken/api/Daiko/v1/getProducts

También sirven getProductSearch/:keyword, getProductByCategory/:category,
getProductByLabels/ y getProductByKey: es la misma función y cambia según lo que venga en
la ruta o el body. No hay versión v2.

  • Body:
    Campo Requerido Uso
    cliente_id Sí Para calcular precio, descuento e impuestos del cliente.
    moneda_id Sí Moneda en la que se calcula el precio.
    clave No Busca por clave de artículo (contiene). Si viene, se ignoran los demás filtros.
    query (o path :keyword) No Búsqueda por nombre que empieza con el texto. Si no encuentra nada, reintenta con “contiene”.
    categoria (o path :category) No Filtra por línea de artículo (contiene).
    etiquetas No Array de textos; busca en los valores de los clasificadores del artículo. Requiere que la base tenga clasificadores (CAPTURA_CLASIFICADORES).
    filtros No { marca, tipo, medida, caracteristicas, compatibilidad }, cada uno array de textos. Se combinan con AND sobre el nombre. Si no hay resultados, se van quitando uno a uno.
    existencia No true → sólo artículos con existencia > 0 en almacen_id. Default false.
    almacen_id Sí si existencia: true Almacén donde se revisa la existencia.
    per_page No Activa la paginación (tamaño de página). Va en la raíz del body.
    current_page No Página (default 1). Va en la raíz del body.
  • Con query y sin categoria/etiquetas, el mismo texto también se busca como línea de
    artículo y como etiquetas (separado por espacios); los resultados se combinan con OR.
  • Sólo devuelve artículos activos (ESTATUS = 'A'), ordenados por nombre.
  • Respuesta 200 (sin paginación):
    { "productos": [ { "ARTICULO_ID": 12846, "NOMBRE": "...", "PRECIO": 500,
        "PCTJE_IMPUESTO": 16, "PCTJE_RET": 0, "MONTO_IMPUESTO": 80, "IMPUESTO_ID": 3,
        "CLAVE": "ABC-1", "UNIDAD_VENTA": "PZA", "NOMBRE_PRECIO": "Lista 1",
        "ES_JUEGO": "N", "ES_ALMACENABLE": "S", "ROL": "N", "ES_CATALOGADO": "S" } ] }
    
  • Respuesta 200 (con per_page): { "meta": { "count", "count1", "current_page", "page_size" }, "data": [...] }
    (count = total que cumple el filtro; count1 = los que vienen en esta página).
  • Errores: "La base de datos no permite búsqueda por etiquetas" (sólo etiquetas y la base
    no tiene clasificadores), "El parámetro almacen_id no ha sido indicado.".

Obtener un producto por id

POST /accessToken/api/Daiko/v1/getProduct/:id

  • Path: id — ARTICULO_ID.
  • Body: cliente_id y moneda_id requeridos (mismo uso que arriba).
  • Respuesta 200: el artículo con NOTAS_VENTAS y precio/impuesto ya calculados para ese
    cliente y moneda (no filtra por estatus).
  • Errores: "No existe el producto. Proporciona uno válido".

Listado de categorías

POST /accessToken/api/Daiko/v1/getListCategory

  • Body: opcional per_page / current_page en la raíz para paginar.
  • Respuesta 200: { "Categorias": [ { "LINEA_ARTICULO_ID", "NOMBRE" }, ... ] }. Con paginación,
    Categorias trae { meta, data }.

Clientes y direcciones

Obtener clientes

POST /accessToken/api/Daiko/v1/getCustomers

  • Body (ambos opcionales):
    • cliente_id — busca ese cliente exacto, y también un prospecto con ese mismo id.
    • name — búsqueda por nombre (contiene, sin distinguir mayúsculas), en clientes y prospectos.
    • Si no se manda ninguno de los dos, regresa TODOS los clientes (sin prospectos). Usar
      con cuidado en catálogos grandes.
  • Respuesta 200: arreglo [{ "CLIENTE_ID", "NOMBRE", "MONEDA_ID", "VENDEDOR_ID", "CLI_PRO": "C" }, ...]
    (los prospectos vienen con "CLI_PRO": "P").

Estatus de un cliente

POST /accessToken/api/Daiko/v1/getstatusCustomers/:id

  • Path: id — CLIENTE_ID.
  • Respuesta 200: { "clienteDetalle": [{ "CLIENTE_ID", "NOMBRE", "MONEDA_ID", "COND_PAGO_ID", "TIPO_CLIENTE_ID", "VENDEDOR_ID", "NOTAS" }] }
  • Errores: "Cliente no existe." (sólo busca clientes, no prospectos).

Crear prospecto

POST /accessToken/api/Daiko/v1/createProspecto

  • Body:
    Campo Requerido
    nombre Sí
    nombre_contacto Sí
    celular Sí — queda como celular del contacto principal
    telefono_oficina No
    clave No — clave de cliente, si se quiere fijar una en particular
    permisos_default No — si es true y el CRM está en MODO_USO = 4, asigna al contacto todos los permisos (GENERAR_PEDIDOS, VER_EXISTENCIAS, VER_SALDO, ESTATUS_PEDIDO)
  • Usa los catálogos predeterminados del CRM (tipo de cliente, vendedor, moneda, zona, tipo de contacto).
  • Respuesta 200: { "message": "Prospecto creado exitosamente.", "PROSPECTO_ID": ... }

Obtener direcciones del cliente

POST /accessToken/api/Daiko/v1/getAddresses

  • Body: cliente_id requerido.
  • Respuesta 200: { "error": false, "data": [ { "DIR_CLI_ID", "NOMBRE", "ES_DEFAULT", "VIA_EMBARQUE", "VIA_EMBARQUE_ID" }, ... ] } (máximo 1000).

Crear dirección

POST /accessToken/api/Daiko/v1/createAddress

  • Body: cliente_id y nombre_consig requeridos; además cualquier campo de dirección
    que use apiCrm/addresses (calle, num_ext, num_int, colonia, estado, ciudad,
    poblacion, codigo_postal, etc.). Los nombres se pasan a mayúsculas y, si no existen, se
    crean los catálogos de país (México), estado, ciudad, municipio y código postal.
  • Delega el guardado en apiCrm/addresses/controller.js (save).
  • Respuesta 200: { "error": false, "data": { "DIR_CLI_ID": ... } }

Invitados (sin uso en el flujo actual)

  • POST /accessToken/api/Daiko/v1/getGuest — devuelve el cliente con NOMBRE = 'Invitado'
    ({ "CLIENTE_ID", "NOMBRE" }), o respuesta vacía si no existe.
  • POST /accessToken/api/Daiko/v1/getIdGuest — :warning: crea un cliente nuevo en cada llamada
    (nombre fijo 'Juan'). Es código de prueba; no usar en producción.

Consultas para el contacto

Los tres endpoints validan que el celular pertenezca a un contacto del cliente con el permiso
indicado (CONTACTO_PERMISOS). Si no lo tiene, responden un error de permiso.

Saldo vencido del cliente

POST /accessToken/api/Daiko/v1/getBalanceDue

  • Body: cliente_id, celular requeridos. Permiso: VER_SALDO.
  • Respuesta 200: datos de saldo vencido (Utils.getBalanceDue).

Existencia de un artículo

POST /accessToken/api/Daiko/v1/getStockArticle

  • Body: cliente_id, celular, articulo_id requeridos. Permiso: VER_EXISTENCIAS.
  • Respuesta 200: existencia del artículo (usa el almacén del ERP si crm.system >= 2).

Seguimiento/tracking de un pedido

POST /accessToken/api/Daiko/v1/getTrackingPedido

  • Body: cliente_id, celular, folio requeridos. Permiso: ESTATUS_PEDIDO.
  • folio es el folio de la cotización (DCDOCTOS_PDV.FOLIO).
  • Respuesta 200: cadena de documentos de la cotización (Utils.getTracking).
  • Errores: "El folio proporcionado no se encuentra registrado."

PDF de cotización

Obtener PDF

POST /accessToken/api/Daiko/v1/getPDF

Genera el PDF de una cotización con el formato que la cuenta marcó para Daiko
(FORMATOS_REPORTES.ES_DAIKO = 'S'), igual al PDF de crmzeus (imágenes, márgenes, condición de
pago, cantidad en letra). No hay versión v2.

  • Body: { "carrito_id": 1026408 } (DOCTO_PV_ID) — requerido.
  • Respuesta 200:
    { "content": "<PDF en base64>", "name": "Cotizacion 1026408.pdf" }
    
    content viene sin el prefijo data:application/pdf;base64,.
  • Sin formato marcado:
    { "error": true, "code": "NO_FORMATO_DAIKO", "message": "No hay un formato de cotización marcado para el Agente vendedor transaccional (Daiko)." }
    En ese caso el bot debe generar su propio PDF (no hay formato de respaldo).

Multimedia

Imagen de un artículo

GET /accessToken/api/Daiko/v1/image/:articulo_id

  • Path: articulo_id.
  • Query: ?token=... como alternativa al header api_access_token (para usarlo directo en
    un <img src>).
  • Respuesta 200: la imagen principal en binario (Content-Type: image/jpeg, cache 24 h), o
    un SVG “SIN IMAGEN” (cache 1 h) si el artículo no tiene imagen o pesa más de ~300 KB.

Galería HTML de productos

GET /accessToken/api/Daiko/v1/gallery

Para mandarle al usuario un link que abre una página con tarjetas de producto (imagen + nombre

  • precio opcional), en vez de mandar imágenes sueltas por chat.
  • Query:
    Campo Requerido Notas
    ids Sí Lista de ARTICULO_ID separados por coma.
    token Sí (o header) Se reenvía a cada <img> de la galería.
    cliente_id + moneda_id No Si se mandan ambos, cada tarjeta incluye el precio de lista del cliente (sin políticas ni descuentos).
  • Respuesta 200: HTML completo (Content-Type: text/html), no JSON.
  • Errores: "El parámetro ids es requerido.", "No se proporcionaron IDs válidos."

Utilidades / depuración

Ejecutar SQL directo

POST /accessToken/api/Daiko/v1/SQL

:warning: Endpoint sensible. Ejecuta tal cual el SQL recibido contra la conexión del CRM, sin
filtro. Pensado para depuración; no debería estar disponible para el bot en producción.

  • Body:
    Campo Requerido Notas
    query Sí SQL a ejecutar tal cual.
    db No '' (Firebird Terra, default), '2' (Firebird Microsip) o '3' (BD del ERP).
  • Respuesta 200: { "data": [...] }

Endpoints obsoletos (no usar)

Tienen versión v2 y la v1 ya no se usa. Siguen ruteados, pero trabajan sobre los documentos de
Microsip (DOCTOS_VE) y no sobre las cotizaciones de Terra.

v1 (no usar) Usar
v1/getIdCart/:id v2/getIdCart/:id
v1/createCart/:id v2/createCart/:id
v1/getCart/:id v2/getCart/:id
v1/cart/:id/add, v1/cart/:id/update, v1/cart/:id/remove v2/cart/:id/:action
v1/statusCart/:id v2/statusCart/:id
v1/setStatusCart/:id v2/setStatusCart/:id
v1/deleteCart/:id v2/deleteCart/:id
v1/createDocto/:id (pedía tipo_docto) v2/createDocto/:id

Los demás endpoints v1 de esta guía (productos, clientes, direcciones, consultas, PDF,
imágenes) siguen vigentes porque no tienen v2.


Puntos a cuidar

Comportamientos del código actual que conviene conocer (no son cambios hechos en este documento):

  1. articulo_id como número en cart/:id/:action. La partida se busca con comparación
    estricta; si llega "12846" (texto), update/remove responden “no existe en el carrito” y
    add lo agrega como partida nueva en vez de sumar.
  2. unidades como número en add. Si el artículo ya está y unidades llega como texto, la
    suma se vuelve concatenación ("2" + 3 = "23").
  3. Paginación en la raíz. getProducts y getListCategory leen per_page/current_page del
    body raíz; un objeto pagination: {...} se ignora.
  4. existencia: true filtra después de paginar. Una página puede traer menos de per_page
    y meta.count incluye artículos sin existencia.
  5. Token inválido en createCart. La conexión a la base se abre antes de revisar el resultado
    del token; con un token inválido la respuesta puede ser un error genérico en vez de
    “El token proporcionado no es válido”.
  6. setStatusCart no restringe transiciones (puede reabrir cancelados o ganados), y
    cart/:id/observations no valida el estatus del carrito.
  7. Errores con stack completo. Las excepciones devuelven el stack del servidor en message;
    el bot no debería mostrarlo tal cual al usuario.