nexusflex-mcp 3.69.1 → 3.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api.mjs +13 -2
- package/docs.mjs +508 -510
- package/package.json +1 -1
- package/server.mjs +49 -48
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexusflex-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.71.0",
|
|
4
4
|
"description": "MCP de Nexus Flex: operá tu nodo/cuenta desde un asistente de IA (altas de clientes, stock, KPIs). Login por autorización web (device-flow). NO toca dinero.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/server.mjs
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// ============================================================================
|
|
26
26
|
import { runDeviceFlow, saveToken, clearToken, tokenFilePath } from "./device-auth.mjs";
|
|
27
27
|
import { api, log, API_URL } from "./api.mjs";
|
|
28
|
-
import { renderAyuda, guiaOnboarding, renderFlujo } from "./docs.mjs";
|
|
28
|
+
import { renderAyuda, guiaOnboarding, renderFlujo, FLUJOS } from "./docs.mjs";
|
|
29
29
|
|
|
30
30
|
// --- Subcomandos de línea de comando (login/logout) antes de arrancar el server ---
|
|
31
31
|
const cmd = process.argv[2];
|
|
@@ -56,8 +56,10 @@ const ALLOW_PRECIOS = truthy(process.env.NEXUSFLEX_MCP_ALLOW_PRECIOS);
|
|
|
56
56
|
|
|
57
57
|
// ATADO CON: backend/src/services/mcp-remote.ts (MCP_VERSION + NOVEDADES + mismos tools, salvo SOLO_REMOTO)
|
|
58
58
|
// y mcp/package.json. Lo verifica backend/src/coherencia.test.ts. Ver CLAUDE.md → "Cosas que van juntas".
|
|
59
|
-
const MCP_VERSION = "3.
|
|
59
|
+
const MCP_VERSION = "3.71.0"; // bumpear junto con package.json + backend/services/mcp-remote.ts
|
|
60
60
|
const NOVEDADES = [
|
|
61
|
+
"3.71.0 — Habilitadas por el dueño: `liquidacion_excluir_envio` / `liquidacion_reincluir_envio` (sacar o devolver UN envío del cobro de una liquidación emitida, con motivo obligatorio) y `facturacion_estado` / `facturacion_preparar_cliente` (qué falta para facturar y cargar los datos fiscales de un cliente) ya funcionan por MCP: el filtro de dinero las deja pasar con su método exacto. Siguen sin pagar, cobrar ni emitir comprobantes, y piden permiso finanzas. Además `mis_envios` ahora sí filtra por `estado`.",
|
|
62
|
+
"3.70.0 — Descripciones al día: textos de las herramientas corregidos para que digan lo que hacen hoy (`envio_entregar`, `envio_asignar_mensajero`, `generar_enlace_vinculacion` vence en 48 hs, `grupo_miembros` y `provisorio_conciliar` apuntan a `grupos_tarifas`), `flujo` lista todos los flujos disponibles, `sugerencias_listar` solo para el superadmin, `envio_cargar` exige `telefono`, parámetros con formato (fechas YYYY-MM-DD, montos en ARS, nombre o id) y las instrucciones del conector más cortas (la guía sigue en `guia`). `liquidacion_excluir_envio`/`reincluir` avisan que hoy el filtro de dinero los bloquea por MCP (se hace desde la PWA).",
|
|
61
63
|
"3.69.1 — AYUDA completa: `ayuda` ahora tiene la ficha (qué hace, cómo usarlo, ejemplo, qué NO hace) de los 20 tools que no la tenían (cierre semanal, zonas de grupo, tarifas de grupo, sin vendedor, corregir estado, pago al cadete, retiros…) y los 26 que no aparecían en el índice. La ayuda del paquete npm se genera desde la del conector, así que ya no se desincronizan. No mueve dinero.",
|
|
62
64
|
"3.69.0 — Control por foto de la TANDA DE UN CADETE (propios + de otros nodos mezclados): en `envio_desde_etiqueta_ml` alcanza con el `mlQr` crudo — de ahí sale el número de ML y el vendedor, y el vendedor se reconoce igual que en el escáner (cuenta vinculada O cuenta aprendida en juntadas/escaneos; antes solo la vinculada). `nodoEntrega` ya no necesita `grupo`: si el cliente es del mismo nodo que entrega es un paquete propio (sin traspaso) y si es de otro nodo se usa el grupo que comparten. Si no conoce al vendedor, contesta 404 con el senderId para que preguntes de qué nodo/cliente es. Si el envío ya existía, no lo duplica: lo completa y lo avanza. No mueve dinero.",
|
|
63
65
|
"3.68.0 — Control por foto: `envio_desde_etiqueta_ml` acepta `nodoEntrega` (nombre o id de un nodo del `grupo`) = el nodo que RECIBIÓ y entregó el paquete. Con `fecha` + `grupo` + `nodoEntrega` una planilla atrasada entra al CIERRE SEMANAL entre nodos en su día real (ej. 5 envíos de un cliente de Envíos Frank que entregó FastCorreo el lunes 14: fecha 14/09, grupo Portela, nodoEntrega FastCorreo). Antes solo se ponía con `autoRutear` y si la zona tenía un único responsable; si no, quedaba fuera del cierre. Si el envío ya tenía OTRO nodo que entrega no se pisa (avisa). Si la semana ya cerró, entra en el cierre siguiente marcado como tarde, con su fecha real. No mueve dinero.",
|
|
@@ -126,7 +128,7 @@ const NOVEDADES = [
|
|
|
126
128
|
"3.4.0 — Marketplace de rutas públicas (ruta_publicar/ofertar/adjudicar); admin también reparte; registrar envío desde foto de etiqueta ML; alta/edición de choferes; mcp_version.",
|
|
127
129
|
"3.3.0 — Afiliados y comisión por envío; métricas por tipo; gestión de colectas (asignar/rechazar entre nodos); cuentas vinculadas; sucursales; colecta a pedido; metazonas flexibles.",
|
|
128
130
|
];
|
|
129
|
-
const INSTRUCCIONES = "MCP de Nexus Flex.
|
|
131
|
+
const INSTRUCCIONES = "MCP de Nexus Flex. Si te preguntan la versión, consultá `mcp_version` (este texto puede ser anterior a una actualización). Para arrancar: `guia` (cómo trabajar según tu rol) y `ayuda` (índice de herramientas). El MCP NUNCA mueve dinero.\n\nCuando el usuario quiera completar un alta o una carga que tiene flujo guiado (ver `flujo`), traé el paso a paso y completalo entero antes de darlo por terminado: los pasos siguientes (lista de precios, perfil de zona, colecta) son los que se olvidan y dejan el alta a medias.";
|
|
130
132
|
const server = new McpServer({ name: "nexusflex", version: MCP_VERSION }, { instructions: INSTRUCCIONES });
|
|
131
133
|
|
|
132
134
|
/** Formatea un ApiResult como respuesta de tool MCP, con mensajes claros y SIN
|
|
@@ -225,8 +227,8 @@ tool("guia", {
|
|
|
225
227
|
// Flujo guiado: paso a paso de un objetivo para no dejar nada incompleto (Fase A).
|
|
226
228
|
tool("flujo", {
|
|
227
229
|
title: "Flujo guiado paso a paso",
|
|
228
|
-
description: "Te devuelve el PASO A PASO para completar bien un objetivo, sin dejar nada a medias (ej. dar de alta un cliente con su lista de precios, o el CONTROL POR FOTO de una carpeta de etiquetas ML). Sin argumento lista los flujos disponibles; pasá `objetivo` (
|
|
229
|
-
inputSchema: { objetivo: z.string().optional().describe(
|
|
230
|
+
description: "Te devuelve el PASO A PASO para completar bien un objetivo, sin dejar nada a medias (ej. dar de alta un cliente con su lista de precios, o el CONTROL POR FOTO de una carpeta de etiquetas ML). Sin argumento lista los flujos disponibles; pasá `objetivo` (una de las claves de abajo). Usalo para GUIAR al usuario que no sabe qué datos faltan.",
|
|
231
|
+
inputSchema: { objetivo: z.string().optional().describe(`Vacío = lista los flujos disponibles. Claves: ${Object.keys(FLUJOS).join(", ")}.`) },
|
|
230
232
|
}, async ({ objetivo }) => ({ content: [{ type: "text", text: renderFlujo(objetivo) }] }));
|
|
231
233
|
|
|
232
234
|
// Sugerencias: cualquier usuario (todos los roles) puede proponer funciones/mejoras.
|
|
@@ -242,7 +244,7 @@ tool("mis_sugerencias", {
|
|
|
242
244
|
}, async () => run(() => api("GET", "/feedback/mis-sugerencias")));
|
|
243
245
|
tool("flujo_registrar", {
|
|
244
246
|
title: "Registrar un flujo aprendido (indagación)",
|
|
245
|
-
description: "
|
|
247
|
+
description: "Usalo cuando, tras varias preguntas de aclaración, se entendió lo que el usuario quería: registra el objetivo y los pasos para convertirlo en un flujo directo. Avisale al usuario que lo registrás. Registrás `objetivo` (lo que terminó queriendo) y `pasos` (las preguntas/decisiones que lo aclararon + herramientas usadas). No mueve dinero.",
|
|
246
248
|
inputSchema: { objetivo: z.string().min(1), pasos: z.string().min(1) },
|
|
247
249
|
}, async ({ objetivo, pasos }) => run(() => api("POST", "/feedback/sugerencias", { categoria: "mejora", mensaje: `[FLUJO APRENDIDO] Objetivo: ${objetivo}\nPasos que lo aclararon: ${pasos}` })));
|
|
248
250
|
|
|
@@ -252,14 +254,14 @@ tool("flujo_registrar", {
|
|
|
252
254
|
if (isCliente) {
|
|
253
255
|
tool("mis_envios", {
|
|
254
256
|
title: "Mis envíos",
|
|
255
|
-
description: "Tus envíos/paquetes (solo los tuyos)
|
|
256
|
-
inputSchema: { estado: z.string().optional().describe("
|
|
257
|
+
description: "Tus envíos/paquetes (solo los tuyos), los 300 más recientes, cada uno con su `estado`. No incluye datos de otros clientes ni del nodo.",
|
|
258
|
+
inputSchema: { estado: z.string().optional().describe("Estado: 'A retirar', 'Colectado', 'En centro de distribución', 'En camino', 'Entregado' (o una novedad, ej. 'Comprador ausente'). Filtra por los que EMPIEZAN con ese texto ('Entregado' incluye 'Entregado (Flex)'); los 300 más recientes de ese estado.") },
|
|
257
259
|
}, async ({ estado }) => run(() => api("GET", `/portal/envios${q({ estado })}`)));
|
|
258
260
|
|
|
259
261
|
tool("mis_kpis", {
|
|
260
262
|
title: "Mis métricas",
|
|
261
263
|
description: "Tus métricas: volumen, calidad de entrega, saldo, última liquidación y un benchmark ANÓNIMO contra el promedio de tu nodo (nunca ves a quién corresponde cada número).",
|
|
262
|
-
inputSchema: { desde: z.string().optional(), hasta: z.string().optional() },
|
|
264
|
+
inputSchema: { desde: z.string().optional().describe("Fecha YYYY-MM-DD (pasá desde y hasta juntas; si falta una, usa el mes en curso)"), hasta: z.string().optional().describe("Fecha YYYY-MM-DD") },
|
|
263
265
|
}, async ({ desde, hasta }) => run(() => api("GET", `/kpi/vendedor${q({ desde, hasta })}`)));
|
|
264
266
|
|
|
265
267
|
// --- Control de stock del vendedor (WMS Fase 1). Se registran SIEMPRE para
|
|
@@ -333,7 +335,7 @@ if (isStaff) {
|
|
|
333
335
|
description: "Clientes/vendedores del nodo con su lista de precio asignada, más las listas disponibles. Scopeado a tu nodo.",
|
|
334
336
|
inputSchema: {},
|
|
335
337
|
}, async () => run(() => api("GET", "/gestion/formularios")));
|
|
336
|
-
tool("sin_vendedor", { title: "Cuentas de ML y envíos sin vendedor", description: "Cuentas de Mercado Libre y envíos cargados con foto que tu nodo tiene SIN VENDEDOR (típico de un nodo recién dado de alta, o de etiquetas de cuentas no vinculadas que se escanearon). Devuelve `cuentas` (por cuenta de ML: cuántos envíos, ejemplos de destinatarios, si ya está vinculada) y `envios` (los cargados con foto, con la pista de la etiqueta), y los `clientes` del nodo (con si tienen usuario). Después usá `asignar_sin_vendedor`. Admin global: pasá `nodo`. No mueve dinero.", inputSchema: { nodo:
|
|
338
|
+
tool("sin_vendedor", { title: "Cuentas de ML y envíos sin vendedor", description: "Cuentas de Mercado Libre y envíos cargados con foto que tu nodo tiene SIN VENDEDOR (típico de un nodo recién dado de alta, o de etiquetas de cuentas no vinculadas que se escanearon). Devuelve `cuentas` (por cuenta de ML: cuántos envíos, ejemplos de destinatarios, si ya está vinculada) y `envios` (los cargados con foto, con la pista de la etiqueta), y los `clientes` del nodo (con si tienen usuario). Después usá `asignar_sin_vendedor`. Admin global: pasá `nodo`. No mueve dinero.", inputSchema: { nodo: zNodo() } },
|
|
337
339
|
async ({ nodo }) => run(() => api("GET", `/sin-vendedor${q({ nodo })}`)));
|
|
338
340
|
if (ALLOW_WRITE)
|
|
339
341
|
tool("asignar_sin_vendedor", {
|
|
@@ -369,7 +371,7 @@ if (isStaff) {
|
|
|
369
371
|
tool("kpi_nodo", {
|
|
370
372
|
title: "Métricas del nodo",
|
|
371
373
|
description: "Tablero del nodo: volumen y entregas con variación mensual, P&L real, top de clientes y clientes en caída. Solo lectura, scopeado a tu nodo.",
|
|
372
|
-
inputSchema: { desde: z.string().optional()
|
|
374
|
+
inputSchema: { desde: z.string().optional().describe("Fecha YYYY-MM-DD (pasá desde y hasta juntas; si falta una, usa el mes en curso)"), hasta: z.string().optional().describe("Fecha YYYY-MM-DD"), nodo: zNodo() },
|
|
373
375
|
}, async ({ desde, hasta, nodo }) => run(() => api("GET", `/kpi/nodo${q({ desde, hasta, nodo })}`)));
|
|
374
376
|
}
|
|
375
377
|
|
|
@@ -377,19 +379,19 @@ if (isStaff) {
|
|
|
377
379
|
tool("stock_nodo", {
|
|
378
380
|
title: "Stock del nodo",
|
|
379
381
|
description: "Stock del depósito del nodo. Opcional: filtrar por un cliente.",
|
|
380
|
-
inputSchema: { cliente: z.string().optional().describe("
|
|
382
|
+
inputSchema: { cliente: z.string().optional().describe("Id del cliente de tu nodo (de `clientes_del_nodo`); no acepta nombre") },
|
|
381
383
|
}, async ({ cliente }) => run(() => api("GET", `/wms/stock${q({ cliente })}`)));
|
|
382
384
|
|
|
383
385
|
tool("productos_nodo", {
|
|
384
386
|
title: "Catálogo del nodo",
|
|
385
387
|
description: "Catálogo de productos del depósito del nodo. Opcional: filtrar por cliente.",
|
|
386
|
-
inputSchema: { cliente: z.string().optional() },
|
|
388
|
+
inputSchema: { cliente: z.string().optional().describe("Id del cliente de tu nodo (de `clientes_del_nodo`); no acepta nombre") },
|
|
387
389
|
}, async ({ cliente }) => run(() => api("GET", `/wms/productos${q({ cliente })}`)));
|
|
388
390
|
|
|
389
391
|
tool("top_productos", {
|
|
390
392
|
title: "Productos más despachados del nodo",
|
|
391
393
|
description: "Ranking de productos más despachados del nodo en un rango (default: mes en curso). Opcional: filtrar por un cliente.",
|
|
392
|
-
inputSchema: { desde: z.string().optional().describe("YYYY-MM-DD"), hasta: z.string().optional().describe("YYYY-MM-DD"), cliente: z.string().optional() },
|
|
394
|
+
inputSchema: { desde: z.string().optional().describe("YYYY-MM-DD"), hasta: z.string().optional().describe("YYYY-MM-DD"), cliente: z.string().optional().describe("Id del cliente de tu nodo (de `clientes_del_nodo`); no acepta nombre") },
|
|
393
395
|
}, async ({ desde, hasta, cliente }) => run(() => api("GET", `/wms/top-productos${q({ desde, hasta, cliente })}`)));
|
|
394
396
|
}
|
|
395
397
|
}
|
|
@@ -462,8 +464,14 @@ if (esGlobal) {
|
|
|
462
464
|
tool("logo_subir", {
|
|
463
465
|
title: "Subir logo / marca de un nodo (marca blanca)",
|
|
464
466
|
description: "Sube o actualiza el logo y la marca de un nodo (marca blanca). El logo va como data-URI base64 (ej. data:image/png;base64,...). Solo admin global. No toca dinero.",
|
|
465
|
-
inputSchema: { nodo: z.number().int().positive(), logo: z.string().min(1).describe("imagen en data-URI base64, ej. data:image/png;base64,..."), marca: z.string().optional(), slug: z.string().optional(), color: z.string().optional() },
|
|
467
|
+
inputSchema: { nodo: z.number().int().positive().describe("Id del nodo (de `nodos_listar`)"), logo: z.string().min(1).describe("imagen en data-URI base64, ej. data:image/png;base64,..."), marca: z.string().optional(), slug: z.string().optional(), color: z.string().optional() },
|
|
466
468
|
}, async ({ nodo, logo, marca, slug, color }) => run(() => api("PUT", `/logisticas/${nodo}/branding`, { logo, marca, slug, color })));
|
|
469
|
+
|
|
470
|
+
tool("sugerencias_listar", {
|
|
471
|
+
title: "Sugerencias de usuarios (admin)",
|
|
472
|
+
description: "Solo superadmin: todas las sugerencias de la red (funciones nuevas/mejoras/bugs) con usuario, rol, nodo, estado. Filtrá por `rol` y/o `estado`. Para las propias está `mis_sugerencias`. Solo lectura.",
|
|
473
|
+
inputSchema: { rol: z.string().optional(), estado: z.string().optional().describe("nueva | vista | en_evaluacion | implementada | descartada") },
|
|
474
|
+
}, async ({ rol, estado }) => run(() => api("GET", `/feedback/sugerencias${q({ rol, estado })}`)));
|
|
467
475
|
}
|
|
468
476
|
|
|
469
477
|
// ============================================================================
|
|
@@ -474,10 +482,10 @@ if (esGlobal) {
|
|
|
474
482
|
if (isCliente || (isStaff && puede("gestion"))) {
|
|
475
483
|
tool("generar_enlace_vinculacion", {
|
|
476
484
|
title: "Generar enlace de vinculación de tienda",
|
|
477
|
-
description: "Genera el enlace (link) para vincular una tienda —Mercado Libre, TiendaNube o Tienda Negocio— y traer sus ventas a Nexus Flex. Si sos vendedor genera el TUYO; si sos staff podés generarlo PARA un cliente de tu nodo pasando 'cliente'
|
|
485
|
+
description: "Genera el enlace (link) para vincular una tienda —Mercado Libre, TiendaNube o Tienda Negocio— y traer sus ventas a Nexus Flex. Si sos vendedor genera el TUYO; si sos staff podés generarlo PARA un cliente de tu nodo pasando 'cliente' y mandarle ese enlace para que lo autorice desde su propia cuenta de la tienda. El enlace vence en 48 hs. No toca dinero.",
|
|
478
486
|
inputSchema: {
|
|
479
487
|
proveedor: z.enum(["ml", "tiendanube", "tiendanegocio"]).describe("ml = Mercado Libre · tiendanube · tiendanegocio"),
|
|
480
|
-
cliente: z.string().optional().describe("Solo staff:
|
|
488
|
+
cliente: z.string().optional().describe("Solo staff: nombre o id del cliente de tu nodo"),
|
|
481
489
|
},
|
|
482
490
|
}, async ({ proveedor, cliente }) => run(() => api("GET", `/${proveedor}/auth${q({ cliente })}`)));
|
|
483
491
|
}
|
|
@@ -501,13 +509,6 @@ if (isCliente || isStaff) {
|
|
|
501
509
|
inputSchema: { estado: z.string().optional().describe("abierto | resuelto") },
|
|
502
510
|
}, async ({ estado }) => run(() => api("GET", `/feedback/reclamos${q({ estado })}`)));
|
|
503
511
|
}
|
|
504
|
-
if (isStaff) {
|
|
505
|
-
tool("sugerencias_listar", {
|
|
506
|
-
title: "Sugerencias de usuarios (admin)",
|
|
507
|
-
description: "Sugerencias entrantes de los usuarios (funciones nuevas/mejoras/bugs) con usuario, rol, nodo, estado. Filtrá por `rol` y/o `estado`. Operador: las de su nodo; admin global: todas. Solo lectura.",
|
|
508
|
-
inputSchema: { rol: z.string().optional(), estado: z.string().optional().describe("nueva | vista | en_evaluacion | implementada | descartada") },
|
|
509
|
-
}, async ({ rol, estado }) => run(() => api("GET", `/feedback/sugerencias${q({ rol, estado })}`)));
|
|
510
|
-
}
|
|
511
512
|
|
|
512
513
|
// ── ADMIN DE GRUPO ── Los nodos originales de un grupo son sus admins; gestionan quién está
|
|
513
514
|
// adentro y quién administra. Ver miembros lo puede cualquier miembro; sumar/expulsar/permiso
|
|
@@ -520,7 +521,7 @@ if (isStaff) {
|
|
|
520
521
|
}, async () => run(() => api("GET", "/logisticas/grupos")));
|
|
521
522
|
tool("grupo_miembros", {
|
|
522
523
|
title: "Miembros de un grupo logístico",
|
|
523
|
-
description: "Lista los nodos de un grupo (por id), marcando quién es ADMIN de grupo y si vos lo sos. El id del grupo
|
|
524
|
+
description: "Lista los nodos de un grupo (por id), marcando quién es ADMIN de grupo y si vos lo sos. El id del grupo sale de `grupos_tarifas`. Solo miembros del grupo. Solo lectura.",
|
|
524
525
|
inputSchema: { grupo: z.number().int().positive().describe("id del grupo") },
|
|
525
526
|
}, async ({ grupo }) => run(() => api("GET", `/qr/grupo/${grupo}/miembros`)));
|
|
526
527
|
tool("grupo_sumar_nodo", {
|
|
@@ -841,9 +842,9 @@ if (ALLOW_WRITE) {
|
|
|
841
842
|
title: "Cargar un envío (y etiqueta)",
|
|
842
843
|
description: "Registra un envío nuevo (queda 'A retirar') y devuelve el tracking + un LINK a la etiqueta imprimible + un LINK para subir una foto del envío desde el celular. El vendedor lo carga para sí mismo; el staff pasa `cliente` (nombre, de su nodo); el MENSAJERO pasa `cliente` y SOLO puede registrar clientes de su nodo o que haya colectado (marketplace incluido). Datos obligatorios: destinatario, telefono, direccion, localidad. Si el envío NO es de HOY (planilla que se carga días después), pasá `fecha` — es la que define en qué SEMANA se liquida. No toca dinero (montoCobro es el cobro contra entrega, no un movimiento).",
|
|
843
844
|
inputSchema: {
|
|
844
|
-
cliente: zStr().describe("
|
|
845
|
+
cliente: zStr().describe("Staff y mensajero: el vendedor del envío (staff: nombre, de tu nodo; mensajero: nombre o id, de tu nodo o que hayas colectado). El vendedor NO lo manda."),
|
|
845
846
|
destinatario: z.string().min(1).describe("Nombre de quien recibe"),
|
|
846
|
-
telefono:
|
|
847
|
+
telefono: z.union([z.string().min(1), z.number()]).transform((x) => String(x)).describe("Teléfono del destinatario (obligatorio: el backend rechaza el envío sin él)"),
|
|
847
848
|
direccion: z.string().min(1),
|
|
848
849
|
localidad: z.string().min(1),
|
|
849
850
|
cp: zStr(),
|
|
@@ -859,7 +860,7 @@ if (ALLOW_WRITE) {
|
|
|
859
860
|
if (isStaff || rol === "mensajero") {
|
|
860
861
|
tool("envio_desde_etiqueta_ml", {
|
|
861
862
|
title: "Registrar envío desde una etiqueta de Mercado Libre (foto)",
|
|
862
|
-
description: "
|
|
863
|
+
description: "Da de alta (o completa) un envío de Mercado Libre a partir de la etiqueta. El vendedor se resuelve por `mlQr`; si no, por `mlSenderId` (cuenta vinculada o aprendida); si no, por `cliente`. Si no se conoce, devuelve 404 con el senderId: preguntale al usuario de quién es. Es idempotente: si el envío ya estaba cargado no se duplica (se completa y, con `avanzarA`, se avanza). `nodoEntrega`: el nodo que lo entregó (control por foto). Dígitos ilegibles en la altura de la dirección: un '*' por cada uno (ej. 'Aguirre 31**'), no los adivines. Devuelve {id, tracking, yaExistia, noAvanzado?}. No mueve dinero. Para una carpeta de fotos, ver `flujo control_por_foto`.",
|
|
863
864
|
inputSchema: {
|
|
864
865
|
cliente: zStr().describe("Vendedor por nombre/id (si no mandás mlSenderId)"),
|
|
865
866
|
mlSenderId: zStr().describe("sender_id del vendedor en ML (mapea a su cuenta vinculada)"),
|
|
@@ -886,7 +887,7 @@ if (ALLOW_WRITE) {
|
|
|
886
887
|
}, async ({ tracking }) => run(() => api("POST", "/flujo/escanear", { codigo: tracking, accion: "procesar" })));
|
|
887
888
|
tool("envio_entregar", {
|
|
888
889
|
title: "Marcar ENTREGADO un envío ya cargado",
|
|
889
|
-
description: "Marca
|
|
890
|
+
description: "Marca ENTREGADO uno o varios envíos ya cargados, por tracking, sin foto ni firma. Usalo cuando no estás cargando desde una etiqueta (para eso está `envio_desde_etiqueta_ml` con `avanzarA`). Corre la entrega REAL: sella la logística de entrega para el clearing y le avisa al vendedor. Con `fecha` el historial dice el día REAL de entrega, no la hora del control; sobre un envío que YA figura entregado CORRIGE la fecha del cierre (vuelve en `fechaCorregida`), que es como se arregla una tanda cerrada con el día equivocado. Los que ya estaban entregados y no hay nada que corregirles vuelven en `yaEstaban` y no rompen el lote; los que están en un estado de EXCEPCIÓN (Cancelado, Devuelto al vendedor…) NO se entregan y vuelven en `errores` con su estado real. Staff, y solo envíos de tu nodo (admin global: cualquiera). No mueve dinero.",
|
|
890
891
|
inputSchema: {
|
|
891
892
|
trackings: z.array(zStr()).min(1).describe("Tracking(s) de los envíos a marcar entregados (ej. ['TN-2057600163','NFABC123'])"),
|
|
892
893
|
fecha: zStr().describe("Día REAL de entrega (dd/mm/aaaa o aaaa-mm-dd) si NO es HOY: la tanda que se controla por foto días después queda en el historial con el día en que salió, no con el del control. No se admite futura. Cambia el evento 'Entregado' del historial, NO la semana en que se liquida el envío — esa se mueve con `envio_editar_fecha`."),
|
|
@@ -894,7 +895,7 @@ if (ALLOW_WRITE) {
|
|
|
894
895
|
}, async (args) => run(() => api("POST", "/envios/entregar-ref", args)));
|
|
895
896
|
tool("envio_completar_ciclo", {
|
|
896
897
|
title: "Reconstruir los pasos que le faltan a un envío ya cerrado",
|
|
897
|
-
description: "Rellena los pasos que le FALTAN a un envío ya cerrado: 'Colectado', 'En centro de distribución' y 'En camino', cada uno a nombre de QUIEN LO HIZO y fechado en el día real.
|
|
898
|
+
description: "Rellena los pasos que le FALTAN a un envío ya cerrado: 'Colectado', 'En centro de distribución' y 'En camino', cada uno a nombre de QUIEN LO HIZO y fechado en el día real. Solo AGREGA lo que falta: no duplica un paso que ya está, no cambia el estado actual del envío y NO inventa autores (el paso del que no decís quién lo hizo, no se agrega). Pasá una persona por paso (nombre o id, del nodo); si el nombre matchea a varios te devuelve los candidatos con su id en vez de elegir por vos. Staff, y solo envíos de tu nodo. No mueve dinero (el mensajero se asigna con `envio_asignar_mensajero` y el grupo con `envio_asignar_grupo`).",
|
|
898
899
|
inputSchema: {
|
|
899
900
|
trackings: z.array(zStr()).min(1).describe("Tracking(s) de los envíos a completar (ej. ['NFABC123','TN-2057600163'])"),
|
|
900
901
|
fecha: zStr().describe("Día REAL en que se hicieron esos pasos (dd/mm/aaaa o aaaa-mm-dd). Cada paso queda con su hora nominal de ese día: Colectado 9, En centro 12, En camino 15. No se admite futura."),
|
|
@@ -905,7 +906,7 @@ if (ALLOW_WRITE) {
|
|
|
905
906
|
}, async (args) => run(() => api("POST", "/envios/completar-ciclo", args)));
|
|
906
907
|
tool("envio_pago_mensajero", {
|
|
907
908
|
title: "Cargar lo que se le paga al mensajero por esos envíos",
|
|
908
|
-
description: "Carga LO QUE SE LE PAGA a quien repartió cada envío (el valor por envío) y, si al envío le falta el NOMBRE del mensajero, lo completa.
|
|
909
|
+
description: "Carga LO QUE SE LE PAGA a quien repartió cada envío (el valor por envío) y, si al envío le falta el NOMBRE del mensajero, lo completa. Al mensajero se le paga por el nombre guardado en el envío (`Envio.mensajero`), no por el vínculo: este tool escribe valor + nombre. Usa el nombre con el que la persona está cargada (el apodo del resumen, ej. 'Maxi', no 'Maxi Sagarzazu'); en un VENDEDOR con doble rol, el de su cliente. Por defecto NO toca envíos que no estén entregados (un Cancelado o Devuelto vuelve en `noEntregados`); si igual querés pagarlos, pasá `incluirNoEntregados:true`. Si el envío no tiene mensajero y no pasás uno, vuelve en `sinMensajero` sin tocarse. Staff, y solo envíos de tu nodo.",
|
|
909
910
|
inputSchema: {
|
|
910
911
|
trackings: z.array(zStr()).min(1).describe("Tracking(s) de los envíos a pagar (ej. ['NFABC123','TN-2057600163'])"),
|
|
911
912
|
valor: zNum().describe("Lo que se le paga POR ENVÍO, en pesos (ej. 2150). Se escribe igual en todos los que mandes."),
|
|
@@ -915,12 +916,12 @@ if (ALLOW_WRITE) {
|
|
|
915
916
|
}, async (args) => run(() => api("POST", "/envios/pago-mensajero", args)));
|
|
916
917
|
tool("envio_corregir_estado", {
|
|
917
918
|
title: "Corregir el estado de un envío",
|
|
918
|
-
description: "Corrige el ESTADO de un envío ya cargado, para DESHACER algo que quedó mal: un 'Entregado' que no fue, o un envío que una corrección revivió por error. Además de los estados del panel acepta los TERMINALES de excepción ('Cancelado', 'Rechazado por el comprador')
|
|
919
|
+
description: "Corrige el ESTADO de un envío ya cargado, para DESHACER algo que quedó mal: un 'Entregado' que no fue, o un envío que una corrección revivió por error. Además de los estados del panel acepta los TERMINALES de excepción ('Cancelado', 'Rechazado por el comprador'). El `motivo` es OBLIGATORIO (mínimo 10 caracteres) y queda en el historial: una corrección sin explicación es indistinguible de un error. NO toca las banderas de devolución ni de cobro, y no mueve dinero. Staff, y solo envíos de tu nodo.",
|
|
919
920
|
inputSchema: { tracking: zStr().describe("Tracking del envío a corregir"), estado: zStr().describe("Estado destino (ej. 'Cancelado', 'En camino', 'Entregado')"), motivo: z.string().min(10).describe("Por qué se corrige. Queda en el historial del envío.") },
|
|
920
921
|
}, async (args) => run(() => api("POST", "/envios/corregir-estado", args)));
|
|
921
922
|
tool("envio_asignar_mensajero", {
|
|
922
923
|
title: "Asignar / quitar el mensajero de un envío",
|
|
923
|
-
description: "Asigna —o QUITA con `quitar:true`— el MENSAJERO de envíos ya cargados, por tracking.
|
|
924
|
+
description: "Asigna —o QUITA con `quitar:true`— el MENSAJERO de envíos ya cargados, por tracking. Busca entre todos los que pueden repartir (rol mensajero, `reparte`, o vendedor con doble rol); acepta id; si el nombre es ambiguo devuelve candidatos. No mueve dinero: el pago se liquida por NOMBRE (ver `envio_pago_mensajero`).",
|
|
924
925
|
inputSchema: { trackings: z.array(zStr()).min(1).describe("Tracking(s) de los envíos"), mensajero: zStr().optional().describe("Nombre o ID de quien repartió (rol mensajero, o alguien con reparto habilitado)"), quitar: z.boolean().optional().describe("true = deja el envío SIN mensajero asignado") },
|
|
925
926
|
}, async (args) => run(() => api("POST", "/envios/asignar-mensajero", args)));
|
|
926
927
|
tool("envio_estado", {
|
|
@@ -930,7 +931,7 @@ if (ALLOW_WRITE) {
|
|
|
930
931
|
}, async ({ tracking }) => run(() => api("GET", `/flujo/envio-estado?tracking=${encodeURIComponent(tracking)}`)));
|
|
931
932
|
tool("envios_trabados", {
|
|
932
933
|
title: "Envíos trabados del nodo (para destrabar / cerrar)",
|
|
933
|
-
description: "Lista los envíos TRABADOS de tu nodo, con el motivo: 'estancado' (mucho tiempo en 'A retirar'/'En centro' sin avanzar, ~nunca despachado), 'sin_mensajero' (en el centro pero sin mensajero asignado), 'mensajero_detenido' (en camino pero el mensajero no reporta ubicación hace rato) o 'en_camino_sin_cerrar' (en camino hace +24h sin cerrarse
|
|
934
|
+
description: "Lista los envíos TRABADOS de tu nodo, con el motivo: 'estancado' (mucho tiempo en 'A retirar'/'En centro' sin avanzar, ~nunca despachado), 'sin_mensajero' (en el centro pero sin mensajero asignado), 'mensajero_detenido' (en camino pero el mensajero no reporta ubicación hace rato) o 'en_camino_sin_cerrar' (en camino hace +24h sin cerrarse: candidato a cerrar; confirmá con el usuario antes de marcarlo entregado). Ordenados por severidad. Es la misma detección que alimenta el aviso proactivo a los operativos y admins del nodo. Read-only.",
|
|
934
935
|
inputSchema: {},
|
|
935
936
|
}, async () => run(() => api("GET", "/flujo/envios-trabados")));
|
|
936
937
|
tool("planilla_reporte", {
|
|
@@ -947,7 +948,7 @@ if (ALLOW_WRITE) {
|
|
|
947
948
|
title: "Conciliar un nodo provisorio con el nodo real",
|
|
948
949
|
description: "Cuando el nodo real YA está dado de alta, CONCILIA (fusiona) el provisorio: reatribuye sus envíos (origen/entrega) y su membresía de grupo al nodo real, y marca el provisorio como RESUELTO (queda desactivado). El clearing de esos envíos pasa a usar la tarifa del nodo real en el grupo. Aislamiento: como operador solo conciliás los provisorios que creó TU nodo y contra un nodo real que YA opera en ese grupo; si el nodo real todavía no está en el grupo hay que definirle la tarifa → eso lo hace el admin global con `tarifa`. No mueve dinero (reordena el clearing).",
|
|
949
950
|
inputSchema: {
|
|
950
|
-
provisorioId: z.number().int().positive().describe("Id del nodo provisorio a conciliar (
|
|
951
|
+
provisorioId: z.number().int().positive().describe("Id del nodo provisorio a conciliar (lo devolvió `nodo_provisorio_crear`, o lo ves en `grupos_tarifas`)"),
|
|
951
952
|
nodoReal: z.string().min(1).describe("Nodo real destino: nombre o id (nodo ya dado de alta)"),
|
|
952
953
|
tarifa: z.object({ cercana: z.number().optional(), media: z.number().optional(), lejana: z.number().optional(), muyLejana: z.number().optional() }).optional().describe("Solo admin global y solo si el nodo real todavía NO opera en el grupo: tarifa (bandas) con la que se lo agrega."),
|
|
953
954
|
modo: z.enum(["absorber", "desde_ahora"]).optional().describe("absorber (default) = el nodo real se queda con el HISTORIAL del provisorio (reatribuye sus envíos y su grupo). desde_ahora = NO toca el historial; el clearing arranca de acá en adelante."),
|
|
@@ -1017,12 +1018,12 @@ if (ALLOW_WRITE) {
|
|
|
1017
1018
|
tool("ruta_publicar", {
|
|
1018
1019
|
title: "Publicar una ruta/colecta en el marketplace",
|
|
1019
1020
|
description: "Publicá una ruta/colecta/viaje para que CUALQUIER nodo de la red la tome (subasta abierta). VOS le pagás al que la toma. precioMax opcional (tope que ofrecés pagar). No mueve dinero (la obligación se registra al adjudicar).",
|
|
1020
|
-
inputSchema: { titulo: z.string().min(1), tipo: z.enum(["ruta", "colecta", "viaje"]).optional(), descripcion: z.string().optional(), zona: z.string().optional(), precioMax: z.number().optional() },
|
|
1021
|
+
inputSchema: { titulo: z.string().min(1), tipo: z.enum(["ruta", "colecta", "viaje"]).optional(), descripcion: z.string().optional(), zona: z.string().optional(), precioMax: z.number().optional().describe("En pesos (ARS)") },
|
|
1021
1022
|
}, async (args) => run(() => api("POST", "/rutas-publicas", args)));
|
|
1022
1023
|
tool("ruta_ofertar", {
|
|
1023
1024
|
title: "Ofertar por una ruta pública",
|
|
1024
1025
|
description: "Ofertá por una publicación de OTRO nodo: `precio` = lo que cobrás por hacerla (subasta: más barato es mejor para el que publica). Si ya ofertaste, actualiza tu oferta. No podés ofertar en la tuya.",
|
|
1025
|
-
inputSchema: { publicacionId: z.number().int().positive(), precio: z.number(), nota: z.string().optional() },
|
|
1026
|
+
inputSchema: { publicacionId: z.number().int().positive(), precio: z.number().describe("En pesos (ARS)"), nota: z.string().optional() },
|
|
1026
1027
|
}, async (args) => run(() => api("POST", `/rutas-publicas/${args.publicacionId}/ofertar`, { precio: args.precio, nota: args.nota })));
|
|
1027
1028
|
tool("ruta_adjudicar", {
|
|
1028
1029
|
title: "Adjudicar una ruta pública (elegir ganador)",
|
|
@@ -1039,7 +1040,7 @@ if (ALLOW_WRITE) {
|
|
|
1039
1040
|
tool("envio_mensajero_externo", {
|
|
1040
1041
|
title: "Asignar un mensajero EXTERNO a envíos",
|
|
1041
1042
|
description: "Registra que uno o varios envíos los hace un mensajero EXTERNO (alguien de AFUERA de la plataforma, que no tiene usuario). Quedan En camino a nombre de esa persona con su valor manual, así la entrega queda trazada y entra igual en la liquidación de mensajeros. Pasá `valorPorEnvio` para fijar cuánto se le paga por envío; si no lo pasás, figuran como FALTA VALOR hasta completarlo. No reasigna envíos ya entregados. No mueve dinero (registra el valor a pagar, no lo paga).",
|
|
1042
|
-
inputSchema: { envioIds: z.array(z.number().int().positive()).describe("
|
|
1043
|
+
inputSchema: { envioIds: z.array(z.number().int().positive()).describe("El `id` del envío que devuelve `envio_consultar` (no el tracking)"), nombre: z.string().min(1).describe("Nombre de la persona que hace la entrega"), valorPorEnvio: z.number().optional().describe("Cuánto se le paga por envío"), nodo: zNodo() },
|
|
1043
1044
|
}, async ({ nodo, ...rest }) => run(() => api("POST", "/envios/mensajero-externo", { ...rest, logisticaId: nodo })));
|
|
1044
1045
|
|
|
1045
1046
|
tool("envio_editar_zona", {
|
|
@@ -1069,12 +1070,12 @@ if (ALLOW_WRITE) {
|
|
|
1069
1070
|
tool("cobro_corregir", {
|
|
1070
1071
|
title: "Corregir el monto de un cobro",
|
|
1071
1072
|
description: "Ajusta el monto a cobrar contra entrega de un envío (cuando se cobró de más o de menos), guardando el monto ORIGINAL en el historial. envioId + monto nuevo. No mueve dinero en cuentas (corrige el dato del envío).",
|
|
1072
|
-
inputSchema: { envioId: z.number().int().positive(), monto: z.number(), motivo: z.string().optional() },
|
|
1073
|
+
inputSchema: { envioId: z.number().int().positive().describe("El `id` del envío que devuelve `envio_consultar` (no el tracking)"), monto: z.number().describe("En pesos (ARS)"), motivo: z.string().optional() },
|
|
1073
1074
|
}, async ({ envioId, monto, motivo }) => run(() => api("POST", `/envios/${envioId}/corregir-cobro`, { monto, motivo })));
|
|
1074
1075
|
tool("rendicion_revertir", {
|
|
1075
1076
|
title: "Revertir una rendición (marcada por error)",
|
|
1076
1077
|
description: "Revierte un cobro/cambio marcado como 'rendido' cuando en realidad NO se rindió → vuelve a 'a rendir'. Deja registro (quién/cuándo). envioId + tipo (cobro|cambio). Scopeado a tu nodo.",
|
|
1077
|
-
inputSchema: { envioId: z.number().int().positive(), tipo: z.enum(["cobro", "cambio"]), motivo: z.string().optional() },
|
|
1078
|
+
inputSchema: { envioId: z.number().int().positive().describe("El `id` del envío que devuelve `envio_consultar` (no el tracking)"), tipo: z.enum(["cobro", "cambio"]), motivo: z.string().optional() },
|
|
1078
1079
|
}, async (args) => run(() => api("POST", "/flujo/rendicion/revertir", args)));
|
|
1079
1080
|
}
|
|
1080
1081
|
if (puede("gestion")) {
|
|
@@ -1093,7 +1094,7 @@ if (ALLOW_WRITE) {
|
|
|
1093
1094
|
|
|
1094
1095
|
tool("colecta_fija", {
|
|
1095
1096
|
title: "Colecta fija de un vendedor (días)",
|
|
1096
|
-
description: "Define la COLECTA FIJA de un vendedor: los días en que se lo va a buscar SIEMPRE, tenga o no envíos cargados. Es para los que no vinculan sus cuentas ni cargan envíos — los paquetes entran cuando el cadete los escanea en la puerta. Aparecen solos en `colecta_pendientes` esos días
|
|
1097
|
+
description: "Define la COLECTA FIJA de un vendedor: los días en que se lo va a buscar SIEMPRE, tenga o no envíos cargados. Es para los que no vinculan sus cuentas ni cargan envíos — los paquetes entran cuando el cadete los escanea en la puerta. Aparecen solos en `colecta_pendientes` esos días. `dias` acepta números (0=domingo … 6=sábado) o nombres (lunes, martes…); `dias:[]` quita la colecta fija. Config de AGENDA, no de dinero.",
|
|
1097
1098
|
inputSchema: { idCliente: z.string().describe("id del cliente (de clientes_del_nodo)"), dias: z.array(z.union([z.number(), z.string()])).describe("Días: [1,2,3,4,5] o [\"lunes\",\"miércoles\"]. [] = quitar."), nodo: zNodo() },
|
|
1098
1099
|
}, async ({ nodo, ...rest }) => run(() => api("POST", "/colecta/fija", { ...rest, logisticaId: nodo })));
|
|
1099
1100
|
|
|
@@ -1109,7 +1110,7 @@ if (ALLOW_WRITE) {
|
|
|
1109
1110
|
|
|
1110
1111
|
tool("retiros_cargar", {
|
|
1111
1112
|
title: "Cargar la lista de retiros del día",
|
|
1112
|
-
description: "Carga de una vez la LISTA DE RETIROS (colectas) del día: le pasás las direcciones y crea un retiro por cada una.
|
|
1113
|
+
description: "Carga de una vez la LISTA DE RETIROS (colectas) del día: le pasás las direcciones y crea un retiro por cada una. OJO, no confundir con `colecta_asignar`, que es ir a levantarle los paquetes a UN vendedor; esto es la ronda de paradas donde el cadete va a BUSCAR (no tienen destinatario ni teléfono, solo dirección). Si vienen numeradas ('1. Helguera 936'), el número se usa como orden de ruta y se saca de la dirección. `zona` es la que cobra y paga por la tabla Retiros ('Retiro en CABA' o 'Retiro en GBA'): no la inventes. El cadete se asigna a TODA la lista y se escribe también el nombre por el que se le PAGA (el resumen de pago agrupa por nombre, no por el vínculo). Si mandás dos veces la misma dirección para el mismo cliente y día, no se duplica: vuelve en `duplicados`. Quedan en 'A retirar'; para cerrarlas usá `envio_completar_ciclo`. No mueve dinero: registra el trabajo, no lo paga.",
|
|
1113
1114
|
inputSchema: {
|
|
1114
1115
|
cliente: z.string().min(1).describe("Cliente al que se le cargan los retiros (nombre o id, de tu nodo)"),
|
|
1115
1116
|
direcciones: z.array(z.string().min(1)).min(1).describe("Una dirección por elemento, en el orden de la ruta. Pueden venir numeradas ('1. Helguera 936, CABA') o sueltas."),
|
|
@@ -1161,7 +1162,7 @@ if (ALLOW_WRITE) {
|
|
|
1161
1162
|
|
|
1162
1163
|
tool("cliente_editar", {
|
|
1163
1164
|
title: "Editar cliente (solo lo que pasás)",
|
|
1164
|
-
description: "Edita un cliente de TU nodo cambiando SOLO lo que pasás: lo que no mandás queda como
|
|
1165
|
+
description: "Edita un cliente de TU nodo cambiando SOLO lo que pasás: lo que no mandás queda como está. Para VACIAR un campo mandalo vacío (\"\"). `email` cambia el email con el que ENTRA el usuario del cliente (tiene que tener usuario generado; requiere permiso de usuarios; no puede repetirse). No toca dinero. El admin global puede mover el cliente a otro nodo con `nodo`.",
|
|
1165
1166
|
inputSchema: {
|
|
1166
1167
|
id: z.number().int().positive().describe("ID del cliente (de clientes_del_nodo)"),
|
|
1167
1168
|
nombre: z.string().optional().describe("Solo si lo querés cambiar"),
|
|
@@ -1262,12 +1263,12 @@ if (ALLOW_PRECIOS && puede("precios")) {
|
|
|
1262
1263
|
description: "Cambia los precios por zona de una lista creando una VERSIÓN nueva (histórico exacto). Requiere permiso 'precios'; el backend impide tocar listas de otro nodo. No mueve dinero. El admin global elige el nodo de la lista con `nodo`.",
|
|
1263
1264
|
inputSchema: {
|
|
1264
1265
|
idLista: z.string().min(1).describe("ID de la lista (ej. 'B'). Ver 'precios_ver'."),
|
|
1265
|
-
cercana: z.number().optional().describe("Precio zona cercana"),
|
|
1266
|
-
media: z.number().optional().describe("Precio zona media"),
|
|
1267
|
-
lejana: z.number().optional().describe("Precio zona lejana"),
|
|
1268
|
-
muyLejana: z.number().optional().describe("Precio zona muy lejana"),
|
|
1266
|
+
cercana: z.number().optional().describe("Precio zona cercana, en pesos (ARS)"),
|
|
1267
|
+
media: z.number().optional().describe("Precio zona media, en pesos (ARS)"),
|
|
1268
|
+
lejana: z.number().optional().describe("Precio zona lejana, en pesos (ARS)"),
|
|
1269
|
+
muyLejana: z.number().optional().describe("Precio zona muy lejana, en pesos (ARS)"),
|
|
1269
1270
|
referencia: z.string().optional(),
|
|
1270
|
-
vigenciaDesde: z.string().optional().describe("YYYY-MM-DD desde cuándo rige (default: hoy)"),
|
|
1271
|
+
vigenciaDesde: z.string().optional().describe("Fecha YYYY-MM-DD desde cuándo rige (default: hoy)"),
|
|
1271
1272
|
nodo: zNodo(),
|
|
1272
1273
|
},
|
|
1273
1274
|
}, async ({ nodo, ...rest }) => run(() => api("POST", "/precios/clientes/version", { ...rest, logisticaId: nodo })));
|