products-dynamics-pi
1. Descripción general
Según el pom.xml, el proyecto se describe como "Get all products to Dynamics and save PI". Es un microservicio batch (runner) que extrae de Dynamics 365 el catálogo de productos con su stock (por 6 dataAreaId distintos), las variantes de producto, los precios de Privalia de los últimos 3 meses y los códigos de barras de inventario, y los persiste en la base de datos dynamics-pro.
2. Información técnica
| Campo | Valor |
|---|---|
artifactId | products-dynamics-pi |
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 (ProductsDynamicsPiRunner) que ejecuta todo el flujo y cierra la JVM al finalizar.
com.hawkersco.productsdynamicspi— clase principal (ProductsDynamicsPiApplication) y el runner..config—ProductsDynamicsPiConfig(declaración de beans,PersistenceManagedTypesScannerpara las entidades externas dedynamics-commons).
flowchart TD
A[ProductsDynamicsPiRunner] -->|día 1, 03h: getVariantsPaginate| B[Dynamics 365 API]
A -->|siempre: getProductsPaginate x6 dataAreaId| B
A -->|insertProductDynamicsList| C[(dynamics-pro · ProductDynamics)]
A -->|getPricesPrivaliaFromDate últimos 3 meses| B
A -->|truncate + batch insert| D[(dynamics-pro · DynamicsPricesPrivalia)]
A -->|getHWKInventItemBarcodes| B
A -->|truncate + batch insert| E[(dynamics-pro · DynamicsHWKInventItemBarcodes)]
A -->|System.exit al terminar| F[Fin del proceso]
4. Dependencias principales
| Dependencia | Propósito |
|---|---|
spring-boot-starter | Núcleo de Spring Boot (sin web, es un runner CLI) |
com.hawkersco:dynamics-commons:1.0.25-SNAPSHOT | Entidades JPA (ProductDynamics, VariantDynamics, DynamicsPricesPrivalia, DynamicsHWKInventItemBarcodes) y servicios batch |
com.hawkersco:dynamics-client:1.0.25-SNAPSHOT | Cliente @HttpExchange (DynamicsDataClient) para la API REST de Dynamics 365 |
com.hawkersco:privalia-client:1.0.25-SNAPSHOT | Declarada en el pom.xml; sin uso detectado en el código actual (los precios de Privalia se obtienen vía DynamicsDataClient, no vía este cliente) |
spring-boot-starter-test (test) | JUnit 5 + Spring Test (sin suite de tests real; test.sh está vacío según el propio CLAUDE.md) |
5. API / Endpoints
No aplica a este proyecto. Es un batch/runner sin capa REST.
6. Integraciones externas
| Sistema | Protocolo | Dirección | Detalle |
|---|---|---|---|
Dynamics 365 (dynamics.base.url) | HTTP OAuth client_credentials (DynamicsDataClient) | Entrante | Productos/stock (paginado, 1000 por página, 3s entre páginas), variantes (mensual), precios de Privalia, códigos de barras |
PostgreSQL (dynamics-pro) | JDBC | Saliente | Persistencia de todos los datos sincronizados |
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.url / .username / .password | Credenciales de la BD dynamics-pro | ${dbDynamicsUrl}, etc. |
dynamics.login.* | OAuth client_credentials para Dynamics 365 Commerce | ${dynamicsLoginClientId}, etc. |
dynamics.picustomersetup.* | OAuth client_credentials independiente para Dynamics F&O (PI Customer Setup) | ${dynamicsPiClientId}, etc. |
slack.client.url / .auth.token / .channel.id | Configuración de Slack declarada, sin ningún uso real en el código (ver sección 13) | ${slackClientUrl}, etc. |
⚠️ Alerta de seguridad (severidad alta)
El fichero src/main/resources/application.properties (perfil local) contiene actualmente credenciales OAuth reales de Dynamics 365 Commerce y Dynamics F&O — las mismas ya señaladas como expuestas en gio-pos-sync-service e infranete-api (mismo tenant de producción) —, la contraseña de la base de datos PostgreSQL, y un token de bot de Slack (xoxb-...). Ninguno de estos valores se ha reproducido en este documento. Se recomienda:
- Rotar el
client-secretde ambas aplicaciones Azure AD (Commerce y F&O), coordinando con los demás proyectos que comparten estas credenciales. - Rotar la contraseña de BD y el token de Slack.
- 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. spring.jpa.hibernate.ddl-auto=none. Entidades relevantes: ProductDynamics, VariantDynamics, DynamicsPricesPrivalia, DynamicsHWKInventItemBarcodes. Los precios de Privalia y los códigos de barras se sincronizan con estrategia de truncar e insertar (reemplazo completo en cada ejecución); los productos se insertan en bloque tras recorrer los 6 dataAreaId; las variantes se insertan solo si no existen ya (upsert manual por búsqueda previa). 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 * * * *"). En cada ejecución:
- Si es el día 1 del mes a las 03:00 (hora del propio contenedor, no necesariamente la zona horaria configurada), ejecuta la actualización mensual de variantes: pagina todas las variantes de Dynamics e inserta solo las que no existen ya.
- Siempre: recorre los 6
dataAreaId(10, 25, 40, 50, 52, 60) paginando productos/stock y acumula la lista completa antes de insertarla (ver hallazgo crítico en la sección 13). - Siempre: sincroniza precios de Privalia de los últimos 3 meses (truncar + insertar).
- Siempre: sincroniza códigos de barras de inventario HWK (truncar + insertar).
- Cierra la JVM.
10. Ejecución en local
Requisitos previos: JDK 25, Maven, acceso a la BD dynamics-pro y credenciales OAuth válidas de Dynamics 365.
# Compilar sin tests
mvn -B -DskipTests clean install
# Compilar con tests
mvn clean install
# Ejecutar la aplicación localmente
mvn spring-boot:run
No hay suite de tests real. Al ser un CommandLineRunner, no expone Actuator/health: la verificación se hace revisando el log de consola o el número de registros en las tablas correspondientes tras la ejecución.
11. Despliegue
- Imagen: construida con
jib-maven-plugin(baseeclipse-temurin:25-jre,containerizingMode=packaged), publicada eneurope-west3-docker.pkg.dev/pi-saldum/pi-repo/products-dynamics-pi:<tag>. - Orquestación: Kubernetes
CronJob(k8s/cronjob.yaml) en el clúster GKEpi-cluster-hw, namespacepi, ejecutándose cada 15 minutos. - CI/CD (Jenkins): pipeline que construye, empuja la imagen y despliega el
CronJob.
Job de Jenkins: https://jenkins-pi.hawkersco.net/job/products-dynamics-pi/
12. Manejo de errores y logging
No hay una estrategia de excepciones centralizada (no hay @ControllerAdvice, es un runner). getProductDynamicsByDataAreaId captura InterruptedException (restaurando el flag de interrupción) y cualquier otra Exception por página, registrando con Level.SEVERE y continuando con la siguiente página. El resto del flujo (variantes, precios, códigos de barras) no tiene manejo de errores explícito: una excepción ahí propagaría y terminaría el proceso con fallo, quedando registrado como Job fallido en Kubernetes. No hay notificación a Slack pese a la configuración presente (ver hallazgo en la sección 13). Logging mediante java.util.logging.Logger estándar (consola).
13. Notas y consideraciones
- Posible colisión de
idProductDynamicsentredataAreaIddistintos: engetProductDynamicsByDataAreaId, la variableidProductDynamicsse declara e inicializa a1dentro del método, por lo que se reinicia en cada una de las 6 llamadas (una pordataAreaId). El resultado es que la lista acumuladaproductDynamicsListque se pasa ainsertProductDynamicsListcontiene productos de distintas empresas (dataAreaId) con IDs duplicados (1, 2, 3... repetidos 6 veces). SiidProductDynamicsse usa como clave primaria o identificador único en la tablaProductDynamics, esto podría causar sobrescritura de registros entre empresas o un error de restricción de unicidad al insertar, dependiendo de cómoinsertProductDynamicsListgestione los conflictos (no verificado en este repositorio, ya que la implementación vive endynamics-commons). Se recomienda usar un contador global (o un identificador compuesto pordataAreaId+ secuencia) en lugar de reiniciarlo por cada área de datos. - Slack configurado pero sin ningún uso real:
application.properties/application-pro.propertiesincluyenslack.client.url,.auth.tokeny.channel.id, y elCLAUDE.mdafirma explícitamente que hay "Notifications on batch completion/errors". Sin embargo, no hay dependenciaslack-clienten elpom.xmlni ninguna referencia aSlackClienten el código fuente. Es probable que la notificación se haya retirado en algún momento sin limpiar la configuración ni elCLAUDE.md. - Dependencia
privalia-clientsin uso: declarada en elpom.xml, pero los precios de Privalia se obtienen a través deDynamicsDataClient.getPricesPrivaliaFromDate(un endpoint de Dynamics, no de la API de Privalia directamente); no se ha encontrado ningún uso del clienteprivalia-clienten el código. - Condición de fecha basada en hora local del contenedor: la comprobación
day == 1 && hour == 3para la actualización mensual de variantes usaLocalDateTime.now()sin especificar zona horaria explícitamente en ese punto (a diferencia del cálculo de precios, que sí usaEurope/Madrid); el comportamiento depende de la zona horaria configurada en el contenedor/CronJob, que no se ha podido verificar en este documento. - Ver alerta de seguridad en la sección 7 sobre credenciales OAuth y de BD reales expuestas en
application.properties.