Skip to main content

Dynamics Client

1. Descripción general

dynamics-client es una librería cliente (JAR) que actúa como capa de acceso HTTP declarativo (@HttpExchange) sobre la API de Microsoft Dynamics 365, según la description de su pom.xml: "Dynamics Client for API".

El proyecto no es un servicio desplegable, sino una dependencia interna que se publica en el registro de artefactos Maven de Hawkers y es consumida por otros microservicios del ecosistema que necesitan interactuar con Dynamics 365 (ERP/CRM de Microsoft). Encapsula:

  • Autenticación OAuth2 con Azure AD (obtención de token vía formulario x-www-form-urlencoded), ejecutada automáticamente antes de cada petición mediante un interceptor de RestClient.
  • Propagación del contexto de Organization Unit Number (OUN) por hilo (ThreadLocal), para que las peticiones concurrentes de distintas unidades organizativas no se mezclen.
  • Operaciones declarativas sobre pedidos de venta, dimensiones de inventario, devoluciones, clientes, productos, variantes, precios, almacenes, tiendas retail e integraciones POS.
  • Modelos de datos (DTO) para los payloads de petición/respuesta de Dynamics, con doble anotación Gson/Jackson y soporte JAXB para un XML de tracking de envíos.

Dentro del ecosistema de microservicios de Hawkers, dynamics-client es la pieza de integración de bajo nivel que evita que cada microservicio (pedidos, stock, devoluciones, clientes...) tenga que reimplementar la autenticación y las llamadas HTTP contra Dynamics 365.

2. Información técnica

PropiedadValor
artifactIddynamics-client
groupIdcom.hawkersco
version1.0.25-SNAPSHOT
Java25
Spring Boot4.0.6 (spring-boot-starter-parent)
Tipo de artefactoJAR (librería, no ejecutable)
MódulosProyecto único (no multi-módulo)

3. Arquitectura y diseño

Estructura de paquetes bajo com.hawkersco.dynamicsclient:

com.hawkersco.dynamicsclient
├── DynamicsClientApplication.java # @SpringBootApplication (arranque para pruebas locales)
├── client/
│ ├── DynamicsLoginClient.java # @HttpExchange — obtención de token OAuth2
│ ├── DynamicsClient.java # @HttpExchange — pedidos de venta e inventario
│ └── DynamicsDataClient.java # @HttpExchange — entidades de datos (clientes, productos, precios, POS...)
├── conf/
│ ├── DynamicsLoginConf.java # @AutoConfiguration — bean DynamicsLoginClient
│ ├── DynamicsConf.java # @AutoConfiguration — bean DynamicsClient + interceptor de auth
│ └── DynamicsDataConfg.java # @AutoConfiguration — bean DynamicsDataClient + interceptor de auth
├── context/
│ └── DynamicsContext.java # ThreadLocal con el OUN de la petición actual
└── dto/ # 29 POJOs de request/response (Gson + Jackson, uno con JAXB/XML)

Las tres clases de conf/ se registran como auto-configuraciones Spring Boot en META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, por lo que cualquier microservicio que añada dynamics-client como dependencia obtiene los beans DynamicsLoginClient, DynamicsClient y DynamicsDataClient automáticamente, condicionados a que las propiedades dynamics.* requeridas estén presentes (@ConditionalOnProperty).

Flujo principal de autenticación y llamada

sequenceDiagram
participant Consumidor as Microservicio consumidor
participant DynamicsClient
participant Interceptor as RestClient Interceptor
participant DynamicsLoginClient
participant D365 as Dynamics 365 API

Consumidor->>DynamicsClient: llamada (ej. sendOrder)
DynamicsClient->>Interceptor: intercepta petición saliente
Interceptor->>DynamicsLoginClient: login(tenantId, form OAuth2)
DynamicsLoginClient->>D365: POST /{tenantId}/oauth2/token
D365-->>DynamicsLoginClient: access_token
Interceptor->>Interceptor: sleep 2s (rate limit Dynamics)
Interceptor->>Interceptor: set header Authorization: bearer ...
Interceptor->>Interceptor: set header oun (DynamicsContext.getOun())
Interceptor->>D365: ejecuta petición original con headers
D365-->>Consumidor: respuesta (JSON/String)

DynamicsDataConfg sigue el mismo patrón de interceptor para DynamicsDataClient, pero sin el sleep de 2s y enviando el campo oun del formulario de login vacío (no usa DynamicsContext).

4. Dependencias principales

DependenciaPropósito
spring-boot-starterNúcleo de Spring Boot y auto-configuración.
spring-webRestClient, HttpServiceProxyFactory y anotaciones @HttpExchange.
org.projectlombok:lombokGeneración de getters/setters/constructores en los DTO.
com.google.code.gson:gsonSerialización/deserialización JSON (formato usado por Dynamics).
jakarta.xml.bind:jakarta.xml.bind-apiSoporte JAXB para el DTO XML de tracking de envíos (ShipmentTrackingDynamicsXml).
com.fasterxml.jackson.core:jackson-annotationsAnotaciones Jackson complementarias a Gson en los DTO.
com.google.cloud.artifactregistry:artifactregistry-maven-wagon (extensión de build)Publicación del JAR en Google Artifact Registry (distributionManagement).

No se listan dependencias de test: el proyecto no contiene módulo de tests.

5. API / Endpoints

No aplica. dynamics-client no expone endpoints REST propios: es una librería consumida como dependencia por otros microservicios. La clase DynamicsClientApplication (@SpringBootApplication) existe únicamente como contexto de arranque para desarrollo/pruebas locales, no para servir tráfico en producción.

6. Integraciones externas

SistemaProtocoloDirecciónDetalle
Azure AD (OAuth2 token endpoint)HTTPS, POST /{tenantId}/oauth2/token, form-urlencodedSalienteAutenticación de DynamicsClient y DynamicsDataClient vía DynamicsLoginClient.
Microsoft Dynamics 365 — Commerce APIHTTPS, JSONSalienteDynamicsClient: pedidos de venta (/Commerce/SalesOrders), combinaciones de dimensiones de inventario.
Microsoft Dynamics 365 — Data Entities APIHTTPS, JSON (OData)SalienteDynamicsDataClient: clientes, productos, variantes, precios, almacenes, tiendas retail, devoluciones, POS (HWKExternalApp*), transacciones retail.

Todas las integraciones son de tipo cliente saliente (el proyecto llama a Dynamics 365; no recibe llamadas entrantes).

7. Configuración

El proyecto no incluye application.properties/application.yml propio (es una librería); las propiedades se inyectan vía @Value y deben ser definidas por el microservicio consumidor. Claves requeridas por cada @AutoConfiguration (condicionadas con @ConditionalOnProperty):

ClaveDescripciónEjemplo de valor
dynamics.login.base.urlURL base del endpoint de token OAuth2 (Azure AD).https://login.microsoftonline.com
dynamics.base.urlURL base de la API principal de Dynamics 365 (Commerce).https://<entorno>.dynamics.com
dynamics.login.tenant-idTenant de Azure AD.${DYNAMICS_TENANT_ID}
dynamics.login.client-idClient ID de la app registrada (cliente principal).${DYNAMICS_CLIENT_ID}
dynamics.login.client-secretClient secret de la app registrada (cliente principal).********
dynamics.login.resourceResource/audience del token OAuth2 (cliente principal).${DYNAMICS_RESOURCE}
dynamics.login.grant-typeGrant type OAuth2 (compartido por ambos clientes).client_credentials
dynamics.picustomersetup.urlURL base de la API de Data Entities (DynamicsDataClient).https://<entorno>.dynamics.com
dynamics.picustomersetup.login.client-idClient ID de la app registrada para el cliente de datos.${DYNAMICS_DATA_CLIENT_ID}
dynamics.picustomersetup.login.client-secretClient secret de la app registrada para el cliente de datos.********
dynamics.picustomersetup.login.resourceResource/audience del token OAuth2 (cliente de datos).${DYNAMICS_DATA_RESOURCE}

Si falta alguna de las propiedades requeridas por un @AutoConfiguration, el bean correspondiente (DynamicsLoginClient, DynamicsClient o DynamicsDataClient) simplemente no se registra, sin error de arranque.

8. Persistencia

No aplica. El proyecto no gestiona base de datos ni realiza persistencia propia; todos los DTO son objetos de transporte para las llamadas HTTP a Dynamics 365.

9. Procesos programados y mensajería

No aplica. No existen @Scheduled, @KafkaListener ni @RabbitListener en el código. El único comportamiento "temporizado" es el Thread.sleep(2000) dentro del interceptor de DynamicsConf, que no es un job programado sino una espera defensiva ante el rate limit de Dynamics tras la obtención de token.

10. Ejecución en local

Requisitos previos:

  • JDK 25.
  • Maven 3.
  • Credenciales válidas de Azure AD/Dynamics 365 (proporcionadas por el consumidor vía las propiedades de la sección 7).

Comandos:

# Build con tests (no hay tests en el repo, la fase test no ejecuta nada)
mvn clean install

# Build sin tests (equivalente al comportamiento de CI)
mvn -DskipTests clean install

Al ser una librería, no se "levanta" como servicio independiente ni expone actuator/health. Para probarla en local es necesario:

  1. Instalarla en el repositorio Maven local (mvn clean install) o consumir la versión publicada en Artifact Registry.
  2. Añadirla como dependencia en un microservicio consumidor que defina las propiedades dynamics.* de la sección 7.
  3. Verificar el correcto arranque comprobando que los beans DynamicsClient/DynamicsDataClient se inyectan sin error en el microservicio consumidor.

11. Despliegue

El proyecto se publica como artefacto Maven en Google Artifact Registry (artifactregistry://europe-west3-maven.pkg.dev/pi-saldum/pi-repo-maven), no se despliega como contenedor ni servicio.

El Jenkinsfile define un pipeline de dos etapas:

  1. Checkoutcheckout scm.
  2. Publish to Artifact Registrymvn deploy -DskipTests.

Job de Jenkins del proyecto: https://jenkins-pi.hawkersco.net/job/dynamics-client/

12. Manejo de errores y logging

  • DynamicsConf y DynamicsDataConfg capturan cualquier excepción producida durante la obtención del token o la construcción de la petición dentro del interceptor (catch (Exception e)), registrando el mensaje con java.util.logging.Logger a nivel WARNING y dejando que la petición HTTP se ejecute igualmente (execution.execute(request, body)), aunque el token/headers no se hayan podido establecer.
  • En DynamicsConf, la interrupción del hilo durante el Thread.sleep se captura explícitamente (InterruptedException), restaurando el flag de interrupción (Thread.currentThread().interrupt()) y registrando el mensaje.
  • No hay códigos de error propios ni un @ControllerAdvice: al ser una librería sin capa REST, los errores de Dynamics 365 se propagan tal cual en el ResponseEntity<String> devuelto (código de estado y cuerpo crudo), quedando a cargo del microservicio consumidor interpretarlos.
  • El logging usa java.util.logging (JUL) en lugar de SLF4J/Logback; no hay configuración de formato ni destino de logs en el propio proyecto (hereda la configuración del consumidor).

13. Notas y consideraciones

  • El interceptor de autenticación solicita un token OAuth2 en cada petición HTTP, sin caché de token (a diferencia de otros clientes del ecosistema, como auro-client, que sí cachean el token). Esto implica una llamada adicional a Azure AD por cada operación de negocio, más el Thread.sleep(2000) en DynamicsConf, lo que puede introducir latencia notable bajo carga.
  • Si la respuesta de login no es 2xx, el interceptor no lanza excepción ni aborta la petición: simplemente no añade los headers de autenticación y continúa (execution.execute(request, body)), lo que puede derivar en peticiones a Dynamics 365 sin token válido y errores 401 silenciosos aguas abajo si no se revisan los logs.
  • DynamicsContext (ThreadLocal) requiere que el consumidor llame explícitamente a clear() tras cada petición para evitar fugas de memoria/contexto entre hilos reutilizados (pool de threads); el propio dynamics-client no gestiona este ciclo de vida automáticamente.
  • No existen tests unitarios en el repositorio; el flag -DskipTests en Jenkins refleja esto directamente, no es una omisión de CI.
  • Los DTO combinan anotaciones Gson (@SerializedName) y Jackson (@JsonProperty) simultáneamente sobre los mismos campos, indicando que distintos consumidores podrían serializar con una u otra librería; mantener ambas sincronizadas es responsabilidad manual al añadir nuevos campos.
  • El endpoint getInventDimensionsCombinations en DynamicsClient usa @PathVariable sobre un parámetro de query ($top={top}) en lugar de @RequestParam; funciona por la forma en que Spring resuelve plantillas de URI en @HttpExchange, pero es un patrón poco convencional a tener en cuenta si se depuran problemas de serialización de parámetros.