Skip to main content

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

CampoValor
artifactIdproducts-dynamics-pi
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 (ProductsDynamicsPiRunner) que ejecuta todo el flujo y cierra la JVM al finalizar.

  • com.hawkersco.productsdynamicspi — clase principal (ProductsDynamicsPiApplication) y el runner.
  • .configProductsDynamicsPiConfig (declaración de beans, PersistenceManagedTypesScanner para las entidades externas de dynamics-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

DependenciaPropósito
spring-boot-starterNúcleo de Spring Boot (sin web, es un runner CLI)
com.hawkersco:dynamics-commons:1.0.25-SNAPSHOTEntidades JPA (ProductDynamics, VariantDynamics, DynamicsPricesPrivalia, DynamicsHWKInventItemBarcodes) y servicios batch
com.hawkersco:dynamics-client:1.0.25-SNAPSHOTCliente @HttpExchange (DynamicsDataClient) para la API REST de Dynamics 365
com.hawkersco:privalia-client:1.0.25-SNAPSHOTDeclarada 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

SistemaProtocoloDirecciónDetalle
Dynamics 365 (dynamics.base.url)HTTP OAuth client_credentials (DynamicsDataClient)EntranteProductos/stock (paginado, 1000 por página, 3s entre páginas), variantes (mensual), precios de Privalia, códigos de barras
PostgreSQL (dynamics-pro)JDBCSalientePersistencia 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).

ClaveDescripciónEjemplo (producción)
spring.datasource.url / .username / .passwordCredenciales 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.idConfiguració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:

  1. Rotar el client-secret de ambas aplicaciones Azure AD (Commerce y F&O), coordinando con los demás proyectos que comparten estas credenciales.
  2. Rotar la contraseña de BD y el token de Slack.
  3. Sustituir los valores hardcodeados de application.properties por credenciales de un entorno de desarrollo aislado.
  4. 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:

  1. 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.
  2. 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).
  3. Siempre: sincroniza precios de Privalia de los últimos 3 meses (truncar + insertar).
  4. Siempre: sincroniza códigos de barras de inventario HWK (truncar + insertar).
  5. 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 (base eclipse-temurin:25-jre, containerizingMode=packaged), publicada en europe-west3-docker.pkg.dev/pi-saldum/pi-repo/products-dynamics-pi:<tag>.
  • Orquestación: Kubernetes CronJob (k8s/cronjob.yaml) en el clúster GKE pi-cluster-hw, namespace pi, 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 idProductDynamics entre dataAreaId distintos: en getProductDynamicsByDataAreaId, la variable idProductDynamics se declara e inicializa a 1 dentro del método, por lo que se reinicia en cada una de las 6 llamadas (una por dataAreaId). El resultado es que la lista acumulada productDynamicsList que se pasa a insertProductDynamicsList contiene productos de distintas empresas (dataAreaId) con IDs duplicados (1, 2, 3... repetidos 6 veces). Si idProductDynamics se usa como clave primaria o identificador único en la tabla ProductDynamics, esto podría causar sobrescritura de registros entre empresas o un error de restricción de unicidad al insertar, dependiendo de cómo insertProductDynamicsList gestione los conflictos (no verificado en este repositorio, ya que la implementación vive en dynamics-commons). Se recomienda usar un contador global (o un identificador compuesto por dataAreaId + secuencia) en lugar de reiniciarlo por cada área de datos.
  • Slack configurado pero sin ningún uso real: application.properties/application-pro.properties incluyen slack.client.url, .auth.token y .channel.id, y el CLAUDE.md afirma explícitamente que hay "Notifications on batch completion/errors". Sin embargo, no hay dependencia slack-client en el pom.xml ni ninguna referencia a SlackClient en el código fuente. Es probable que la notificación se haya retirado en algún momento sin limpiar la configuración ni el CLAUDE.md.
  • Dependencia privalia-client sin uso: declarada en el pom.xml, pero los precios de Privalia se obtienen a través de DynamicsDataClient.getPricesPrivaliaFromDate (un endpoint de Dynamics, no de la API de Privalia directamente); no se ha encontrado ningún uso del cliente privalia-client en el código.
  • Condición de fecha basada en hora local del contenedor: la comprobación day == 1 && hour == 3 para la actualización mensual de variantes usa LocalDateTime.now() sin especificar zona horaria explícitamente en ese punto (a diferencia del cálculo de precios, que sí usa Europe/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.