Endpoints que consume escritorio.portascloud.ar,
la aplicación para probar los endpoints que se van a utilizar en
importas.com.ar
Estos endpoints sirven a la aplicación escritorio.portascloud.ar, que reemplaza
gradualmente al escritorio Symfony 1.4 alojado en portas.com.ar/escritorio.
La aplicación no tiene credenciales de base de datos: todo el acceso a datos y archivos pasa por esta API. Eso es deliberado — permite mover la aplicación a otro servidor cambiando una sola URL, sin tocar código.
freewaynet.web_clientes_usuarios (clientes finales de Portas que entran al escritorio).
No son los mismos que los de /v1/usuarios, que salen de la tabla
usuarios y son del backoffice. Son dos padrones distintos con IDs propios.
Hay dos credenciales distintas, con roles distintos:
| Credencial | Va en | Identifica a | Quién la tiene |
|---|---|---|---|
| API key | Authorization: Bearer <key> |
La aplicación | El servidor de la app (en su .env). Nunca el navegador. |
| Token de sesión | X-Sesion: <token> |
El usuario final | Se emite en el login y vive en la sesión del servidor de la app. |
id_cliente (ver la sección siguiente).
401: un problema de sesión no se
disfraza de resultado vacío.
| Scope | Habilita |
|---|---|
web:read | Lectura: GET /v1/web/sesion y futuros listados |
web:write | Escritura: login, logout y futuras acciones del usuario |
Se mantienen separados de datos:read / datos:write (que usa FreeWayNet) para que
una filtración de la key del escritorio no habilite el alta de legajos.
POST /v1/web/notificaciones/{id}/visto, gobernada por WEB_LAN_ESCRITURA en el
.env: estuvo en modo solo lectura (respondía 503 modo_solo_lectura) hasta
que se habilitó en producción el 2026-09-03 para que importas marque
leídas las notificaciones al mostrarlas.
Como la app puede correr en el mismo servidor que la API, sin esta cabecera el log de accesos registraría siempre la IP del propio servidor. La app reenvía acá la IP real del visitante.
id_clienteTodos los endpoints /v1/web/* que devuelven datos aceptan el
parámetro id_cliente. Acepta uno o varios separados por coma
(?id_cliente=84,1167). Por compatibilidad también se acepta
cliente, que es como se llamaba en los ocho reportes.
La respuesta trae todos los clientes. Es deliberado: cuando el integrador
tiene su propia tabla de usuarios y sus propias reglas de quién ve qué, el
recorte es responsabilidad suya y la API no puede adivinarlo. No devolvemos
422 por un parámetro ausente.
Se combina con el alcance de la sesión mediante AND. Un usuario con
sesión de cliente que mande el id_cliente de otro no ve nada — su
propio alcance ya lo encierra:
| Quién llama | Sin id_cliente | Con id_cliente=84 |
|---|---|---|
| Sin sesión (solo API key) | todos los clientes | solo el 84 |
Sesión delegada por id_cliente | todos los clientes | solo el 84 |
| Personal de Portas | todos los clientes | solo el 84 |
| Usuario del cliente 84 | solo el 84 | solo el 84 |
| Usuario del cliente 12 | solo el 12 | nada |
En /v1/web/legajos/{id_carpeta}/documentos el parámetro se usa para
verificar que el legajo sea de ese cliente: si no lo es, 404. Sin
id_cliente se sirve igual, con el mismo criterio que el resto. Conviene mandarlo:
los id_carpeta son predecibles (anio.despachante.nro_orden) y es la
única barrera que evita que uno adivinado devuelva documentación ajena.
id_cliente efectivo y el nombre del
usuario final. Ese nombre se toma del parámetro usuario de la request, o
de la sesión si se abrió con /v1/web/sesion-delegada. Los dos son
opcionales, pero mandarlos es lo que permite responder después "quién
consultó los comprobantes de tal cliente".
POST/v1/web/login
— scope web:write
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
usuario | string | SÍ | Normalmente el email del usuario |
clave | string | SÍ | Contraseña |
token se devuelve una sola vez: en la base se guarda
únicamente su hash SHA-256. Si se pierde, hay que volver a hacer login.
El escritorio no tiene un solo tipo de usuario. persona se decide con el mismo
orden que executeIngresar del Symfony viejo, y determina tanto el menú como
qué operaciones se ven:
persona | Condición | Alcance de los datos |
|---|---|---|
cliente | existe la fila en clientes (no alcanza con
que id_cliente tenga valor) | carpetas.id_cliente |
proveedor | tiene id_proveedor_cliente |
el viejo le fuerza id_cliente = 0, asi que las pantallas de cliente
le salen vacías. Su pantalla propia es /clientes_oficios |
transportista | tiene id_transportista |
su menú solo tiene /transp_oficios |
empleado | ninguna de las anteriores — personal de Portas | sin filtro: ve todas las operaciones, de todos los clientes |
Cuando persona es empleado, el objeto interno trae
{ id_usuario, id_perfil, aprueba_proyectos }. La credencial la da el perfil
interno 18 (Gerencia); el viejo además pisa el perfil del usuario 102 con el 18.
src/WebAlcance.php) y entra en la clave
de caché. Repetir el WHERE id_cliente a mano en cada endpoint es lo que
tarde o temprano deja uno sin filtro.
Se responde lo mismo si el usuario no existe o si la clave es incorrecta — a propósito, para no
revelar qué usuarios existen. Si el usuario existe pero está dado de baja:
usuario_inactivo.
La sesión vence a las 12 horas. Cada uso válido actualiza
fecha_ultimo_uso, pero no extiende el vencimiento.
POST/v1/web/sesion-delegada
— scope web:write
Para un sistema que tiene su propia tabla de usuarios y autentica por su
cuenta — hoy, importas.com.ar. Registra quién va a consultar y
devuelve un token igual al del login.
id_cliente por
parámetro, este endpoint no hace falta para consultar datos: se puede llamar
a todo solo con la API key. Lo que aporta es que cada llamada posterior quede atribuida a un
usuario sin repetir el dato en cada request. La alternativa liviana es mandar
usuario como parámetro en cada llamada.
id_cliente de la sesión es informativo: queda en el
log pero no filtra. Quien filtra es el parámetro de cada llamada. Así
una misma sesión sirve para consultar varios clientes.
Si el sistema externo autentica contra los mismos usuarios de Portas
(web_clientes_usuarios, la tabla de la LAN), lo único que hay que mandar es
cuál es. Portas resuelve el resto contra su propia base: es el login sin el chequeo de
la clave, porque esa ya la validó el sistema externo.
Devuelve lo mismo que el login — sesion + el objeto usuario
completo — y la sesión es idéntica a la de un login propio: mismo
id_usuario_web, misma persona, mismo alcance.
persona: "empleado" con id_cliente: 0 y
alcance a todas las operaciones, sin que el integrador tenga que pedirlo ni saber
distinguirlo.
| Código | Cuándo |
|---|---|
404 usuario_inexistente | ese usuario no existe en Portas |
403 usuario_inactivo | existe pero está dado de baja |
Para cuando los usuarios del sistema externo no existen en Portas. Se manda
el nombre del usuario — tal cual lo tenga en su base, no hace falta que signifique nada
acá — y opcionalmente un id_cliente de referencia.
Recordar que ese id_cliente queda para el log y no filtra: lo
que decide qué datos vuelven es el parámetro id_cliente de cada
llamada. Una misma sesión sirve para consultar cualquier cliente.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
usuario | string | — | Modo A. Si viene, se ignoran los demás campos |
id_cliente | int | SÍ (modo B) | Cliente de Portas al que va a ver esta sesión |
persona | string | No | cliente (default) o empleado. Con empleado la
sesión ve todos los clientes y id_cliente no se
pide (si viene, se ignora) |
usuario_externo | string | No | Identificador del usuario en el sistema que llama. Se guarda para auditoría: sin él, la sesión queda trazada solo a nivel aplicación |
El token es el mismo tipo que devuelve el login: se manda en
X-Sesion y no cambia nada más. Dura 12 horas, se cierra
con POST /v1/web/logout y GET /v1/web/sesion lo responde igual que a
un login propio, con origen: "delegada" en el bloque sesion.
| Código | Cuándo |
|---|---|
422 parametro_faltante | modo B sin id_cliente |
422 parametro_invalido | persona distinta de
cliente / empleado |
404 cliente_inexistente | ese id_cliente no existe en
FreeWayNet. Se valida para que no se abran sesiones que después no devuelven nada
y parecen un error de la API — no es un control de permisos |
403 cliente_inactivo | el cliente está dado de baja |
403 missing_scope | la key no tiene web:write |
GET/v1/web/sesion
— scope web:read
Revalida el token y devuelve el usuario actualizado. La app lo llama en cada pantalla para detectar sesiones vencidas y usuarios dados de baja mientras estaban adentro.
id_cliente? De acá, o del
usuario que devuelve el login: son el mismo objeto. Es la única forma, y
es a propósito — el token es opaco, no lleva ningún dato adentro.
id_cliente que tiene guardado contra el token, en todos los endpoints.
Si igual se manda uno distinto, se ignora. Usarlo para el saludo, el menú, los
títulos de pantalla; nunca como filtro de seguridad.
sesion_invalida (inexistente, vencida o cerrada) o usuario_inactivo.
En el segundo caso la sesión se cierra sola con motivo forzada.
POST/v1/web/logout
— scope web:write
Es idempotente: si el token ya estaba cerrado o no existe, responde 200 igual. Cerrar sesión nunca debería fallarle al usuario.
cerrada: false significa que no había ninguna sesión abierta con ese token.
GET/v1/web/oficios
— scope web:read (sesión opcional)
Reemplaza /oficios, /oficio_traer_pag y /historial del sistema
viejo. El cliente sale de la sesión, nunca del request: un usuario no puede pedir
los legajos de otro cliente.
| Parámetro | Tipo | Descripción |
|---|---|---|
historial | 0 | 1 | Sin él: operaciones en curso (oculta anuladas y cobradas). Con 1: historial completo |
page | int | Página, base 1 (default 1) |
limit | int | Filas por página (default 25, máximo 200) |
legajo | string | Número de legajo (94961) o id completo (2026.04.94961) |
referencia | string | Búsqueda parcial |
mercaderia | string | Búsqueda parcial |
tipo | impo | expo | Importación o exportación |
desde / hasta | fecha | Rango de oficialización. Acepta AAAA-MM-DD o DD-MM-AAAA |
| Campo | Cómo se calcula |
|---|---|
estado |
Estado del legajo, pisado por la autorización de pago si existe: 0 = Pago Visto COMEX, 1 = Pago Contabilizado, 2 = Pago Autorizado |
canal | Canal aduanero: verde (1), naranja (2),
rojo (3, 4, 6), o null |
documentos.completo |
true si están los 3 documentos del paquete (despacho AFIP, factura Portas y gastos) |
documentos.total |
Cantidad de documentos publicados (items 1, 9, 11, 12, 17, 18) |
estado de view_consulta_caratula_resumen no es una columna:
es la función almacenada GetEstadoCliente(id_carpeta), que se ejecuta
una vez por fila. Filtrar u ordenar por ella obliga a materializar la vista
completa del cliente. Medido: 5,8 s.
| Paso | Qué hace | Costo |
|---|---|---|
| 1 | Pagina sobre la tabla carpetas (indexada por cliente, fecha y anulada) | 0,13 s |
| 2 | Trae el detalle solo de los ids de esa página: la función corre 25 veces, no 10.891 | 0,07 s |
| — | El COUNT total se cachea 5 minutos (cambia de a un legajo por vez) | 0,00 s con hit |
Equivalencia verificada: estado NOT IN ('Anulada','Anulada Aduana','Cobrada') devuelve 7797
legajos y anulada=0 AND anulada_aduana=0 devuelve 7809; la diferencia de 12 son exactamente
los "Cobrada", que se descartan en el paso 2.
Además, el template viejo dispara 4 consultas por fila (resumen, dos conteos de documentos y autorización de pago): 80 round-trips para 20 filas. Acá son dos consultas fijas, independientemente del tamaño de la página.
GET/v1/web/legajos/{id_carpeta}/documentos
GET/v1/web/legajos/{id_carpeta}/documentos/{familia}.{pdf|zip}
Ambos con scope web:read (sesión opcional). Reemplazan /oficios/descargas,
bajar_pdf, bajar_factura y bajar_zip del sistema viejo.
Scan000NN.pdf).
| Familia | Nombre presentable | Formatos | Equivale en el viejo a |
|---|---|---|---|
f3101 | Despacho / F3101 | Despacho AFIP (item 1) | |
pdf_provisorio | Despacho provisorio | item 17 | |
factura_portas | Factura Portas | item 11 | |
gastos | Gastos de terceros | pdf, zip | item 9 |
bcra | Presentación BCRA | item 18 | |
djai | DJAI / SIMI | item 12 | |
cert_importacion | Certificado de Importación | — |
404 (no 403: responder "prohibido"
confirmaría que existe). Verificado: dueño 200 / ajeno 404 / sin sesión 401.
Cada descarga queda registrada en web_descargas (usuario, legajo, familia, IP, fecha).
El sistema viejo no las registra en ningún lado.
El campo historico_en_sistema_anterior vale true cuando el legajo no tiene
entregables y es anterior a 2022, que es desde cuando el storage nuevo tiene contenido.
No significa que el legajo no tenga documentos: significa que están en el sistema anterior.
| Año | Legajos con documento (viejo) | Con entregable (nuevo) | Cobertura |
|---|---|---|---|
| 2026 | 1.640 | 1.662 | 100% |
| 2025 | 2.981 | 2.980 | 100% |
| 2024 | 3.361 | 3.344 | 99,5% |
| 2023 | 3.752 | 3.736 | 99,6% |
| 2022 | 3.772 | 3.758 | 99,6% |
| 2011–2021 | ~38.000 | 0 | 0% — falta backfill |
GET/v1/web/movimientos
— scope web:read (sesión opcional)
Cronología de hitos y eventos de las operaciones del cliente. Reemplaza
/ultimos_mov y /movimiento_traer_pag.
| Parámetro | Descripción |
|---|---|
legajo | Número de legajo |
desde / hasta | Rango de fechas (AAAA-MM-DD o DD-MM-AAAA) |
page / limit | Paginado (default 25, máx 200) |
El total se cachea 5 minutos, igual que en el listado de oficios: contar 16.734 movimientos cuesta 0,55 s y traer la página 0,29 s.
GET/v1/web/notificaciones
— scope web:read (sesión opcional)
POST/v1/web/notificaciones/{id}/visto
— scope web:write (sesión opcional)
Es lo que el sistema viejo muestra en el sobre de la barra superior y en la pantalla
/default/notificaciones: resumen.sin_ver es el número del badge. El viejo
marca todo como visto al abrir la pantalla; acá eso lo hace el integrador con un
POST .../visto por cada ítem que vino con "vista": false.
notificaciones.id_usuario_web),
no por cliente: dos personas de la misma empresa no ven lo mismo. De quién son
(desde 2026-09-03):
id_usuario_web y no coincide,
403 usuario_ajeno.id_cliente): hace falta
id_usuario_web (alias id_usuario), el id de web_clientes_usuarios.
Sin él, 422 parametro_faltante; inexistente 404 usuario_inexistente;
inactivo 403 usuario_inactivo.id_cliente por parámetro del resto de la API:
la barrera es la API key, y el integrador decide qué usuario suyo ve qué. Marcar una
notificación de otro usuario devuelve 404 y la deja sin marcar.
| Parámetro | Descripción |
|---|---|
id_usuario_web | Usuario cuyas notificaciones se leen o marcan (ver arriba). En el
POST se acepta por query string o en el cuerpo JSON |
sin_ver | 1 para traer solo las no leídas |
page / limit | Paginado (default 20, máx 100) |
Se devuelve texto (plano) y no texto_html: el HTML viene de plantillas
de mail del sistema viejo y no es seguro inyectarlo tal cual en la página.
POST .../visto es idempotente: marcar una ya leída responde 200.
GET/v1/web/dashboard
— scope web:read (sesión opcional)
Todo el home en una sola llamada: tarjetas de resumen, próximos arribos y últimos movimientos. Es deliberadamente un endpoint compuesto — el sistema viejo hace lo contrario y dispara media docena de llamadas AJAX para pintar la misma pantalla.
son_pendientes: true significa que el cliente no tiene arribos futuros y se están
mostrando los que ya llegaron y siguen abiertos — que es lo que quiere ver en ese caso.
Los cuatro conteos y las dos listas se cachean 5 minutos en web_totales_cache.
Medición del home completo, punta a punta:
| Cliente | Sin caché | Con caché |
|---|---|---|
| Ferrero (7.809 operaciones) | 0,63 s | 0,09 s |
| Coca Cola FEMSA (10.651) | 2,78 s | 0,08 s |
LIMIT 6) costaba 0,77 s
en un cliente grande, más que todos los conteos juntos.
GET/v1/web/operaciones?tipo=expo|impo
— scope web:read (sesión opcional)
Réplica de /dashboard_expo y /dashboard_impo: el estado actual de las
operaciones activas, con las fechas del ciclo logístico y los semáforos.
| Condición | Detalle |
|---|---|
| Cliente | el de la sesión |
| Despachante | NOT IN (40, 97, 96) |
| Operación | 2 = exportación, 1 = importación |
| Carga | sin finalizar, o finalizada hace menos de 30 días — esto es lo que hace que sea "estado actual" y no el historial completo |
| Estado | sin anuladas ni cobradas |
Cada fila trae transito y liberacion con esta forma:
| estado | Significado |
|---|---|
ok | dentro del plazo de referencia |
alerta | hasta 3 días de exceso |
excedido | más de 3 días de exceso |
en_curso | todavía no terminó (cuenta desde el inicio hasta hoy) |
sin_datos | falta la fecha de inicio |
Umbrales de tránsito por vía (los del sistema viejo): aéreo 4 días, marítimo 30, terrestre y ferrocarril 10. Liberación: 7 días.
== 3), asi que un exceso de 1 o 2 días se
veía rojo igual que uno de 40. Acá amarillo es "hasta 3 días de exceso"
(<= 3), que es lo que la regla evidentemente quiso decir.
El template viejo dispara seis consultas por fila (resumen, digitalizaciones, clientes, embarques, cargas y evento de ingreso a depósito). Acá son dos consultas fijas.
Además se fuerza IGNORE INDEX (oficializacion): sin eso MySQL resuelve el
ORDER BY escaneando el índice de fecha hacia atrás hasta juntar la página, y con un
filtro selectivo (25 exportaciones entre miles de carpetas) recorre medio índice —
4,0 s medidos, 2,0 s forzando el índice por cliente. La lista de ids y el total se
cachean 5 minutos: la carga siguiente es de 0,06 s.
Verificado contra el sistema viejo: mismas operaciones, mismo orden y mismos
valores (comparado fila por fila con la respuesta AJAX de dashboard_traer_pag).
GET/v1/web/arribos?mes=AAAA-MM
— scope web:read (sesión opcional)
Arribos del mes agrupados por día, listos para pintar un calendario. Réplica de
/dashboard_calendario_proximos_arribos.
| Campo | Significado |
|---|---|
fecha | COALESCE(fecha_llegada, fecha_esperada_buque) |
estimada | true si no hay llegada real y se usó la esperada del buque |
abierta | carga sin finalizar |
hora | hora del hito de carga más avanzado que ya tenga fecha: finalización → inicio → coordinación. Misma prioridad que el sistema viejo |
etapa | finalizada | demorada |
programada | sin_coordinar. Ver abajo |
detalle | todo lo que el viejo mostraba sólo al hacer clic (popover) |
| Etapa | Condición |
|---|---|
finalizada | hay fecha_finalizacion_carga |
demorada | sin finalizar y la fecha prevista de carga (inicio → coordinación → aviso) ya pasó. Es la «sirena» del sistema viejo |
programada | sin finalizar, con fecha prevista futura |
sin_coordinar | sin ninguna fecha de carga cargada |
view_consulta_caratula_resumen. El viejo arma su
popover con esa vista, que ejecuta GetEstadoCliente() por fila. Acá los mismos
campos se leen de las tablas base (carpetas, carpetas_embarques,
carpetas_cargas, carpetas_clientes) sin ese costo.
operacion = 1) y solo del despachante
04, ambas fijas en el código. Se conservaron para no cambiar lo que el usuario ve, pero
quedan explícitas en filtros_aplicados por si se decide ampliarlas.
El rango incluye una semana antes y después del mes, porque la grilla del calendario muestra
semanas completas. El resultado se cachea 5 minutos; la clave lleva un v con la
versión del payload, que hay que subir cada vez que se le agregan campos.
(<b>responsable</b>), donde responsable es
carpetas_cargas.id_empleado_carga2. Cuando no hay segundo responsable asignado
—lo habitual— la línea queda en (), que al tamaño de la tarjeta se lee
como un cero. No es una cantidad ni un cálculo: son paréntesis vacíos.
Rendimiento: el viejo tarda 1,92 s en cargar la página y después pide los datos por AJAX aparte; acá la pantalla completa sale en 0,07 s.
GET/v1/web/liberacion?desde=&hasta=&por=llegada|oficializacion
— scope web:read (sesión opcional)
Cuántos días pasan entre que llega la mercadería y que se libera de aduana, contra el tope
esperado. Réplica de /dashboard_impo_detalle.
| Concepto | Cómo se calcula |
|---|---|
| Inicio | fecha_llegada, o fecha_esperada_buque si no hay real |
| Fin | fecha_finalizacion_carga, o fecha_inicio_carga, o HOY (en curso) |
| Tope | 10 días hábiles si la carga es suelta, 7 en contenedor |
| Días hábiles | del intervalo, descontando lo que figure en freewaynet.feriados |
Devuelve también un resumen con promedios y cumplimiento:
case '2020-01-01': …, solo de 2020) y además compara esos textos contra un
timestamp, asi que la condición nunca da verdadera: no se descuenta
ningún feriado, ni los de 2020. Acá se usa la tabla freewaynet.feriados, que ya
existía, está mantenida hasta 2026 e incluye fines de semana y feriados.
GET/v1/web/cronograma?mes=AAAA-MM
— scope web:read (sesión opcional)
Los hitos de trabajo del mes agrupados por día. Réplica de /dashboard_cronograma
(que se alimenta de calendario_actualizar_dashboard).
/v1/web/arribos. Aquel ubica la carpeta por la fecha
en que llega la mercadería; éste la ubica por las fechas de trabajo, y por eso
una misma carpeta puede aparecer hasta tres veces en el mes. Además, el de arribos muestra
sólo importaciones del despachante 04; éste no filtra: trae impo y expo, todos
los despachantes.
| Hito | etiqueta | Fecha en la que se ubica |
|---|---|---|
control_documentacion | C.Doc. | fecha_control_documentacion |
verificacion | Verif. | fecha_verificacion |
carga | Carga | COALESCE(finalizacion, inicio, coordinacion) |
Son exactamente los tres list_data.push() del template viejo. El COALESCE original
tiene siete fechas, pero el viejo sólo dibuja la tarjeta de carga cuando hay finalización,
inicio o coordinación (su variable $tiene_carga), asi que las otras cuatro nunca
llegan a usarse: las tres de acá son equivalentes.
| Campo | Significado |
|---|---|
etapa | en el hito carga:
finalizada / demorada / programada /
sin_coordinar. En los otros dos hitos siempre cumplido:
el registro de la fecha ya es el hecho |
peligrosa | la carpeta tiene el evento 207 — CARGA PELIGROSA (IMO) |
total | hitos del mes pedido (lo que muestra el encabezado) |
total_rango | incluye la semana extra a cada lado que la grilla usa para completar las semanas |
carpetas_cache, carpetas_cargas_cache, usuarios_cache,
clientes_cache y view_consulta_caratula_resumen_cache: el espejo que se
refresca por cron y que existe sólo para tapar la latencia contra Canadá. Acá se lee la base
viva. Además, cuando una carpeta no está en el espejo, el viejo cae a la vista real fila
por fila — una consulta a Argentina por cada una, que es de dónde sale buena parte de sus
2,2 s.
list_data.push() están dentro de if(isset($cliente)), donde
$cliente sale de clientes_cache. Si un cliente todavía no está en el
espejo, su calendario sale vacío sin ningún mensaje. Un par de líneas más
arriba, $terminal = $resumen->getTerminal() queda fuera de su
if(isset($resumen)): una carpeta sin fila en la vista resumen tumba la pantalla
entera.
| Viejo | Nuevo | |
|---|---|---|
| Ver el calendario | 2,18 s la página + 0,71 s el AJAX = 2,89 s | 0,086 s |
| Origen | espejo portas_cache (+ fallback fila por fila) |
base viva |
Verificado: con ANDINA EMPAQUES en julio 2026, los dos sistemas devuelven los mismos 13 eventos — mismos legajos, mismos hitos, mismas horas.
El cronograma es de las pocas pantallas que el viejo muestra en las dos ramas
del menú: la del cliente y la del empleado interno. Para el empleado no hay filtro de
cliente, y cada tarjeta agrega la razón social (el campo
cliente, que a un cliente se le manda vacío porque sería siempre el mismo).
having fecha >= HOY - 10 dias: en julio 2026 eso son
86 cargas desde el 18, y navegar a un mes anterior no muestra nada. Acá se
trae el mes completo (212 operaciones). (2) El viejo dibuja sólo el hito Carga
para el interno — los push de Verif. y C.Doc. están
detrás de hasCredential('Cliente'). Acá las tres personas ven los tres hitos
(346 en total). Verificado: filtrando por carga y desde el 18, el nuevo da exactamente las
mismas 86.
La página del interno son ~1,3 MB de HTML (386 tarjetas con su panel de detalle). Con
mod_deflate viajan 57 KB y carga en 0,13 s.
GET/v1/web/comprobantes?page=&limit=&estado=todos|vencidos|a_vencer
— scope web:read (sesión opcional)
Réplica de /comp_adeudados, que el sistema viejo pagina de a 10 por AJAX.
freewaynet.view_cli_comprobantes_adeudados. El modelo Symfony se llama
CliComprobantesAdeudados y esa tabla no existe: el nombre real está
en BaseCliComprobantesAdeudadosPeer::TABLE_NAME. Vale como regla general para
todo el Symfony viejo.
total trae lo que queda por cobrar, ya descontadas las imputaciones;
el valor original del comprobante viaja aparte en total_comprobante.
Hasta esa fecha la vista devolvía el total del comprobante y los pagos parciales
no se descontaban: la deuda salía 39 % más alta ($3.782 M contra
$2.721 M reales, 548 de 2.509 comprobantes inflados). Se agregó la columna
saldo a la vista, calculada igual que FreeWayNet
(total_comprobante − ABS(SUM(importe)) de
cli_comprobantes_imputaciones donde el comprobante figura como
id_comprobante_afectado, excluyendo imputaciones de comprobantes anulados).
| Campo del item | Significado |
|---|---|
total |
saldo pendiente del comprobante (N/C en negativo). Es el que cuadra con FreeWayNet
y el que suma resumen.monto |
total_comprobante |
total original del comprobante, sin descontar imputaciones. Sirve para mostrar «facturado vs adeudado»; es lo que informaba el sistema viejo |
vencido / dias_vencido |
calculados contra CURDATE(); el viejo los deja a ojo del lector |
cliente | solo se manda al personal de Portas |
El resumen es del conjunto completo, no de la página: el viejo lista los
comprobantes pero no dice cuánto suman ni cuáles vencieron.
monto y monto_vencido suman saldos.
Verificado: Escandinavia, 22 comprobantes en los dos sistemas; los saldos contra FreeWayNet, 2.513 comprobantes sin una sola diferencia (2026-08-10). Rendimiento: viejo ~2 s la página + 7,7 s el AJAX; nuevo 0,99 s en frío y 0,08 s cacheado (se cachean por separado los totales y cada página).
GET/v1/web/tracking?estado=en_viaje|todos
— scope web:read (sesión opcional)
Contenedores del cliente en seguimiento. Réplica de /tracking_maritimo.
Sale de eta_contenedores + eta_contenedores_carpetas, la misma
fuente que /v1/legajos/{id}/eta, y comparte con ese endpoint el parseo
del json_completo (EtaGet::armarParaWeb()): posición,
locaciones, eventos y trayectoria salen idénticos, sin duplicar la lógica.
Actualizar_Contenedores_ETA de FreeWayNet, y esa API key está
vencida desde 2025-12. Las 52 filas de la tabla tienen json_completo vacío y
ultima_pos_latitud/longitud en NULL. Lo que sí está
actualizado es el eta (del 16-07 al 07-09 de 2026).
posicion: null, y el día que vuelva la key empieza a devolver posición y
trayectoria sin tocar una línea. tracking_disponible le dice a
la pantalla si puede dibujar el mapa o si tiene que explicar por qué no hay.
| Campo | Significado |
|---|---|
legajos[] | un contenedor puede venir en más de una carpeta; se agrupa por contenedor y se acumulan los legajos, igual que el viejo, que indexa su lista por número de contenedor |
posicion | {lat, lng} o null |
trayectoria | pares [lat, lng] del viaje completo |
tracking_disponible | true si al menos un contenedor
tiene posición |
Verificado: Ferrero, 52 contenedores — los 52 de la tabla, sin repetir (el JOIN por carpeta devolvía 64 filas).
GET/v1/web/reportes/operaciones?formato=json|xlsx&…
— scope web:read (sesión opcional)
Primer reporte del submenú Reportes. Una fila por ítem de despacho, con los
datos declarados en María. Réplica de default/reportesOperaciones +
generar_reporte_operaciones.
| Parámetro | Valores |
|---|---|
cliente | uno o varios id_cliente por comas (o
cliente[]= repetido). Vale para los ocho reportes.
Se suma al alcance de la sesión, no lo reemplaza |
tipo | impo | expo |
destinacion | código exacto (IC04, EC01…) |
of_desde / of_hasta | fecha de oficialización, AAAA-MM-DD |
carga_desde / carga_hasta | finalización de carga, AAAA-MM-DD |
via | lista por comas: Aerea, Maritima, Terrestre, Ferrocarril |
canal | lista por comas: verde, naranja, rojo (rojo agrupa 3, 4 y 6) |
incluir_anuladas | 1 para traerlas; por defecto se excluyen |
formato | json (vista previa, 200 filas) o xlsx (hasta 20.000) |
.xls que en realidad es MHTML: una
página web con cabecera multipart/related y un armazón de
«Excel single file web page» pegado en el template. Excel lo abre avisando que el
formato no coincide con la extensión, y otras herramientas lo leen mal o no lo leen.
Acá se genera OOXML de verdad con src/Xlsx.php: un ZIP con las
piezas mínimas, sin dependencias, con encabezado en negrita y panel congelado.
Validado con openpyxl sin advertencias.
Anulada que las marca.
maria_caratula_cache y
compañía. Comparando el mismo rango (Ferrero, julio 2026): con
incluir_anuladas=1 el nuevo devuelve 77 despachos y el viejo 74,
y no falta ninguno — los 3 de diferencia son los que el espejo todavía no tiene. Dos
se oficializaron ese mismo día, pero uno es del 1 de julio: el espejo lo
seguía sin tener 27 días después.
La pantalla, además, muestra el resultado antes de bajarlo: la del viejo es solo un formulario con un botón «Bajar Excel», sin forma de ver si el filtro quedó bien sin abrir el archivo.
| Endpoint | Pantalla vieja | Fuente | Columnas |
|---|---|---|---|
/v1/web/reportes/status?tipo=impo|expo |
Operaciones Status Impo / Expo | view_operaciones_status | 56 |
/v1/web/reportes/liquidaciones |
Liquidaciones | view_consulta_liquidaciones | 27 |
/v1/web/reportes/facturacion |
Facturacion x Items | cli_comprobantes + cli_comprobantes_carpetas | 14 |
/v1/web/reportes/bcra |
BCRA comunicacion A7466 | view_items_categoria_bcra | 44 |
/v1/web/reportes/desvios |
Reporte Desvios Maritimos (Ferrero) | view_cargas_maritimas_costos | — |
/v1/web/reportes/comercial |
Reporte Comercial (Ferrero) | view_consulta_items_comercial | — |
/v1/web/reportes/items |
Items de Exp/Impo (Ferrero) | view_consulta_items_expo | 43 |
«Liquidaciones x Area» (Ferrero) es el mismo endpoint de liquidaciones con
area=1: agrega la columna Doc Transporte, única diferencia entre los
dos reportes del viejo. El reporte de Desvíos del viejo no filtra por cliente
(Where true); el nuevo filtra siempre por la sesión.
En el viejo Status Impo y Status Expo son dos pantallas con dos acciones; acá es un endpoint con un parámetro, porque la consulta es la misma.
GET/v1/web/clientes
— scope web:read + sesión
Es uno de los pocos que sigue exigiendo sesión, porque decide
qué devolver según quién pregunta. Un integrador la obtiene con
/v1/web/sesion-delegada y recibe la lista completa, que es lo que necesita para
poblar su propio selector de clientes.
Devuelve id_cliente y razon_social para poblar el selector
Cliente de los reportes, que acepta varios a la vez. Es el equivalente del
cambiar_cliente del escritorio viejo, que la migración no había
trasladado: hasta ahora el personal de Portas veía todas las operaciones sin forma de
acotar el reporte a un cliente.
view_operaciones_status termina en GROUP BY c.id_carpeta, y una
vista con GROUP BY no se fusiona con la consulta que la usa: MySQL la
materializa entera (95.000 filas, 27 LEFT JOIN y una subconsulta correlacionada) y recién
después aplica el WHERE. Medido: 37 s filtrando por cliente y mes, y
43 s incluso pasándole la lista exacta de 59 id_carpeta —
el truco de dos pasos que sirve en oficios acá no hace nada.
SHOW CREATE VIEW (en vez de copiar 6,6 KB de SQL que quedaría
desactualizado) y si la vista cambiara de forma se cae a consultarla normalmente.
max_execution_time = 18000. Medido de punta a punta: 42,4 s
contra 0,29 s.
tipo en la vista
de Status fallaba: no es una columna sino una expresión
(IF(operacion = 1,'Impo','Expo')), asi que su collation es COERCIBLE igual que
la del parámetro — ninguna gana y MySQL corta. Por el camino rápido el filtro pasa a
ser operacion = 1|2 (un entero) y el problema desaparece; en el camino de
respaldo se resuelve con un COLLATE explícito.
| Reporte | Viejo | Nuevo | Resultado |
|---|---|---|---|
| Operaciones | 74 despachos | 77 | ninguno falta; los 3 extra no están en el espejo |
| Status Impo | 59 legajos / 42,4 s | 59 / 0,29 s | idéntico |
| Liquidaciones | 895 filas / 73 despachos | 928 / 75 | ninguno falta; los 2 extra son los mismos que faltan en el espejo |
| Facturacion x Items | 94 filas / 47 legajos | 94 / 47 | idéntico |
| BCRA A7466 | 126 filas (Ferrero, jun 2026) | 126 | idéntico: 5.292 celdas comparadas una a una, 0 diferencias |
view_items_categoria_bcra no tiene GROUP BY, pero lleva una
subconsulta correlacionada en el SELECT (nro_lna) y eso también
impide fusionarla: filtrarla desde afuera materializa las ~670.000 filas (16 LEFT JOIN más un
join por LEFT(POSICION_ARANCELARIA,14) sin índice). Medido: 40 s
hasta pasándole la lista exacta de id_carpeta. Se aplica el mismo remedio — el cuerpo se
lee con SHOW CREATE VIEW y las condiciones se agregan a su WHERE (acá van al
final: no hay GROUP BY) —: un mes de Ferrero pasa de 40 s a 0,72 s; su
historial completo (9.546 filas), 4,2 s.
of_desde (último año
y medio) para no materializar el universo entero. Solo importaciones: la vista fija
operacion = 1.
| Endpoint | Pantalla | Parámetros | Notas |
|---|---|---|---|
GET /v1/web/rutas | Rutas | — | rutas terrestres del cliente y sus hitos |
GET /v1/web/temporales | Operaciones Temporales (Ferrero y Andina) | estado, temporal, con_saldo |
base portasco_moa_tagui (mismo servidor MySQL, otro esquema). El proceso
que la alimenta está congelado desde marzo 2025: la pantalla lo
avisa con ultima_actualizacion; el viejo lo oculta. |
GET /v1/web/comercial | Operaciones de Comercial (Ferrero) | tipo, legajo, referencia, proveedor,
producto, codigo, page, limit |
view_consulta_items_comercial, paginado por carpeta (12,3 s → 1,1 s) |
Pantallas de la persona empleado: sin filtro por cliente
(WebAlcance global). Un cliente que pruebe estas URLs ve sus propios datos o nada,
según la pantalla.
| Endpoint | Pantalla | Parámetros | Notas |
|---|---|---|---|
GET /v1/web/arribos-internos | Proximos Arribos / Sin Fecha Arribos | vista=por_llegar|sin_fecha, dias |
la del viejo estaba muerta desde 2022 (rango 2020–2022 fijo en el código) |
GET /v1/web/checklist | Check List Impo / Expo | tipo=impo|expo, legajo, responsable, dias |
panel operativo completo; marca FC/BL/PL faltantes (el viejo listaba sin decir cuáles faltan y filtraba fijo 2022). SIMI/SIRA no se replicaron: sin vigencia. |
GET /v1/web/oficializaciones | Oficializaciones x Cliente / Diarias / Mensuales | vista=cliente|diaria|mensual, desde, hasta |
agrega directo contra carpetas (las vistas del viejo materializan por su
GROUP BY interno); «x Cliente» excluye IDA4, las otras dos no — igual que
el viejo, y ahora lo dice en pantalla |
GET /v1/web/rendiciones | Rendiciones | desde, hasta, evento, solo_excedidas |
corte por evento agregado: hoy el 62% excede su tope (topes desactualizados). Bug del viejo corregido: comparaba todo contra el tope de exportación. |
GET /v1/web/vacaciones | Cronograma Vacaciones | mes |
grilla persona × día (el viejo traía las 1.227 solicitudes por un
HAVING mal puesto) |
GET /v1/web/carpetas | Operaciones en Curso (interno) | tipo, via, page, limit |
NO es /v1/web/oficios: no filtra por cliente y agrega filtro POR cliente |
GET/v1/web/oficios-externos
— scope web:read + sesión
El listado de operaciones de las dos personas externas. El criterio lo decide la persona de la sesión, nunca el request; un cliente recibe 403:
| Persona | Pantalla vieja | Criterio |
|---|---|---|
| proveedor | /clientes_oficios («Operaciones del Clientes») |
carpetas_clientes.id_proveedor_cliente de la sesión |
| transportista | /transp_oficios (su única pantalla) |
carpetas con remitos: cli_comprobantes tipo N/E +
cli_comprobantes_remitos.id_transportista |
Mismos filtros y misma fila que /v1/web/oficios (comparte
WebOficios::detalle()). Fiel al viejo: no oculta anuladas y excluye despachante 40.
Sin descargas de documentos: WebAlcance no les da acceso, igual que el sistema
anterior. La escritura del viejo (fotos de remitos, alta de viajes) no se migra hasta definir la
política de escrituras.
Verificado: Scania (proveedor 5906) 119 = 119 operaciones; transportista 240: 1.876 = 1.876, con carpetas del mismo día.
| HTTP | error | Cuándo |
|---|---|---|
| 401 | missing_auth | Falta el header Authorization |
| 401 | invalid_key | API key inexistente |
| 401 | credenciales_invalidas | Usuario o clave incorrectos |
| 401 | usuario_inactivo | El usuario existe pero está dado de baja |
| 401 | sesion_invalida | Token inexistente, vencido o cerrado |
| 403 | inactive_key | API key dada de baja |
| 403 | missing_scope | La key no tiene el scope necesario |
| 422 | parametro_faltante | Falta usuario o clave |
| 502 | db_unreachable | No responde la MySQL de FreeWayNet (red Portas) |
| Aspecto | Sistema viejo (Symfony) | Esta API |
|---|---|---|
| Token "recordarme" | md5(usuario . password) — derivable de las credenciales |
32 bytes aleatorios, sin relación con la clave |
| Guardado del token | En claro | Solo el hash SHA-256 |
| Cierre remoto | No existe | Sí: cerrada = 1 en web_sesiones |
| Log de ingresos fallidos | No se registran | Sí, en web_ingresos |
| Comparación de clave | Igualdad simple | hash_equals (tiempo constante) |
web_clientes_usuarios.clave, porque el sistema Symfony viejo las compara así y sigue en
producción. Mientras convivan los dos sistemas no se puede cambiar el formato. El paso a
bcrypt/argon2 está previsto para cuando se apague el viejo. La clave nunca viaja en
una respuesta de esta API.
Se agregan por fase, en orden de uso real medido sobre los accesos del sistema viejo. Esta página se actualiza con cada fase.
| Fase | Endpoint | Reemplaza a | Estado |
|---|---|---|---|
| 1 | POST /v1/web/login | /ingresar | ✔ disponible |
| 1 | GET /v1/web/sesion | sesión de Symfony | ✔ disponible |
| 1 | POST /v1/web/logout | /salir | ✔ disponible |
| 2 | GET /v1/web/oficios | /oficios + oficio_traer_pag | ✔ disponible |
| 2 | GET /v1/web/oficios?historial=1 | /historial | ✔ disponible |
| 3 | GET /v1/web/legajos/{id}/documentos | listado de PDF | ✔ disponible |
| 3 | GET /v1/web/legajos/{id}/documentos/{familia}.{tipo} | bajar_pdf, bajar_factura, bajar_zip | ✔ disponible |
| 4 | GET /v1/web/notificaciones + POST .../visto | /default/notificaciones | ✔ disponible |
| 4 | GET /v1/web/movimientos | /ultimos_mov | ✔ disponible |
| 5 | GET /v1/web/dashboard | home del sistema viejo | ✔ disponible |
| 6 | POST /v1/web/no-migrada | — (registra pantallas pedidas y ausentes) | ✔ disponible |
| 7 | indicadores, cronogramas, comprobantes, tracking, reportes (9b–9h) | menú del cliente | ✔ disponible |
| 8 | menú interno completo (9j) | pantallas del empleado | ✔ disponible |
| 9 | GET /v1/web/oficios-externos (9k) | /clientes_oficios + /transp_oficios | ✔ disponible |
| — | Mi Perfil | /perfil | pospuesto: define la política de escrituras |
| — | Tracking Terrestre (Ferrero) | /tracking_terrestre | bloqueado: requiere Remote MySQL en WnPower |
| — | Cash Flow / Lista Cash Flow | /cashflow + /lista_cashflow | no se migra: sin uso desde 2024-04, módulo de escritura |
| — | Tablero Materia Prima (Ferrero) | /dashboard_kpi_diario | no se migra: el del viejo es una maqueta con datos fijos de 2021 |
| — | Proyectos / Trámites | /proyectos, /tramites | no se migra: base local de WnPower, 5 filas de prueba, cero accesos |