Skip to main content

deporvillage-update-stock

1. Descripción general

Según el pom.xml, el proyecto se describe como "Deporvillage update stock". Es un microservicio batch (runner) que sincroniza el stock disponible en el ERP interno (Dynamics) con el marketplace Deporvillage, generando un fichero CSV (EAN,QTY) y subiéndolo por FTP al servidor del proveedor. El mapeo entre el SKU interno y el EAN de producto se obtiene de una hoja de Google Sheets mantenida manualmente.

Forma parte de la familia de runners de actualización de stock del ecosistema Hawkers (mismo patrón que decathlon-update-stock, eci-update-stock), pero es el único de ellos que depende de Google Sheets como fuente de mapeo de referencias.

2. Información técnica

CampoValor
artifactIddeporvillage-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 con un único CommandLineRunner (DeporvillageUpdateStockRunner) que ejecuta todo el flujo y termina la JVM al finalizar.

Paquetes principales:

  • com.hawkersco.deporvillageupdatestock — clase principal (DeporvillageUpdateStockApplication) y el runner.
  • .configDeporvillageFtpProperties (record @ConfigurationProperties, prefijo deporvillage.ftp), DeporvillageUpdateStockConfig (bean manual de ProductDynamicsService + PersistenceManagedTypes).
flowchart TD
A[DeporvillageUpdateStockRunner] -->|1. loadSkuMap| B[Google Sheets<br/>Hoja 1!A2:C]
A -->|2. findProductDynamicsByWharehouseItemNumberList| C[(dynamics-pro<br/>ProductDynamicsService)]
A -->|3. writeCsvFile| D[depor_dir/deporvillage_stock.csv]
D -->|4. uploadToFtp| E[FTP Deporvillage<br/>providers.deporvillage.tech]
A -->|5. System.exit| F[Fin del proceso]

La clase principal usa @ConfigurationPropertiesScan (en vez de declarar DeporvillageFtpProperties con @Component) y @EnableJpaRepositories({"com.hawkersco.dynamicscommons.repository"}). DeporvillageUpdateStockConfig declara manualmente ProductDynamicsService (no es @Component en la librería externa) y el bean PersistenceManagedTypes necesario para que JPA escanee las entidades de com.hawkersco.dynamicscommons.dao.

4. Dependencias principales

DependenciaPropósito
spring-boot-starterNúcleo de Spring Boot (sin web, es un runner CLI)
com.google.apis:google-api-services-sheetsCliente de la API de Google Sheets para leer el mapeo SKU/EAN
org.apache.commons:commons-csv:1.14.1Generación del fichero CSV de stock
com.hawkersco:pi-function-commons:1.0.25-SNAPSHOTUtilidades comunes: DirectoryUtils, FtpUtils, SheetsServiceUtils
com.hawkersco:dynamics-commons:1.0.25-SNAPSHOTEntidades/servicios JPA de la BD dynamics-pro (ProductDynamicsService, ProductDynamics)
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
Google Sheets (hoja 15y_CO0uS1l4jMW1e-MESRiDhtMsGDNwvOXT1b7CpiyE)HTTP (Google Sheets API vía SheetsServiceUtils)EntranteLectura del mapeo SKU→EAN (rango Hoja 1!A2:C)
Dynamics (dynamics-pro, PostgreSQL)JDBCEntranteLectura de stock disponible por almacén AU00 y lista de SKUs
Servidor FTP Deporvillage (providers.deporvillage.tech)FTP (FtpUtils)SalienteSubida del CSV deporvillage_stock.csv con columnas EAN,QTY

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)
deporvillage.ftp.serverHost del servidor FTP de Deporvillage${deporvillageFtpServer}
deporvillage.ftp.portPuerto FTP (declarado, ver nota en sección 13)${deporvillageFtpPort}
deporvillage.ftp.userUsuario FTP${deporvillageFtpUser}
deporvillage.ftp.passContraseña FTP${deporvillageFtpPass}
deporvillage.ftp.dirDirectorio remoto de subida/
spring.datasource.urlURL JDBC de la BD dynamics-pro${dbDynamicsProUrl}
spring.datasource.usernameUsuario de BD${dbDynamicsProUsername}
spring.datasource.passwordContraseña de BD${dbDynamicsProPassword}
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 del servidor FTP de Deporvillage y contraseña de la base de datos PostgreSQL. Ninguno de estos valores se ha reproducido en este documento. Se recomienda:

  1. Rotar la contraseña FTP de Deporvillage y la contraseña de la base de datos.
  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("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. Además, el runner gestiona un directorio local depor_dir/ que borra y recrea en cada ejecución (DirectoryUtils.deleteDirectoryStream + createDirIfNotExist) para almacenar temporalmente el CSV generado.

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 una vez por hora, en el minuto 40 (schedule: "40 * * * *", zona horaria Europe/Madrid, concurrencyPolicy: Forbid, activeDeadlineSeconds: 21600). Flujo único ejecutado por DeporvillageUpdateStockRunner:

  1. Limpia y recrea el directorio local depor_dir/.
  2. loadSkuMap() — lee la hoja de Google Sheets configurada y construye un Map<SKU sin puntos, EAN sin puntos> a partir de las columnas A (EAN) y B (SKU) del rango Hoja 1!A2:C (la columna C del rango declarado no se usa en el código).
  3. Consulta ProductDynamicsService.findProductDynamicsByWharehouseItemNumberList("AU00", skuList) para obtener el stock disponible de los SKUs presentes en el mapa.
  4. writeCsvFile(...) — escribe depor_dir/deporvillage_stock.csv con cabecera EAN,QTY; las filas cuyo SKU no aparezca en el mapa de Sheets se omiten.
  5. uploadToFtp() — sube el CSV al servidor FTP de Deporvillage y borra el fichero local si la subida es correcta.
  6. Cierra la JVM (System.exit(SpringApplication.exit(context))).

10. Ejecución en local

Requisitos previos: JDK 25, Maven, acceso a la BD dynamics-pro, credenciales FTP válidas de Deporvillage y credenciales de Google Sheets configuradas para SheetsServiceUtils (pendiente de verificar el mecanismo exacto de autenticación, ya que no se ha localizado el fichero de credenciales de la cuenta de servicio en este repositorio).

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

# Compilar con tests
./mvnw 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 servidor FTP de Deporvillage que el fichero se actualizó.

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/deporvillage-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 una vez por hora.
  • 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 menciona etapas adicionales de KICS Scan y SonarQube, que no están presentes en el Jenkinsfile actual (ver sección 13).
  • Las variables sensibles se inyectan en el pod mediante un Secret de Kubernetes llamado igual que la app (deporvillage-update-stock).

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

12. Manejo de errores y logging

No hay una estrategia de excepciones centralizada (no hay @ControllerAdvice, es un runner). writeCsvFile y uploadToFtp capturan IOException de forma independiente y registran el error con Level.SEVERE mediante java.util.logging.Logger (consola), sin propagar la excepción ni notificar por otro canal (a diferencia de otros runners de la familia que sí usan Slack). El resto del flujo (loadSkuMap, la consulta a Dynamics) no captura errores explícitamente: una excepción ahí propaga hacia arriba y termina el proceso con fallo, lo cual será registrado por Kubernetes como Job fallido.

13. Notas y consideraciones

  • Puerto FTP configurado pero no usado: DeporvillageFtpProperties.port() se declara y se puebla desde deporvillage.ftp.port, pero uploadToFtp() llama a FtpUtils.connect(ftpProperties.server(), ftpProperties.user(), ftpProperties.pass()) sin pasar el puerto. Si FtpUtils.connect no usa un puerto por defecto equivalente al configurado, la propiedad port es efectivamente ignorada.
  • Columna C del rango de Sheets sin uso: el rango leído es Hoja 1!A2:C, pero loadSkuMap() solo utiliza row.get(0) (EAN) y row.get(1) (SKU); la columna C se ignora. Podría ser un rango sobredimensionado a propósito (margen) o indicar un campo que se dejó de usar.
  • Falta notificación de errores: a diferencia de otros runners del ecosistema (p. ej. decathlon-create-db), este proyecto no integra slack-client ni ningún otro canal de alerta; los fallos solo quedan en los logs de Kubernetes/Jenkins.
  • CLAUDE.md con Jenkinsfile desactualizado: describe un pipeline con etapas Build → KICS Scan → SonarQube → Test → Push → Deployment → Clean, mientras que el Jenkinsfile real solo tiene Checkout → Build & Push → Deploy to GKE, igual que el resto de runners de la familia *-update-stock/*-create-db. El resto de la descripción de arquitectura de CLAUDE.md (flujo Sheets→Dynamics→CSV→FTP) sí coincide con el código actual.
  • Autenticación de Google Sheets no verificable desde el repositorio: no se ha localizado en el código ni en la configuración cómo se autentica SheetsServiceUtils.getSheetsService() (cuenta de servicio, credenciales por variable de entorno, etc.); queda pendiente de verificar en la propia librería pi-function-commons.
  • Ver alerta de seguridad en la sección 7 sobre credenciales reales expuestas en application.properties.