Skip to main content

eci-update-stock

1. Descripción general

Según el pom.xml, el proyecto se describe como "eci-update-stock". Es un microservicio batch (runner) que sincroniza el stock disponible en el ERP interno (Dynamics) con el marketplace El Corte Inglés (ECI), generando un fichero CSV con el formato offer-sku;quantity y subiéndolo a ECI mediante su API de importación de stock (STO01).

Forma parte de la familia de runners de actualización de stock del ecosistema Hawkers. Su lógica es prácticamente idéntica, línea a línea, a la de decathlon-update-stock, salvo por el cliente de marketplace utilizado (EciClient en lugar de DecathlonClient).

2. Información técnica

CampoValor
artifactIdeci-update-stock
groupIdcom.hawkersco
version1.0.25
Java25
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 con un único CommandLineRunner (EciUpdateStockRunner) que ejecuta todo el flujo. A diferencia de otros runners del ecosistema, no cierra explícitamente la JVM con System.exit; el proceso termina de forma natural al acabar main().

Paquetes principales:

  • com.hawkersco.eciupdatestock — clase principal (EciUpdateStockApplication) y el runner.
  • .configDynamicsDbConfig (datasource/EntityManager/TransactionManager hacia la BD dynamics-pro), EciUpdateStockConfig (bean manual de ProductDynamicsService).
flowchart TD
A[EciUpdateStockRunner] -->|fetchStockBySku| B[(dynamics-pro<br/>ProductDynamicsService)]
A -->|getOffers paginado| C[Marketplace ECI]
A -->|buildCsvRows + exportAndUploadCsv| D[CSV local]
D -->|importStockFile| C

DynamicsDbConfig declara manualmente el único DataSource/EntityManagerFactory/TransactionManager de la aplicación (marcados como @Primary), apuntando a com.hawkersco.dynamicscommons.repository/dao. EciUpdateStockConfig declara el bean ProductDynamicsService (no es @Component en la librería externa dynamics-commons). No se usa @EnableJpaRepositories a nivel de clase principal: el escaneo de repositorios se declara directamente en DynamicsDbConfig vía @EnableJpaRepositories(basePackages = {"com.hawkersco.dynamicscommons.repository"}).

4. Dependencias principales

DependenciaPropósito
spring-boot-starterNúcleo de Spring Boot (sin web, es un runner CLI)
spring-webNecesaria para RestClient/HttpServiceProxyFactory, usados por eci-client tras su migración desde Feign
spring-test (compile)Se usa en producción para MockMultipartFile al construir el payload de subida del CSV
com.hawkersco:eci-client:1.0.25-SNAPSHOTCliente @HttpExchange (EciClient) para el marketplace de El Corte Inglés
com.hawkersco:dynamics-commons:1.0.25-SNAPSHOTEntidades/servicios JPA de la BD dynamics-pro (ProductDynamicsService, ProductDynamics)
com.hawkersco:pi-function-commons:1.0.25-SNAPSHOTUtilidades comunes (no se ha detectado uso directo en el código actual)
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
Dynamics (dynamics-pro, PostgreSQL)JDBCEntranteLectura de stock disponible (ProductDynamicsService.findProductDynamicsByWharehouseAndDataAreaId)
Marketplace ECI (marketplace.elcorteingles.es)HTTP (@HttpExchange vía EciClient)Entrante/SalientePaginación de ofertas (getOffers) y subida del CSV de stock (importStockFile)

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.jdbc-urlURL JDBC de la BD dynamics-pro${dbDynamicsUrl}
spring.datasource.usernameUsuario de BD${dbDynamicsUsername}
spring.datasource.passwordContraseña de BD${dbDynamicsPassword}
eci.auth.client.urlURL base del marketplace ECI${eciCredUrl}
eci.credentials.keyAPI key de autenticación con ECI${eciCredKey}
eci.csv.pathRuta local del fichero CSV temporal./eci_stock_update.csv
spring.output.ansi.enabledColores ANSI en el log de consolaALWAYS

⚠️ 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 y API key de El Corte Inglés. Ninguno de estos valores se ha reproducido en este documento. Se recomienda:

  1. Rotar la contraseña de BD y la API key de ECI expuestas.
  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 dynamics-pro, acceso vía JPA a través de la librería dynamics-commons (@EnableJpaRepositories(basePackages = "com.hawkersco.dynamicscommons.repository") declarado en DynamicsDbConfig). spring.jpa.hibernate.ddl-auto=none: no hay generación ni migración automática del esquema desde este proyecto. Entidad relevante: ProductDynamics (campos itemNumber, availableOnHandQuantity). 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 15 minutos (schedule: "*/15 * * * *", zona horaria Europe/Madrid, concurrencyPolicy: Forbid). Flujo único ejecutado por EciUpdateStockRunner:

  1. fetchStockBySku() — consulta ProductDynamicsService.findProductDynamicsByWharehouseAndDataAreaId("AU03", "10") y construye un Map<SKU, availableOnHandQuantity>.
  2. buildCsvRows(stockBySku) — pagina eciClient.getOffers(100, offset), y para cada oferta busca su stock en el mapa de Dynamics; si no hay coincidencia (o el SKU es null), se omite la fila. El stock se trunca a entero con RoundingMode.FLOOR (sin aplicar ningún porcentaje).
  3. Si hay filas, exportAndUploadCsv(csvRows) — escribe el CSV (offer-sku;quantity), lo sube a ECI vía eciClient.importStockFile(...) (formulario multipart, fichero STO01) y borra el fichero temporal local.

10. Ejecución en local

Requisitos previos: JDK 25, Maven, acceso a la BD dynamics-pro y credenciales válidas del marketplace ECI 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=ClassName#methodName

# 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 comprobar en el panel de ECI que la importación STO01 se procesó correctamente.

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-update-stock:<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 15 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). El CLAUDE.md del repositorio describe un pipeline más amplio (Build → KICS Scan → SonarQube → Test → Push → Deploy → Clean), que no coincide con el Jenkinsfile real (ver sección 13).
  • Las variables sensibles se inyectan en el pod mediante un Secret de Kubernetes llamado igual que la app (eci-update-stock).

Job de Jenkins: https://jenkins-pi.hawkersco.net/job/eci-update-stock/

12. Manejo de errores y logging

No hay una estrategia de excepciones centralizada (no hay @ControllerAdvice, es un runner). El método run() envuelve toda su ejecución en un try/catch genérico que registra la excepción con Level.WARNING y continúa, sin propagarla ni notificar por Slack u otro canal (este proyecto no depende de slack-client). Logging mediante java.util.logging.Logger estándar (consola).

13. Notas y consideraciones

  • CLAUDE.md gravemente desactualizado: describe una arquitectura con doble datasource (dynamics-pro + marketplaces), dependencia marketplaces-commons, cálculo de stock ajustado por un porcentaje configurado por SKU (nmEci, vía MarketplaceStockService), y un EciUpdateStockOldRunner "existente pero desactivado". Ninguno de estos elementos existe en el código actual: no hay dependencia marketplaces-commons en el pom.xml, no hay segundo datasource, no existe ningún fichero EciUpdateStockOldRunner en el árbol de fuentes, y el stock se sube directamente desde el availableOnHandQuantity de Dynamics sin aplicar ningún porcentaje. El mecanismo real es idéntico al de decathlon-update-stock: export a CSV + subida por importStockFile (fichero STO01). Se recomienda regenerar el CLAUDE.md para reflejar el comportamiento real; también describe erróneamente etapas de Jenkins (KICS Scan, SonarQube, Clean) inexistentes en el Jenkinsfile actual.
  • Origen del mensaje de log "ECI" copiado a otros proyectos: el mensaje "Error al obtener offers de ECI. Status: {0}" de buildCsvRows es correcto y coherente en este proyecto (es efectivamente un runner de ECI). Este mismo texto aparece copiado literalmente, sin adaptar, en decathlon-update-stock (documentado allí como hallazgo de copia entre proyectos): ese runner de Decathlon reproduce el mensaje mencionando "ECI" en vez de "Decathlon", lo que confirma que eci-update-stock fue la plantilla origen de la que se copió el código sin renombrar los textos.
  • Sin cierre explícito de la JVM: a diferencia de la mayoría de los runners de la familia (decathlon-update-stock, deporvillage-update-stock...), este proyecto no llama a System.exit(SpringApplication.exit(context)); el proceso termina de forma implícita al acabar main(). Funcionalmente equivalente en un CommandLineRunner de un solo bean, pero estilísticamente inconsistente con el resto de la familia.
  • Dependencia de test en producción: se usa org.springframework.mock.web.MockMultipartFile (de spring-test, declarada como dependencia compile, no solo test) para construir el payload multipart hacia eciClient.importStockFile. Aunque funciona (y está documentado explícitamente en un comentario del pom.xml), es una utilidad pensada para tests; sería más correcto usar ByteArrayResource o similar en código de producción — mismo patrón ya señalado en decathlon-update-stock.
  • Ver alerta de seguridad en la sección 7 sobre credenciales reales expuestas en application.properties.