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
| Campo | Valor |
|---|---|
artifactId | eci-update-stock |
groupId | com.hawkersco |
version | 1.0.25 |
| Java | 25 |
| Spring Boot | 4.0.6 |
| Tipo de artefacto | jar (ejecutable, Spring Boot batch/CLI) |
| 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 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..config—DynamicsDbConfig(datasource/EntityManager/TransactionManager hacia la BDdynamics-pro),EciUpdateStockConfig(bean manual deProductDynamicsService).
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
| Dependencia | Propósito |
|---|---|
spring-boot-starter | Núcleo de Spring Boot (sin web, es un runner CLI) |
spring-web | Necesaria 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-SNAPSHOT | Cliente @HttpExchange (EciClient) para el marketplace de El Corte Inglés |
com.hawkersco:dynamics-commons:1.0.25-SNAPSHOT | Entidades/servicios JPA de la BD dynamics-pro (ProductDynamicsService, ProductDynamics) |
com.hawkersco:pi-function-commons:1.0.25-SNAPSHOT | Utilidades 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
| Sistema | Protocolo | Dirección | Detalle |
|---|---|---|---|
Dynamics (dynamics-pro, PostgreSQL) | JDBC | Entrante | Lectura de stock disponible (ProductDynamicsService.findProductDynamicsByWharehouseAndDataAreaId) |
Marketplace ECI (marketplace.elcorteingles.es) | HTTP (@HttpExchange vía EciClient) | Entrante/Saliente | Paginació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).
| Clave | Descripción | Ejemplo (producción) |
|---|---|---|
spring.datasource.jdbc-url | URL JDBC de la BD dynamics-pro | ${dbDynamicsUrl} |
spring.datasource.username | Usuario de BD | ${dbDynamicsUsername} |
spring.datasource.password | Contraseña de BD | ${dbDynamicsPassword} |
eci.auth.client.url | URL base del marketplace ECI | ${eciCredUrl} |
eci.credentials.key | API key de autenticación con ECI | ${eciCredKey} |
eci.csv.path | Ruta local del fichero CSV temporal | ./eci_stock_update.csv |
spring.output.ansi.enabled | Colores ANSI en el log de consola | ALWAYS |
⚠️ 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:
- Rotar la contraseña de BD y la API key de ECI expuestas.
- 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 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:
fetchStockBySku()— consultaProductDynamicsService.findProductDynamicsByWharehouseAndDataAreaId("AU03", "10")y construye unMap<SKU, availableOnHandQuantity>.buildCsvRows(stockBySku)— paginaeciClient.getOffers(100, offset), y para cada oferta busca su stock en el mapa de Dynamics; si no hay coincidencia (o el SKU esnull), se omite la fila. El stock se trunca a entero conRoundingMode.FLOOR(sin aplicar ningún porcentaje).- Si hay filas,
exportAndUploadCsv(csvRows)— escribe el CSV (offer-sku;quantity), lo sube a ECI víaeciClient.importStockFile(...)(formulario multipart, ficheroSTO01) 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(baseeclipse-temurin:25-jre,containerizingMode=packaged), publicada eneurope-west3-docker.pkg.dev/pi-saldum/pi-repo/eci-update-stock:<tag>. - Orquestación: Kubernetes
CronJob(k8s/cronjob.yaml) en el clúster GKEpi-cluster-hw(zonaeurope-west3-a, proyectopi-saldum), namespacepi, ejecutándose cada 15 minutos. - CI/CD (Jenkins): pipeline con 3 etapas —
Checkout→Build & Push(sustituyeapplication-pro.propertiesporapplication.propertiesantes demvn clean package jib:build) →Deploy to GKE(borra elCronJobexistente con--ignore-not-foundy aplica el manifiesto templado víased). ElCLAUDE.mddel repositorio describe un pipeline más amplio (Build → KICS Scan → SonarQube → Test → Push → Deploy → Clean), que no coincide con elJenkinsfilereal (ver sección 13). - Las variables sensibles se inyectan en el pod mediante un
Secretde 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.mdgravemente desactualizado: describe una arquitectura con doble datasource (dynamics-pro+marketplaces), dependenciamarketplaces-commons, cálculo de stock ajustado por un porcentaje configurado por SKU (nmEci, víaMarketplaceStockService), y unEciUpdateStockOldRunner"existente pero desactivado". Ninguno de estos elementos existe en el código actual: no hay dependenciamarketplaces-commonsen elpom.xml, no hay segundo datasource, no existe ningún ficheroEciUpdateStockOldRunneren el árbol de fuentes, y el stock se sube directamente desde elavailableOnHandQuantityde Dynamics sin aplicar ningún porcentaje. El mecanismo real es idéntico al dedecathlon-update-stock: export a CSV + subida porimportStockFile(ficheroSTO01). Se recomienda regenerar elCLAUDE.mdpara reflejar el comportamiento real; también describe erróneamente etapas de Jenkins (KICS Scan,SonarQube,Clean) inexistentes en elJenkinsfileactual.- Origen del mensaje de log "ECI" copiado a otros proyectos: el mensaje
"Error al obtener offers de ECI. Status: {0}"debuildCsvRowses correcto y coherente en este proyecto (es efectivamente un runner de ECI). Este mismo texto aparece copiado literalmente, sin adaptar, endecathlon-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 queeci-update-stockfue 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 aSystem.exit(SpringApplication.exit(context)); el proceso termina de forma implícita al acabarmain(). Funcionalmente equivalente en unCommandLineRunnerde 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(despring-test, declarada como dependenciacompile, no solotest) para construir el payload multipart haciaeciClient.importStockFile. Aunque funciona (y está documentado explícitamente en un comentario delpom.xml), es una utilidad pensada para tests; sería más correcto usarByteArrayResourceo similar en código de producción — mismo patrón ya señalado endecathlon-update-stock. - Ver alerta de seguridad en la sección 7 sobre credenciales reales expuestas en
application.properties.