Skip to main content

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

CampoValor
artifactIdlogistic-co
groupIdcom.hawkersco
version1.0.25
Java25 (maven.compiler.release=25)
Spring Boot4.0.6
Tipo de artefactojar (ejecutable, Spring Boot batch/CLI, spring.main.web-application-type=none)
MódulosNo 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.
  • .configLogisticCoConfiguration (declaración manual de servicios de logistics-commons).
  • .utilsLogisticCoUtils (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

DependenciaPropósito
spring-boot-starterNúcleo de Spring Boot (sin web, es un runner CLI)
spring-webNecesaria para RestClient/@HttpExchange en una app no-web (sustituye a Spring Cloud Feign)
com.googlecode.json-simple:json-simple:1.1.1Parseo de los JSON de códigos DANE
org.apache.commons:commons-lang3:3.17.0Utilidades varias
com.hawkersco:logistics-commons:1.0.25-SNAPSHOTEntidades JPA y servicios de logística compartidos
com.hawkersco:pi-function-commons:1.0.25-SNAPSHOTDateUtils, StorageUtils, SeveralUtils (serialización JAXB→XML)
com.hawkersco:servientrega-client:1.0.25-SNAPSHOTCliente SOAP (ServientregaSoapClient) y modelo JAXB (Salidas, SalidasResponse, ParamsForm)
com.hawkersco:slack-client:1.0.25-SNAPSHOTNotificaciones 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

SistemaProtocoloDirecciónDetalle
Servientrega WMS (wms.servientrega.com/suite/webservices/salidas.php)SOAP (ServientregaSoapClient)SalienteEnvío del pedido (Salidas); respuesta XML parseada vía JAXB
Google Cloud Storage (bucket pi-logistics-segment)API de GCSSalienteAuditoría de requests/responses
SlackHTTP (SlackClient)SalienteAlertas de fallo de envío y de resolución DANE
PostgreSQL (logistics)JDBCEntrante/SalienteLectura 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).

ClaveDescripciónEjemplo (producción)
spring.datasource.url / .username / .passwordCredenciales de la BD logistics${dbLogisitcsUrl}, etc.
gcs.bucket.nameBucket de GCS para auditoríapi-logistics-segment
logisticco.servientrega.credentials.login / .password / .locationCredenciales SOAP de Servientrega${servCredLogin}, etc.
slack.client.url / .auth.token / .channel.id / .channel-atc-mx.idConfiguración del cliente Slack (canal general y canal ATC-MX)${slackClientUrl}, etc.
hawkers.orders.testCadena que marca un pedido de prueba dentro de rawDatalahermanadeaxel

⚠️ 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:

  1. Rotar la contraseña de BD, las credenciales de Servientrega y el token de Slack.
  2. Sustituir los valores hardcodeados de application.properties por credenciales de un entorno de desarrollo aislado.
  3. 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 contra dane-code-alt.json, aplicando primero correcciones de nombre de ciudad desde dane-code-fix.json. Si no puede resolver DANE, marca el pedido como DANE_INCORRECT y 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 contra dane-code-mp.json buscando 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 (base eclipse-temurin:25-jre, containerizingMode=packaged), publicada en europe-west3-docker.pkg.dev/pi-saldum/pi-repo/logistic-co:<tag>.
  • Orquestación: Kubernetes CronJob (k8s/cronjob.yaml) en el clúster GKE pi-cluster-hw (zona europe-west3-a, proyecto pi-saldum), namespace pi, ejecutándose cada 5 minutos.
  • CI/CD (Jenkins): pipeline real de 3 etapas — CheckoutBuild & PushDeploy to GKE. El CLAUDE.md describe un pipeline más extenso (Build → KICS Scan → SonarQube → Test → Push → Deploy), que no coincide con el Jenkinsfile actual.
  • Las variables sensibles se inyectan en el pod mediante un Secret de 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 response compartido entre iteraciones del bucle sin reiniciar: en ambos runners, response es un campo de instancia (no una variable local) que se reasigna en sendOrderRequestLogisticServientrega antes de cada llamada SOAP. Si la llamada al cliente SOAP lanza una excepción (RestClientResponseException) antes de completar la asignación, response conserva el valor de la iteración anterior (de un pedido distinto). El código posterior (response.getBody(), comprobaciones de MSG_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. En LogisticCoRunner existe una comprobación if (response == null) return false; tras el catch, pero no protege frente a un response no nulo pero obsoleto de una iteración previa. Se recomienda declarar response como variable local dentro del método en lugar de campo de instancia, o reiniciarla explícitamente (response = null) al inicio de cada llamada.
  • LogisticCoMpRunner.getDane puede lanzar NoSuchElementException sin pasar por el flujo de "DANE incorrecto": si ninguna entrada de dane-code-mp.json coincide con el nombre de ciudad del pedido, jsonObjectList queda vacío y la rama else llama a jsonObjectList.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 el try/catch genérico de run(), que envía una notificación Slack genérica en vez de invocar markDaneIncorrect (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 en logistic-au, al detectar un pedido de test se ejecuta break en lugar de continue, deteniendo el procesamiento de los pedidos de marketplace restantes de esa ejecución (no ocurre en LogisticCoRunner, que sí usa continue).
  • CLAUDE.md verificado 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.