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
| Campo | Valor |
|---|---|
artifactId | deporvillage-update-stock |
groupId | com.hawkersco |
version | 1.0.25 |
| Java | 25 (maven.compiler.release=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 (DeporvillageUpdateStockRunner) que ejecuta todo el flujo y termina la JVM al finalizar.
Paquetes principales:
com.hawkersco.deporvillageupdatestock— clase principal (DeporvillageUpdateStockApplication) y el runner..config—DeporvillageFtpProperties(record@ConfigurationProperties, prefijodeporvillage.ftp),DeporvillageUpdateStockConfig(bean manual deProductDynamicsService+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
| Dependencia | Propósito |
|---|---|
spring-boot-starter | Núcleo de Spring Boot (sin web, es un runner CLI) |
com.google.apis:google-api-services-sheets | Cliente de la API de Google Sheets para leer el mapeo SKU/EAN |
org.apache.commons:commons-csv:1.14.1 | Generación del fichero CSV de stock |
com.hawkersco:pi-function-commons:1.0.25-SNAPSHOT | Utilidades comunes: DirectoryUtils, FtpUtils, SheetsServiceUtils |
com.hawkersco:dynamics-commons:1.0.25-SNAPSHOT | Entidades/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
| Sistema | Protocolo | Dirección | Detalle |
|---|---|---|---|
Google Sheets (hoja 15y_CO0uS1l4jMW1e-MESRiDhtMsGDNwvOXT1b7CpiyE) | HTTP (Google Sheets API vía SheetsServiceUtils) | Entrante | Lectura del mapeo SKU→EAN (rango Hoja 1!A2:C) |
Dynamics (dynamics-pro, PostgreSQL) | JDBC | Entrante | Lectura de stock disponible por almacén AU00 y lista de SKUs |
Servidor FTP Deporvillage (providers.deporvillage.tech) | FTP (FtpUtils) | Saliente | Subida 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).
| Clave | Descripción | Ejemplo (producción) |
|---|---|---|
deporvillage.ftp.server | Host del servidor FTP de Deporvillage | ${deporvillageFtpServer} |
deporvillage.ftp.port | Puerto FTP (declarado, ver nota en sección 13) | ${deporvillageFtpPort} |
deporvillage.ftp.user | Usuario FTP | ${deporvillageFtpUser} |
deporvillage.ftp.pass | Contraseña FTP | ${deporvillageFtpPass} |
deporvillage.ftp.dir | Directorio remoto de subida | / |
spring.datasource.url | URL JDBC de la BD dynamics-pro | ${dbDynamicsProUrl} |
spring.datasource.username | Usuario de BD | ${dbDynamicsProUsername} |
spring.datasource.password | Contraseña de BD | ${dbDynamicsProPassword} |
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 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:
- Rotar la contraseña FTP de Deporvillage y la contraseña de la base de datos.
- 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("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:
- Limpia y recrea el directorio local
depor_dir/. loadSkuMap()— lee la hoja de Google Sheets configurada y construye unMap<SKU sin puntos, EAN sin puntos>a partir de las columnas A (EAN) y B (SKU) del rangoHoja 1!A2:C(la columna C del rango declarado no se usa en el código).- Consulta
ProductDynamicsService.findProductDynamicsByWharehouseItemNumberList("AU00", skuList)para obtener el stock disponible de los SKUs presentes en el mapa. writeCsvFile(...)— escribedepor_dir/deporvillage_stock.csvcon cabeceraEAN,QTY; las filas cuyo SKU no aparezca en el mapa de Sheets se omiten.uploadToFtp()— sube el CSV al servidor FTP de Deporvillage y borra el fichero local si la subida es correcta.- 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(baseeclipse-temurin:25-jre,containerizingMode=packaged), publicada eneurope-west3-docker.pkg.dev/pi-saldum/pi-repo/deporvillage-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 una vez por hora. - 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 menciona etapas adicionales deKICS ScanySonarQube, que no están presentes en elJenkinsfileactual (ver sección 13). - Las variables sensibles se inyectan en el pod mediante un
Secretde 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 desdedeporvillage.ftp.port, perouploadToFtp()llama aFtpUtils.connect(ftpProperties.server(), ftpProperties.user(), ftpProperties.pass())sin pasar el puerto. SiFtpUtils.connectno usa un puerto por defecto equivalente al configurado, la propiedadportes efectivamente ignorada. - Columna C del rango de Sheets sin uso: el rango leído es
Hoja 1!A2:C, peroloadSkuMap()solo utilizarow.get(0)(EAN) yrow.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 integraslack-clientni ningún otro canal de alerta; los fallos solo quedan en los logs de Kubernetes/Jenkins. CLAUDE.mdcon Jenkinsfile desactualizado: describe un pipeline con etapasBuild → KICS Scan → SonarQube → Test → Push → Deployment → Clean, mientras que elJenkinsfilereal solo tieneCheckout → 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 deCLAUDE.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íapi-function-commons. - Ver alerta de seguridad en la sección 7 sobre credenciales reales expuestas en
application.properties.