logistics-change-status
1. Descripción general
Según el pom.xml, el proyecto se describe como "Logistics Change Status". Es una API REST que actúa como agregador de webhooks: recibe actualizaciones de estado de pedido de múltiples transportistas externos (Auro, Servientrega, 99minutos, NPF, Cubbo, Times Logistics, LogisFashion Chile, Sprint Logistics, Sarmed, LogSolutions, Sprint Logistics UK), valida el payload (de forma desigual según el proveedor, ver hallazgo de seguridad) y lo almacena como fichero JSON/XML en Google Cloud Storage para su procesamiento posterior por otros servicios. No hay base de datos ni lógica de actualización saliente de pedidos: el servicio solo recibe, valida (parcialmente) y almacena.
2. Información técnica
| Campo | Valor |
|---|---|
artifactId | logistics-change-status |
groupId | com.hawkersco |
version | 1.0.25 |
| Java | 25 |
| Spring Boot | 4.0.6 |
| Tipo de artefacto | jar (ejecutable, API web de larga duración) |
| Módulos | No aplica (proyecto de módulo único) |
3. Arquitectura y diseño
.controller—LogisticsChangeStatusController, único controlador con 12 endpoints (1 de health check + 11 de recepción por proveedor)..config—GcsConfig(bean de GCS),LogisticsChangeStatusConfig(usuarios BasicAuth en memoria),App2ConfigurationAdapter(cadena de filtros de seguridad + CORS)..utils—LogisticsChangeStatusUtil(validación de JSON genérica, verificación de cabeceras 99min, verificación de token Sarmed),LogisticsChangeStatusConst..model— DTOs por proveedor (StatusOrdersAuro,StatusOrderNpf,StatusOrderServientrega,StatusOrderDistricenter) — no todos los proveedores tienen DTO propio; varios endpoints trabajan directamente sobre elStringJSON crudo.
flowchart TD
A[Transportista externo] -->|POST /api/change-status-order/proveedor| B[LogisticsChangeStatusController]
B -->|validación según proveedor, ver hallazgo| C{¿válido?}
C -->|sí| D[GCS pi-logistics-segment]
C -->|no| E[400 Bad Request]
B -.->|error de escritura GCS| F[Slack]
4. Dependencias principales
| Dependencia | Propósito |
|---|---|
spring-boot-starter-web | API REST |
spring-boot-starter-security | BasicAuth en memoria + cadena de filtros |
org.json:json | Validación y construcción de JSON |
org.apache.poi:poi / poi-ooxml | Declaradas, sin uso aparente en el controlador actual |
com.hawkersco:slack-client | Notificaciones de error |
com.hawkersco:pi-function-commons | StorageUtils (escritura en GCS), DateUtils |
5. API / Endpoints
Todos bajo el prefijo /api.
| Método | Ruta | Proveedor | Autenticación | Validación de contenido |
|---|---|---|---|---|
GET | /check | — | Pública | — |
POST | /change-status-order/auro | Auro | BasicAuth, ROLE_AURO | isJSONValid (sintaxis JSON genérica) |
POST | /change-status-order/servientrega | Servientrega | BasicAuth, ROLE_SERVENTREGA | isJSONValid |
POST | /change-status-order/npf | NPF | Pública (permitAll) | Ninguna — ni siquiera sintaxis JSON |
POST | /change-status-order/cubbo | Cubbo | Pública | Ninguna |
POST | /change-status-order/99min | 99minutos | Pública, pero con verificación de cabeceras (verifyHeader99min) | Cabeceras user/password/flags en Base64 comparadas contra valores configurados |
POST | /change-status-order/times-logistics | Times Logistics | Pública | Ninguna (solo registra las cabeceras en el log) |
POST | /change-status-order/logisfashion-cl/logiscore-status | LogisFashion Chile | BasicAuth (no listado en permitAll, cae en anyRequest().authenticated()) | Ninguna |
POST | /change-status-order/logisfashion-cl/shipping-status | LogisFashion Chile | BasicAuth (ídem) | Ninguna |
POST | /change-status-order/sprintlogistics | Sprint Logistics | Pública | isJSONValid |
POST | /change-status-order/sarmed | Sarmed | Pública, pero con verificación de token (verifyStatusSarmed) | isJSONValid + token en el propio payload comparado contra valor configurado |
POST | /change-status-order/logsolutions | LogSolutions | Pública | isJSONValid |
POST | /change-status-order/sprintlogistics-uk | Sprint Logistics UK | Pública | isJSONValid |
Todas las respuestas de éxito devuelven 200 OK con un JSON {"orderStatus": "..."}", incluso cuando el almacenamiento en GCS falla (en ese caso se notifica a Slack pero la respuesta HTTP sigue siendo 200).
6. Integraciones externas
| Sistema | Protocolo | Dirección | Detalle |
|---|---|---|---|
| Transportistas externos (11 proveedores) | HTTP POST entrante (webhook) | Entrante | Notificación de cambios de estado de pedido |
Google Cloud Storage (bucket pi-logistics-segment) | API de GCS | Saliente | Almacenamiento de cada payload recibido, particionado por proveedor |
| Slack | HTTP (SlackClient) | Saliente | Notificación de errores al escribir en GCS |
7. Configuración
En producción (application-pro.properties) las credenciales llegan por variables de entorno inyectadas como Secret de Kubernetes; en local (application.properties) el repositorio contiene actualmente valores reales hardcodeados (ver alerta de seguridad).
| Clave | Descripción |
|---|---|
server.port | Puerto de escucha (8080 en local, 80 en producción) |
logisticchangestatus.encodepass / .encodepasslogisfashionchile | Hashes BCrypt de las contraseñas BasicAuth (Auro/Servientrega y LogisFashion Chile) |
99min.headers.user / .password / .flags | Valores esperados (en claro, luego codificados a Base64 en tiempo de ejecución) para la verificación de cabeceras de 99minutos |
sarmed.token | Token esperado en el payload de Sarmed |
gcs.bucket.name | Bucket de GCS destino |
slack.client.url / .auth.token / .channel.id | Configuración de Slack |
🛑 Alerta de seguridad — la mayoría de endpoints "públicos" no verifican en absoluto al proveedor que dicen recibir
De los 8 endpoints marcados como públicos (permitAll) en App2ConfigurationAdapter, solo 2 (99min y sarmed) verifican realmente que la petición proviene del proveedor esperado (cabeceras Base64 y token en el payload, respectivamente). Los otros 6 —npf, cubbo, times-logistics, sprintlogistics, logsolutions, sprintlogistics-uk— o bien no aplican ninguna validación (npf, cubbo, times-logistics), o bien solo comprueban que el cuerpo sea JSON sintácticamente válido (sprintlogistics, logsolutions, sprintlogistics-uk) sin verificar ningún token, firma o cabecera específica del proveedor. Además, los dos endpoints de LogisFashion Chile (logiscore-status, shipping-status) están protegidos por BasicAuth pero, una vez autenticados, tampoco aplican ninguna validación de contenido adicional.
En la práctica, esto significa que cualquiera que conozca o adivine estas URLs puede enviar actualizaciones de estado de pedido fabricadas para 6 de los 11 proveedores integrados, y el servicio las almacenará en GCS indistinguibles de una notificación real del transportista, para su posterior consumo por otros sistemas de logística de Hawkers. El CLAUDE.md existente afirma de forma genérica que estos endpoints "rely on custom token/header validation inside LogisticsChangeStatusUtil", lo cual no es cierto salvo para 99min y Sarmed — es la desviación de seguridad más relevante detectada en este proyecto.
El fichero application.properties (perfil local) contiene además, en texto plano, las credenciales de 99minutos y el token de Sarmed, y los hashes BCrypt de las contraseñas BasicAuth, y el token de bot de Slack. Ninguno de estos valores se ha reproducido en este documento. Se recomienda:
- Añadir verificación de token/firma/cabecera específica a los endpoints
npf,cubbo,times-logistics,sprintlogistics,logsolutionsysprintlogistics-uk, siguiendo el mismo patrón ya implementado para99minysarmed. - Rotar las credenciales de 99minutos y el token de Sarmed dado que han estado expuestas en texto plano.
- Sustituir los valores hardcodeados de
application.propertiespor credenciales de un entorno de desarrollo aislado.
8. Persistencia
No aplica a este proyecto. No usa base de datos: el único almacenamiento es Google Cloud Storage, usado como buzón intermedio para el resto del ecosistema.
9. Procesos programados y mensajería
No aplica a este proyecto. No hay @Scheduled ni CronJob — se despliega como Deployment de larga duración que atiende webhooks entrantes en tiempo real.
10. Ejecución en local
Requisitos previos: JDK 25, Maven.
# Compilar sin tests
./mvnw -B -DskipTests clean install
# Ejecutar la aplicación localmente
./mvnw spring-boot:run
# Ejecutar tests
./mvnw test
# Ejecutar un test concreto
./mvnw test -Dtest=LogisticsChangeStatusApplicationTests
# Build Docker
docker build -t logistics-change-status:1.0.25 .
Verificación de que el servicio está operativo: GET /api/check (público).
11. Despliegue
- Imagen: construida con
jib-maven-plugin(baseeclipse-temurin:25-jre,containerizingMode=packaged), publicada eneurope-west3-docker.pkg.dev/pi-saldum/pi-repo/logistics-change-status:<tag>. - Orquestación: Kubernetes
Deployment(una réplica) en el clúster GKEpi-cluster-hw, namespacepi, con credenciales de cuenta de servicio de GCP montadas por volumen. - CI/CD (Jenkins): pipeline real de 3 etapas —
Checkout→Build & Push→Deploy to GKE.
Job de Jenkins: https://jenkins-pi.hawkersco.net/job/logistics-change-status/
12. Manejo de errores y logging
No hay un @RestControllerAdvice centralizado. El método interno store() envuelve la escritura en GCS en un try/catch genérico: en éxito devuelve 200 con estado de éxito; en error, notifica a Slack pero igualmente devuelve 200 con un JSON de estado de error en el cuerpo (nunca un código HTTP 4xx/5xx por fallo de almacenamiento) — el llamador solo puede distinguir el fallo inspeccionando el cuerpo de la respuesta, no el código de estado HTTP. Logging vía SLF4J (@Slf4j).
13. Notas y consideraciones
- Ver alerta de seguridad crítica en la sección 7: la mayoría de los endpoints "públicos" no verifican en absoluto la identidad del proveedor que dicen atender, contradiciendo la descripción genérica del
CLAUDE.mdexistente. - Los errores de almacenamiento siempre devuelven HTTP 200: incluso cuando falla la escritura en GCS, el endpoint responde
200 OKcon un campo de estado de error en el JSON, en lugar de un código de error HTTP — un consumidor automatizado que solo compruebe el código de estado nunca detectará el fallo. - Paquete
consts/citado enCLAUDE.mdno existe como tal:LogisticsChangeStatusConstvive en el paqueteutils/, no en un paqueteconsts/separado. - El resto de la arquitectura descrita en
CLAUDE.md(flujo de recepción, GCS como único almacén, patrón de configuración con dos perfiles, CORS abierto y CSRF deshabilitado) coincide con el código real, verificado directamente enLogisticsChangeStatusControlleryApp2ConfigurationAdapter.