Skip to main content

eci-create-db

1. Descripción general

Según el pom.xml, el proyecto se describe como "ECI create db". Es un microservicio batch (runner) que sincroniza los pedidos del marketplace El Corte Inglés (ECI) con la base de datos interna de logística: acepta los pedidos pendientes en ECI, importa los pedidos en estado de envío como entidades internas (Order, OrderLine, Customer, Shipment...) y genera las etiquetas de envío en PDF a partir de las etiquetas ZPL/PNG proporcionadas por ECI.

Forma parte de la familia de runners de creación de pedidos de marketplace del ecosistema Hawkers (mismo patrón que decathlon-create-db, coppel-create-db, dafiti-create-db), con una particularidad: es el único de esta familia que incluye un paso de conversión y generación de etiqueta de envío (EciLabelRunner) usando el servicio externo Labelary.

2. Información técnica

CampoValor
artifactIdeci-create-db
groupIdcom.hawkersco
version1.0.25
Java25 (maven.compiler.release=25)
Spring Boot4.0.6
Tipo de artefactojar (ejecutable, Spring Boot batch/CLI)
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 compuesta por 3 CommandLineRunner que implementan Ordered y se ejecutan de forma secuencial.

Paquetes principales:

  • com.hawkersco.ecicreatedb — clase principal (EciCreateDbApplication) y los tres runners.
  • .configEciCreateDbConfig (declaración manual de beans de logistics-commons).
  • .utilsEciCreateDbUtils, componente compartido de transformación de datos y aceptación de pedidos.
flowchart TD
A["1. EciAcceptOrdersRunner<br/>Acepta pedidos en waiting"] --> B["2. EciCreateDbRunner<br/>Importa pedidos SHIPPING (últimos 5 días)"]
B --> C["3. EciLabelRunner<br/>Genera etiquetas PDF y cierra la JVM"]

B -.->|usa| U[EciCreateDbUtils]
A -.->|usa| U
C -->|descarga ZIP| ECI[API ECI]
C -->|PDF→PNG| PDF[PdfUtils]
C -->|PNG→PDF| LAB[Labelary API]

EciCreateDbConfig declara manualmente todos los servicios de logistics-commons como @Bean (no son @Component en la librería externa), junto con el bean PersistenceManagedTypes (sustituto de @EntityScan, eliminado en Spring Boot 4) que escanea com.hawkersco.logisticscommons.dao.

4. Dependencias principales

DependenciaPropósito
spring-boot-starterNúcleo de Spring Boot (sin web, es un runner CLI)
com.hawkersco:logistics-commons:1.0.25-SNAPSHOTEntidades JPA y servicios de logística compartidos
com.hawkersco:pi-function-commons:1.0.25-SNAPSHOTUtilidades comunes: DateUtils, PdfUtils, ZipUtils, DirectoryUtils
com.hawkersco:eci-client:1.0.25-SNAPSHOTCliente @HttpExchange (EciClient) para la API de El Corte Inglés
com.hawkersco:slack-client:1.0.25-SNAPSHOTNotificaciones de error a Slack
com.hawkersco:labelary-client:1.0.25-SNAPSHOTCliente @HttpExchange (LabelaryClient) para convertir imágenes de etiqueta a PDF
lombokGeneració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.

6. Integraciones externas

SistemaProtocoloDirecciónDetalle
Marketplace ECI (marketplace.elcorteingles.es)HTTP (@HttpExchange vía EciClient)Entrante/SalienteLectura de pedidos en espera/envío, aceptación de pedidos, descarga del ZIP de etiquetas
Labelary (api.labelary.com)HTTP (@HttpExchange vía LabelaryClient)SalienteConversión de la imagen PNG de la etiqueta a PDF
SlackHTTP (SlackClient/SlackUtils)SalienteAlertas de errores operativos (fuente de pedido no registrada, fallo al guardar Shipment)
PostgreSQL (logistics)JDBCEntrante/SalientePersistencia de pedidos 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).

ClaveDescripciónEjemplo (producción)
spring.datasource.urlURL JDBC de la BD logistics${dbLogisitcsUrl}
spring.datasource.usernameUsuario de BD${dbLogisitcsUsername}
spring.datasource.passwordContraseña de BD${dbLogisitcsPassword}
eci.auth.client.urlURL base de la API de ECI${eciUrl}
eci.credentials.keyAPI key de autenticación con ECI${eciCredentialsKey}
slack.client.urlURL API Slack${slackClientUrl}
slack.auth.tokenToken bot de Slack${slackAuthToken}
slack.channel.idCanal de notificaciones${slackChannelId}
labelary.client.urlURL base del servicio Labelaryhttp://api.labelary.com

⚠️ 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 de ECI 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, la API key de ECI 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, OrderMarketplace, Customer, CustomerSource, Shipment, ShipmentLine, Source. No hay Flyway/Liquibase en este repositorio. El runner de etiquetas también gestiona directorios locales temporales zip/ e image/, que borra y recrea en cada ejecución.

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). Al arrancar, se ejecutan en orden los 3 runners:

OrdenRunnerFunción
1EciAcceptOrdersRunnerObtiene pedidos de ECI en estado "waiting" y los marca como aceptados vía eciClient.acceptOrder
2EciCreateDbRunnerImporta pedidos de ECI en estado SHIPPING de los últimos 5 días (paginado de 100), crea Order, OrderLine, Customer, Shipment
3EciLabelRunnerDescarga y genera las etiquetas PDF de los pedidos sin etiqueta, y cierra la JVM (System.exit(SpringApplication.exit(context)))

Detalles relevantes:

  • EciAcceptOrdersRunner: no hace ninguna comprobación de idempotencia explícita más allá de lo que gestione la propia API de ECI; se limita a recorrer todos los pedidos "waiting" devueltos y aceptarlos uno a uno.
  • EciCreateDbRunner: pagina con Thread.sleep(10_000) entre páginas; antes de crear un pedido comprueba orderService.findBySourceAndCdOrderExternal(SOURCE_ID, orderId); si ya existía un OrderMarketplace con el mismo dsOrderMarketplace/fuente, lo borra y lo recrea. El nombre interno del pedido sigue el patrón MKECI + secuencia numérica de 7 caracteres basada en el último idOrderMarketplace + 1 (no en el idOrder). Los pedidos cuyo shippingTypeLabel contiene "Entrega en centro comercial" reciben el estado PENDING_LABEL en lugar de PENDING_SHIPMENT, a la espera de que EciLabelRunner complete el flujo.
  • EciLabelRunner: para cada Order sin etiqueta (orderService.findByOrdersEciNoLabel()), descarga el ZIP de documentos de ECI, lo descomprime, localiza el PDF de la etiqueta, lo convierte a PNG (PdfUtils.convertPdfToPng, 203 DPI), sube el PNG a Labelary para reconvertirlo a PDF, codifica el resultado en base64 y lo guarda en OrderMarketplace.label, avanzando el pedido a PENDING_SHIPMENT.

10. Ejecución en local

Requisitos previos: JDK 25, Maven, acceso a la BD logistics y credenciales válidas de ECI, Slack y Labelary en un application.properties local.

# Compilar sin tests (igual que en CI)
./mvnw -B -DskipTests clean install

# Ejecutar tests
./mvnw test

# Ejecutar un test concreto
./mvnw test -Dtest=EciCreateDbApplicationTests

# Ejecutar la aplicación localmente
./mvnw spring-boot:run

Al ser un CommandLineRunner, no expone Actuator/health: la forma de verificar la ejecución es revisar el log de consola o consultar el estado (cdProcessingStatus) de la tabla orders tras la ejecución.

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/eci-create-db:<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 con 3 etapas — CheckoutBuild & Push (sustituye application-pro.properties por application.properties antes de mvn clean package jib:build) → Deploy to GKE (borra el CronJob existente con --ignore-not-found y aplica el manifiesto templado vía sed).
  • Las variables sensibles se inyectan en el pod mediante un Secret de Kubernetes llamado igual que la app (eci-create-db).

Job de Jenkins: https://jenkins-pi.hawkersco.net/job/eci-create-db/

12. Manejo de errores y logging

No hay una estrategia de excepciones centralizada (no hay @ControllerAdvice, es un runner). EciAcceptOrdersRunner y EciCreateDbRunner envuelven todo su bucle principal en un try/catch genérico que registra el error con Level.SEVERE y continúa (no propagan, por lo que un fallo en una página no detiene necesariamente las páginas siguientes salvo que rompa el propio bucle). EciCreateDbUtils.saveShipment captura ParseException y notifica a Slack. EciLabelRunner.processOrderLabel captura cualquier Exception por pedido y continúa con el siguiente. Logging mediante java.util.logging.Logger estándar (consola).

13. Notas y consideraciones

  • Inconsistencia en el email de cliente: EciCreateDbUtils.transformToOrderLogistic asigna a order.setDsCustomer(email) el valor orderEci.getOrderID() + "@eci.com" (sin prefijo), mientras que saveCustomer busca/crea el Customer usando "ECIEU" + orderEci.getOrderID() + "@eci.com" (con el prefijo CUSTOMER_EMAIL_PREFIX). Son dos cadenas de email distintas para el mismo pedido: el campo dsCustomer del pedido no coincide con el dsEmail real del Customer asociado. Aunque la relación OrderCustomer se resuelve por idCustomer y no por email, esta discrepancia puede confundir cualquier proceso o consulta que use dsCustomer como referencia de contacto.
  • Posible desalineación de rutas en EciLabelRunner: findPdfName busca el PDF en el directorio zip/<dsOrderMarketplace>, pero la extracción posterior (ZipUtils.searchPdf) y la ruta de salida usan zip/<cdOrderExternal>. En la práctica, dsOrderMarketplace y cdOrderExternal provienen del mismo orderId de ECI, por lo que hoy coinciden, pero el código no lo garantiza explícitamente: si en algún momento estos dos campos se llenaran con valores distintos, el runner dejaría de encontrar el PDF esperado sin ningún aviso claro del motivo.
  • Reintento de páginas con Thread.sleep fijo: EciCreateDbRunner espera 10 segundos entre cada página de resultados, independientemente de si la página devuelta está vacía o llena; en escenarios con muchas páginas esto puede alargar significativamente la ejecución dentro de la ventana de 5 minutos entre invocaciones del CronJob.
  • CLAUDE.md verificado: la descripción del flujo (3 runners, source ID 62, formato de nombre MKECI..., dependencias de configuración) coincide con el código actual; no se han encontrado discrepancias relevantes más allá de los hallazgos anteriores, que no estaban documentados en CLAUDE.md.
  • Ver alerta de seguridad en la sección 7 sobre credenciales reales expuestas en application.properties.