Skip to main content

decathlon-update-stock

1. Descripción general

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

Forma parte de la familia de runners de actualización de stock del ecosistema Hawkers (mismo patrón que otros *-update-stock), leyendo el stock disponible desde la librería compartida dynamics-commons.

2. Información técnica

CampoValor
artifactIddecathlon-update-stock
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 2 CommandLineRunner que implementan Ordered y se ejecutan de forma secuencial. Al finalizar el segundo runner, la aplicación cierra la JVM explícitamente.

Paquetes principales:

  • com.hawkersco.decathlonupdatestock — clase principal (DecathlonUpdateStockApplication) y los dos runners (DecathlonHwUpdateStockRunner, DecathlonNwUpdateStockRunner).
  • .configDynamicsDbConfig (datasource/EntityManager/TransactionManager hacia la BD dynamics-pro), DecathlonUpdateStockConfig (bean manual de ProductDynamicsService).
  • .utilsDecathlonUpdateStockUtils, componente compartido con toda la lógica de negocio.
flowchart TD
A["1. DecathlonHwUpdateStockRunner"] --> C[DecathlonUpdateStockUtils]
B["2. DecathlonNwUpdateStockRunner<br/>(Exit JVM al finalizar)"] --> C
C -->|fetchStockBySku| D[(dynamics-pro<br/>ProductDynamicsService)]
C -->|getOffers paginado| E[Decathlon Mirakl EU]
C -->|buildCsvRows + exportAndUploadCsv| F[CSV local]
F -->|importStockFile| E

DynamicsDbConfig declara manualmente el único DataSource/EntityManagerFactory/TransactionManager de la aplicación (marcados como @Primary), apuntando a com.hawkersco.dynamicscommons.repository/dao. DecathlonUpdateStockConfig declara el bean ProductDynamicsService (no es @Component en la librería externa dynamics-commons).

4. Dependencias principales

DependenciaPropósito
spring-boot-starterNúcleo de Spring Boot (sin web, es un runner CLI)
spring-test (compile)Se usa en producción para MockMultipartFile, ver nota en sección 13
com.hawkersco:decathlon-client:1.0.25-SNAPSHOTCliente @HttpExchange (DecathlonClient) para Mirakl Decathlon EU
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)
Mirakl Decathlon EUHTTP (@HttpExchange vía DecathlonClient)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}
decathlon.credentials.urlURL base Mirakl EU${decathlonCredUrl}
decathlon.credentials.keyAPI key Mirakl EU${decathlonCredKey}
decathlon-au.credentials.urlURL base Mirakl AU (declarada pero no usada en el código, ver sección 13)(vacío en application-pro.properties)
decathlon-au.credentials.keyAPI key Mirakl AU (idem)(vacío en application-pro.properties)
decathlon.csv.pathRuta local del fichero CSV temporal./decathlon_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 keys de Mirakl Decathlon EU y AU. Ninguno de estos valores se ha reproducido en este documento. Se recomienda:

  1. Rotar las credenciales expuestas (contraseña de BD y API keys de Decathlon).
  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")). 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). Al arrancar, se ejecutan en orden los 2 runners:

OrdenRunnerFunción
1DecathlonHwUpdateStockRunnerEjecuta el flujo de sincronización de stock
2DecathlonNwUpdateStockRunnerEjecuta el mismo flujo de sincronización y cierra la JVM (System.exit(SpringApplication.exit(context)))

Flujo compartido (DecathlonUpdateStockUtils), invocado de forma idéntica por ambos runners:

  1. fetchStockBySku() — consulta ProductDynamicsService.findProductDynamicsByWharehouseAndDataAreaId("AU03", "10") y construye un Map<SKU, availableOnHandQuantity>.
  2. buildCsvRows(stockBySku) — pagina decathlonClient.getOffers(100, offset) (con espera de 30s entre páginas), filtra las ofertas cuyo SKU empiece por "DC-" (se excluyen), y para cada oferta restante busca su stock en el mapa de Dynamics; si no hay coincidencia, se omite la fila.
  3. Si hay filas, exportAndUploadCsv(csvRows, csvPath) — escribe el CSV (offer-sku;quantity), lo sube a Decathlon vía decathlonClient.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 de Mirakl Decathlon EU en un application.properties local.

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

# Ejecutar tests
./mvnw test

# 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 Mirakl 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/decathlon-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).
  • Las variables sensibles se inyectan en el pod mediante un Secret de Kubernetes llamado igual que la app (decathlon-update-stock).

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

12. Manejo de errores y logging

No hay una estrategia de excepciones centralizada (no hay @ControllerAdvice, es un runner). Cada runner 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/otro canal. DecathlonUpdateStockUtils.exportAndUploadCsv registra el resultado de la importación (éxito o error) mediante java.util.logging.Logger estándar (consola), sin un formato estructurado propio ni integración documentada con un sistema centralizado de logs.

13. Notas y consideraciones

  • CLAUDE.md gravemente desactualizado: el fichero describe una arquitectura completamente distinta a la real — doble datasource (dynamics-pro + marketplaces), dependencia marketplaces-commons, cálculo de stock por porcentaje configurado (MarketplaceStockService) y actualización directa vía DecathlonClient.updateOffers. Ninguno de estos elementos existe en el código actual: no hay dependencia marketplaces-commons en el pom.xml, no hay segundo datasource, y el mecanismo real es un export a CSV + subida por importStockFile (fichero STO01), con el stock tomado directamente del availableOnHandQuantity de Dynamics sin aplicar ningún porcentaje. Se recomienda regenerar el CLAUDE.md para reflejar el comportamiento real.
  • Los dos runners son funcionalmente idénticos: a pesar de sus nombres (DecathlonHwUpdateStockRunner para HAWKERS, DecathlonNwUpdateStockRunner para NORTHWEEK) y de lo que sugiere CLAUDE.md (procesamiento diferenciado por marca/company code), ninguno de los dos filtra por marca: ambos llaman exactamente a la misma secuencia fetchStockBySku()buildCsvRows()exportAndUploadCsv() sin ningún parámetro de compañía. En la práctica, el segundo runner repite el mismo trabajo que el primero (sube el mismo CSV dos veces), lo que indica que la diferenciación por marca prevista en el diseño original nunca se implementó, o se perdió en una refactorización.
  • Mensaje de log copiado de otro proyecto: en DecathlonUpdateStockUtils.buildCsvRows, el log de error ante una respuesta no exitosa de getOffers dice "Error al obtener offers de ECI. Status: {0}", mencionando "ECI" en vez de "Decathlon" — evidencia de copia desde un runner de ECI (eci-update-stock) sin actualizar el texto.
  • Dependencia de test en producción: se usa org.springframework.mock.web.MockMultipartFile (de spring-test, declarada como dependencia compile en el pom.xml, no solo test) para construir el payload multipart hacia decathlonClient.importStockFile. Aunque funciona, es una utilidad pensada para tests; sería más correcto usar ByteArrayResource o similar en código de producción.
  • Configuración AU sin uso: decathlon-au.credentials.url y decathlon-au.credentials.key están declaradas en ambos perfiles de configuración pero no se usan en ningún punto del código (no existe cliente AU en este proyecto, a diferencia de decathlon-create-db). Es configuración residual que puede eliminarse.
  • Ver alerta de seguridad en la sección 7 sobre credenciales reales expuestas en application.properties.