logistic-co
1. Descripción general
Según el pom.xml, el proyecto se describe como "Logistic from Colombia". Es un microservicio batch (runner) que envía los pedidos pendientes de Colombia al carrier Servientrega mediante una API SOAP, tanto para pedidos directos como para pedidos de marketplace (Dafiti, Mercado Libre). Resuelve los códigos geográficos DANE (departamento/ciudad) necesarios para el envío, sube los XML de request/response a GCS como auditoría, y notifica por Slack los fallos.
2. Información técnica
| Campo | Valor |
|---|---|
artifactId | logistic-co |
groupId | com.hawkersco |
version | 1.0.25 |
| Java | 25 (maven.compiler.release=25) |
| Spring Boot | 4.0.6 |
| Tipo de artefacto | jar (ejecutable, Spring Boot batch/CLI, spring.main.web-application-type=none) |
| Módulos | No aplica (proyecto de módulo único) |
3. Arquitectura y diseño
No es una API REST: es una aplicación Spring Boot CLI con dos CommandLineRunner activos, ejecutados en orden.
Paquetes principales:
com.hawkersco.logisticco— clase principal (LogisticCoApplication) y los dos runners..config—LogisticCoConfiguration(declaración manual de servicios delogistics-commons)..utils—LogisticCoUtils(transiciones de éxito/error compartidas + limpieza de acentos),LogisticCoConst.- Recursos:
dane-code-alt.json(pedidos directos),dane-code-mp.json(pedidos de marketplace),dane-code-fix.json(correcciones de nombres de ciudad mal escritos).
flowchart TD
A["LogisticCoRunner (order=1)<br/>Pedidos directos"] -->|SOAP XML| B[Servientrega API]
C["LogisticCoMpRunner (order=2)<br/>Pedidos Dafiti/Mercado Libre"] -->|SOAP XML| B
A -->|sube request/response| D[(GCS pi-logistics-segment)]
C -->|sube request/response| D
A -->|si falla 4 veces| E[Slack canal ATC-MX]
C -->|si falla 4 veces| E
C -->|System.exit al terminar| F[Fin del proceso]
LogisticCoConfiguration declara manualmente los servicios de logistics-commons usados. Ambos runners comparten prácticamente la misma lógica de construcción del XML SOAP y de evaluación de respuesta, con diferencias en el origen del código DANE (fichero distinto según sea pedido directo o de marketplace) y en los campos de clasificación (Clasificador2: Dafiti/Mercado Libre).
4. Dependencias principales
| Dependencia | Propósito |
|---|---|
spring-boot-starter | Núcleo de Spring Boot (sin web, es un runner CLI) |
spring-web | Necesaria para RestClient/@HttpExchange en una app no-web (sustituye a Spring Cloud Feign) |
com.googlecode.json-simple:json-simple:1.1.1 | Parseo de los JSON de códigos DANE |
org.apache.commons:commons-lang3:3.17.0 | Utilidades varias |
com.hawkersco:logistics-commons:1.0.25-SNAPSHOT | Entidades JPA y servicios de logística compartidos |
com.hawkersco:pi-function-commons:1.0.25-SNAPSHOT | DateUtils, StorageUtils, SeveralUtils (serialización JAXB→XML) |
com.hawkersco:servientrega-client:1.0.25-SNAPSHOT | Cliente SOAP (ServientregaSoapClient) y modelo JAXB (Salidas, SalidasResponse, ParamsForm) |
com.hawkersco:slack-client:1.0.25-SNAPSHOT | Notificaciones de error a Slack |
spring-boot-starter-test (test) | JUnit 5 + Spring Test |
5. API / Endpoints
No aplica a este proyecto. Es un batch/runner sin capa REST propia.
6. Integraciones externas
| Sistema | Protocolo | Dirección | Detalle |
|---|---|---|---|
Servientrega WMS (wms.servientrega.com/suite/webservices/salidas.php) | SOAP (ServientregaSoapClient) | Saliente | Envío del pedido (Salidas); respuesta XML parseada vía JAXB |
Google Cloud Storage (bucket pi-logistics-segment) | API de GCS | Saliente | Auditoría de requests/responses |
| Slack | HTTP (SlackClient) | Saliente | Alertas de fallo de envío y de resolución DANE |
PostgreSQL (logistics) | JDBC | Entrante/Saliente | Lectura de pedidos pendientes y actualización de estado |
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 | Ejemplo (producción) |
|---|---|---|
spring.datasource.url / .username / .password | Credenciales de la BD logistics | ${dbLogisitcsUrl}, etc. |
gcs.bucket.name | Bucket de GCS para auditoría | pi-logistics-segment |
logisticco.servientrega.credentials.login / .password / .location | Credenciales SOAP de Servientrega | ${servCredLogin}, etc. |
slack.client.url / .auth.token / .channel.id / .channel-atc-mx.id | Configuración del cliente Slack (canal general y canal ATC-MX) | ${slackClientUrl}, etc. |
hawkers.orders.test | Cadena que marca un pedido de prueba dentro de rawData | lahermanadeaxel |
⚠️ Alerta de seguridad
El fichero src/main/resources/application.properties (perfil local) contiene actualmente credenciales reales en texto plano: contraseña de la base de datos PostgreSQL, credenciales de Servientrega (usuario 900791765, contraseña real) y token de bot de Slack (xoxb-...). Ninguno de estos valores se ha reproducido en este documento. Se recomienda:
- Rotar la contraseña de BD, las credenciales de Servientrega y el token de Slack.
- Sustituir los valores hardcodeados de
application.propertiespor credenciales de un entorno de desarrollo aislado. - Revisar el historial de control de versiones, ya que estas credenciales pueden seguir expuestas en commits anteriores.
8. Persistencia
Base de datos PostgreSQL (logistics), acceso vía JPA a través de la librería logistics-commons (@EnableJpaRepositories("com.hawkersco.logisticscommons.repository")). spring.jpa.hibernate.ddl-auto=none. Entidades relevantes: Order, OrderLine, OrderError, OrderMarketplace. No hay Flyway/Liquibase en este repositorio.
9. Procesos programados y mensajería
No hay @Scheduled ni listeners de colas: la periodicidad la impone el CronJob de Kubernetes (k8s/cronjob.yaml), que ejecuta el contenedor cada 5 minutos (schedule: "0/5 * * * *", concurrencyPolicy: Forbid). Se ejecutan en orden los 2 runners activos:
LogisticCoRunner(order=1): procesa pedidos directos (orderService.findByOrdersNoProcessedCo). Resuelve DANE contradane-code-alt.json, aplicando primero correcciones de nombre de ciudad desdedane-code-fix.json. Si no puede resolver DANE, marca el pedido comoDANE_INCORRECTy notifica por Slack.LogisticCoMpRunner(order=2): procesa pedidos de marketplace (orderService.findByOrdersNoProcessedMpCo). Para Mercado Libre usa un código DANE fijo de Bogotá; para el resto (Dafiti) resuelve DANE dinámicamente contradane-code-mp.jsonbuscando por nombre de ciudad y departamento. Al terminar, cierra la JVM (System.exit(SpringApplication.exit(context))).
Ambos runners comparten la misma regla de reintentos: tras 4 intentos fallidos (nmSendLogistic >= 4) el pedido se marca como error definitivo y se notifica al canal Slack "ATC-MX" con enlace al panel interno.
10. Ejecución en local
Requisitos previos: JDK 25, Maven, acceso a la BD logistics, credenciales de aplicación por defecto de Google (GCS) y credenciales SOAP válidas de Servientrega en un application.properties local.
# Compilar sin tests
./mvnw -B -DskipTests clean install
# Ejecutar la aplicación localmente
./mvnw spring-boot:run
# Ejecutar tests
./mvnw test
# Build Docker
docker build -t logistic-co:latest .
Al ser un CommandLineRunner sin servidor web, no expone Actuator/health: la forma de verificar la ejecución es revisar el log de consola o los ficheros subidos a GCS.
11. Despliegue
- Imagen: construida con
jib-maven-plugin(baseeclipse-temurin:25-jre,containerizingMode=packaged), publicada eneurope-west3-docker.pkg.dev/pi-saldum/pi-repo/logistic-co:<tag>. - Orquestación: Kubernetes
CronJob(k8s/cronjob.yaml) en el clúster GKEpi-cluster-hw(zonaeurope-west3-a, proyectopi-saldum), namespacepi, ejecutándose cada 5 minutos. - CI/CD (Jenkins): pipeline real de 3 etapas —
Checkout→Build & Push→Deploy to GKE. ElCLAUDE.mddescribe un pipeline más extenso (Build → KICS Scan → SonarQube → Test → Push → Deploy), que no coincide con elJenkinsfileactual. - Las variables sensibles se inyectan en el pod mediante un
Secretde Kubernetes llamado igual que la app (logistic-co).
Job de Jenkins: https://jenkins-pi.hawkersco.net/job/logistic-co/
12. Manejo de errores y logging
No hay una estrategia de excepciones centralizada (no hay @ControllerAdvice, es un runner). Cada pedido se procesa dentro de un try/catch genérico en el bucle principal que notifica a Slack ante cualquier excepción no controlada. sendOrderRequestLogisticServientrega captura RestClientResponseException (error HTTP de Servientrega) y JAXBException (fallo de parseo de la respuesta) de forma independiente. Tras 4 reintentos fallidos, se marca el pedido como error definitivo y se envía una alerta al canal Slack "ATC-MX". Logging mediante java.util.logging.Logger estándar (consola).
13. Notas y consideraciones
- Bug potencial: campo
responsecompartido entre iteraciones del bucle sin reiniciar: en ambos runners,responsees un campo de instancia (no una variable local) que se reasigna ensendOrderRequestLogisticServientregaantes de cada llamada SOAP. Si la llamada al cliente SOAP lanza una excepción (RestClientResponseException) antes de completar la asignación,responseconserva el valor de la iteración anterior (de un pedido distinto). El código posterior (response.getBody(), comprobaciones deMSG_DOC_REF_YA_EXISTE, etc.) puede entonces evaluar la respuesta de un pedido diferente al que se está procesando en ese momento, arriesgando marcar un pedido fallido como exitoso (o viceversa) basándose en una respuesta que no le corresponde. EnLogisticCoRunnerexiste una comprobaciónif (response == null) return false;tras elcatch, pero no protege frente a unresponseno nulo pero obsoleto de una iteración previa. Se recomienda declararresponsecomo variable local dentro del método en lugar de campo de instancia, o reiniciarla explícitamente (response = null) al inicio de cada llamada. LogisticCoMpRunner.getDanepuede lanzarNoSuchElementExceptionsin pasar por el flujo de "DANE incorrecto": si ninguna entrada dedane-code-mp.jsoncoincide con el nombre de ciudad del pedido,jsonObjectListqueda vacío y la ramaelsellama ajsonObjectList.getFirst()sobre una lista vacía, lanzando una excepción no relacionada con el flujo de manejo de DANE. Esta excepción es capturada por eltry/catchgenérico derun(), que envía una notificación Slack genérica en vez de invocarmarkDaneIncorrect(que produce un mensaje más específico y no incrementa contadores de reintento del mismo modo). Convendría añadir una comprobación explícita de lista vacía antes de acceder al primer elemento.- Pedido de test interrumpe el resto del lote en
LogisticCoMpRunner: igual que enlogistic-au, al detectar un pedido de test se ejecutabreaken lugar decontinue, deteniendo el procesamiento de los pedidos de marketplace restantes de esa ejecución (no ocurre enLogisticCoRunner, que sí usacontinue). CLAUDE.mdverificado en líneas generales: la descripción del flujo, la separación por runners (directo vs. marketplace) y los tres ficheros DANE coincide con el código real. La discrepancia principal está en el pipeline de Jenkins descrito, más extenso que el real.- Ver alerta de seguridad en la sección 7 sobre credenciales reales expuestas en
application.properties.