logistic-au
1. Descripción general
Según el pom.xml, el proyecto se describe como "Logistic from AU". Es un microservicio batch (runner) que toma los pedidos de Australia pendientes de envío logístico, los transforma al formato XML del carrier NPF (National Parcel Federation), los envía a su API de importación, y — solo para pedidos no-marketplace — actualiza el stock en The Iconic tras el envío. Sube también los XML de request/response a Google Cloud Storage como auditoría y notifica errores por Slack.
2. Información técnica
| Campo | Valor |
|---|---|
artifactId | logistic-au |
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, de los cuales solo uno está activo.
Paquetes principales:
com.hawkersco.logisticau— clase principal (LogisticAuApplication) y los dos runners..config—LogisticAuConfiguration(declaración manual de servicios delogistics-commons),NpfClientProperties(record@ConfigurationProperties, prefijologisticau.npfclient)..npf— DTOs anotados con JAXB para (de)serializar el XML de NPF (NpfOrder,BillingAddress,ShippingAddress,Products,Totals...) yNpfUtils(construcción del pedido NPF + POST HTTP + parseo de respuesta)..utils—LogisticAuUtils(transiciones de éxito/error compartidas),LogisticAuConst(constantes).
flowchart TD
A["LogisticAuRunner (order=1, ACTIVO)<br/>Pedidos no-marketplace"] -->|construye XML NPF| B[NPF API]
A -->|sube request/response| C[(GCS pi-logistics-segment)]
A -->|si éxito| D[The Iconic: actualiza stock]
A -->|si falla 4 veces| E[Slack]
F["LogisticAuMpRunner (order=2, DESACTIVADO)<br/>Pedidos marketplace"] -.->|mismo flujo NPF| B
F -.->|System.exit al terminar| G[Fin del proceso]
LogisticAuConfiguration declara manualmente los servicios de logistics-commons usados (no son @Component en la librería externa). NpfClientProperties se inyecta como bean @ConfigurationProperties, pero ambos runners acceden a sus campos (username(), password(), clientcode()) en lugar de usar @Value individuales.
4. Dependencias principales
| Dependencia | Propósito |
|---|---|
spring-boot-starter | Núcleo de Spring Boot (sin web, es un runner CLI) |
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:theiconic-client:1.0.25-SNAPSHOT | Cliente REST (TheIconicRestClient) para consultar/actualizar stock en The Iconic |
com.hawkersco:slack-client:1.0.25-SNAPSHOT | Notificaciones de error a Slack |
lombok | Generación de código boilerplate |
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 |
|---|---|---|---|
API NPF (npfonline.com/NPFM/FMImportAPI_V2.asmx/ImportSingleOrder) | HTTP (POST XML) | Saliente | Envío del pedido en formato XML; respuesta XML parseada vía JAXB |
The Iconic (sellercenter-api.theiconic.com.au) | HTTP (TheIconicRestClient) | Saliente | getProductBySellerSku, getStockProductById, putStockProduct — solo desde LogisticAuRunner |
Google Cloud Storage (bucket pi-logistics-segment) | API de GCS | Saliente | Auditoría de requests (logistic-au/request/yyyy/MM/dd/) y responses (response/ok/, response/ko/) |
| Slack | HTTP (SlackClient) | Saliente | Alertas de fallo de envío y de parseo de respuesta NPF |
PostgreSQL (logistics) | JDBC | Entrante/Saliente | Lectura de pedidos pendientes y actualización de estado vía logistics-commons |
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 |
theiconic.api.* | URL, filtros, versión, API key y credenciales OAuth de The Iconic | ${theiconicApiUrl}, etc. |
logisticau.npfclient.username / .password / .clientcode | Credenciales del cliente NPF (perfil PLY en producción) | ${npfclientUsername}, etc. |
slack.client.url / .auth.token / .channel.id / .channel-atc.id | Configuración del cliente Slack (dos canales: general y ATC) | ${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, API key y clientSecret OAuth de The Iconic, credenciales del cliente NPF de producción (PLY/PLY212) y de un perfil de desarrollo comentado (NPFDRAPI), 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 The Iconic y las credenciales NPF (tanto la de producción como la de desarrollo comentada).
- Rotar el token de Slack expuesto.
- 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, Customer, OrderError, SfccShippingMethod. 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). Solo se ejecuta el runner activo:
LogisticAuRunner(order=1, único activo): obtiene los pedidos AU no procesados no-marketplace (orderService.findByOrdersNoProcessedAu); para cada uno: si elrawDatacontiene la cadena de test, lo marca comoTESTy detiene el bucle (break, nocontinue— ver hallazgo en la sección 13); en caso contrario construye el XML NPF, lo sube a GCS, lo envía al carrier, procesa éxito/error, y si tuvo éxito actualiza el stock en The Iconic restando la cantidad vendida. Tras 4 intentos fallidos (nmSendLogistic >= 4), marca el pedido como error definitivo y notifica por Slack al canal ATC.
Reglas de negocio relevantes (verificadas en el código):
- Un XML de respuesta NPF con
status = "Previously Imported"se trata como éxito (el pedido pasa aINITy se marca como enviado). - El método de envío se resuelve por prioridad: mapeo SFCC (
SfccShippingMethodService) → siidOrderServiceType == 27entonces"Australia Post Express Parcel"→ por defecto"Standard [ISO2 país]". - Las direcciones que llegan como arrays JSON se limpian con una regex precompilada en
NpfUtils.cleanString(). - La descripción de producto se trunca a 30 caracteres antes de enviarla a NPF.
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 válidas de NPF/The Iconic en un application.properties local.
# Compilar sin tests (patrón de CI)
mvn clean install -DskipTests
# Ejecutar tests
mvn test
# Ejecutar un test concreto
mvn test -Dtest=LogisticAuApplicationTests
# Ejecutar la aplicación localmente
mvn spring-boot:run
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 bajo logistic-au/.
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-au:<tag>. ElCLAUDE.mdmenciona una imagen baseeclipse-temurin:17-jdk-alpine, que no coincide con la configuración actual delpom.xml. - 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(sustituyeapplication-pro.propertiesporapplication.properties) →Deploy to GKE(borra elCronJobexistente y aplica el manifiesto templado). ElCLAUDE.mddescribe un pipeline más extenso (Build → KICS scan → SonarQube → Test → Push → Deploy → Clean) que no coincide con elJenkinsfilereal. - Las variables sensibles se inyectan en el pod mediante un
Secretde Kubernetes llamado igual que la app (logistic-au).
Job de Jenkins: https://jenkins-pi.hawkersco.net/job/logistic-au/
12. Manejo de errores y logging
No hay una estrategia de excepciones centralizada (no hay @ControllerAdvice, es un runner). sendOrderRequestLogisticNPF captura excepciones de conexión y de parseo JAXB, notificando a Slack en ambos casos y devolviendo un resultado de fallo sin interrumpir el resto del pedido. LogisticAuUtils.processOrderError incrementa el contador de reintentos (nmSendLogistic) y registra la respuesta de error con Level.SEVERE. Tras 4 reintentos fallidos, se marca el pedido como error definitivo (OrderError) y se envía una alerta específica al canal Slack "ATC" con enlace directo al pedido en el panel interno (infranete.hawkersco.net/order-edit/<pedido>). Logging mediante java.util.logging.Logger estándar (consola).
13. Notas y consideraciones
LogisticAuMpRunnerestá desactivado y los pedidos de marketplace AU no se procesan: la claseLogisticAuMpRunner(order=2) tiene el@Componentcomentado. Según elCLAUDE.md, este runner es responsable de procesar los "pedidos de marketplace" (orderService.findByOrdersNoProcessedMpAu) y de cerrar la aplicación (SpringApplication.exit). Con el runner desactivado, ningún pedido de marketplace AU se envía actualmente a NPF por este servicio, ya que soloLogisticAuRunner(no-marketplace) está activo. ElCLAUDE.mddescribe ambos runners como si estuvieran operativos y no menciona en absoluto que uno de los dos está apagado; es la discrepancia más relevante encontrada en este proyecto y probablemente el motivo de que ningún proceso llame ya aSystem.exit(solo lo hacía el runner desactivado).- Pedido de test detiene el resto del lote: en
LogisticAuRunner.run(), al detectar un pedido de test se ejecutabreaken lugar decontinue, por lo que el bucle se interrumpe por completo y ningún pedido posterior de esa ejecución se procesa hasta la siguiente invocación delCronJob(5 minutos después). Si esto no es intencional, es un defecto que retrasa el procesamiento de pedidos reales cuando aparecen pedidos de prueba en medio del lote. NpfClientPropertiescoexiste con@Valuepuntuales: aunque las credenciales NPF se centralizan en un record@ConfigurationProperties, ambos runners siguen usando@Valueindividuales para el resto de la configuración (gcs.bucket.name,slack.channel.id, etc.), un patrón mixto que el propioCLAUDE.mdseñala como una transición incompleta.- Ver alerta de seguridad en la sección 7 sobre credenciales reales expuestas en
application.properties.