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 deRestClient. - 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
| Propiedad | Valor |
|---|---|
artifactId | dynamics-client |
groupId | com.hawkersco |
version | 1.0.25-SNAPSHOT |
| Java | 25 |
| Spring Boot | 4.0.6 (spring-boot-starter-parent) |
| Tipo de artefacto | JAR (librería, no ejecutable) |
| Módulos | Proyecto ú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
| Dependencia | Propósito |
|---|---|
spring-boot-starter | Núcleo de Spring Boot y auto-configuración. |
spring-web | RestClient, HttpServiceProxyFactory y anotaciones @HttpExchange. |
org.projectlombok:lombok | Generación de getters/setters/constructores en los DTO. |
com.google.code.gson:gson | Serialización/deserialización JSON (formato usado por Dynamics). |
jakarta.xml.bind:jakarta.xml.bind-api | Soporte JAXB para el DTO XML de tracking de envíos (ShipmentTrackingDynamicsXml). |
com.fasterxml.jackson.core:jackson-annotations | Anotaciones 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
| Sistema | Protocolo | Dirección | Detalle |
|---|---|---|---|
| Azure AD (OAuth2 token endpoint) | HTTPS, POST /{tenantId}/oauth2/token, form-urlencoded | Saliente | Autenticación de DynamicsClient y DynamicsDataClient vía DynamicsLoginClient. |
| Microsoft Dynamics 365 — Commerce API | HTTPS, JSON | Saliente | DynamicsClient: pedidos de venta (/Commerce/SalesOrders), combinaciones de dimensiones de inventario. |
| Microsoft Dynamics 365 — Data Entities API | HTTPS, JSON (OData) | Saliente | DynamicsDataClient: 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):
| Clave | Descripción | Ejemplo de valor |
|---|---|---|
dynamics.login.base.url | URL base del endpoint de token OAuth2 (Azure AD). | https://login.microsoftonline.com |
dynamics.base.url | URL base de la API principal de Dynamics 365 (Commerce). | https://<entorno>.dynamics.com |
dynamics.login.tenant-id | Tenant de Azure AD. | ${DYNAMICS_TENANT_ID} |
dynamics.login.client-id | Client ID de la app registrada (cliente principal). | ${DYNAMICS_CLIENT_ID} |
dynamics.login.client-secret | Client secret de la app registrada (cliente principal). | ******** |
dynamics.login.resource | Resource/audience del token OAuth2 (cliente principal). | ${DYNAMICS_RESOURCE} |
dynamics.login.grant-type | Grant type OAuth2 (compartido por ambos clientes). | client_credentials |
dynamics.picustomersetup.url | URL base de la API de Data Entities (DynamicsDataClient). | https://<entorno>.dynamics.com |
dynamics.picustomersetup.login.client-id | Client ID de la app registrada para el cliente de datos. | ${DYNAMICS_DATA_CLIENT_ID} |
dynamics.picustomersetup.login.client-secret | Client secret de la app registrada para el cliente de datos. | ******** |
dynamics.picustomersetup.login.resource | Resource/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:
- Instalarla en el repositorio Maven local (
mvn clean install) o consumir la versión publicada en Artifact Registry. - Añadirla como dependencia en un microservicio consumidor que defina las propiedades
dynamics.*de la sección 7. - Verificar el correcto arranque comprobando que los beans
DynamicsClient/DynamicsDataClientse 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:
- Checkout —
checkout scm. - Publish to Artifact Registry —
mvn deploy -DskipTests.
Job de Jenkins del proyecto: https://jenkins-pi.hawkersco.net/job/dynamics-client/
12. Manejo de errores y logging
DynamicsConfyDynamicsDataConfgcapturan 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 conjava.util.logging.Loggera nivelWARNINGy 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 elThread.sleepse 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 elResponseEntity<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 elThread.sleep(2000)enDynamicsConf, 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 aclear()tras cada petición para evitar fugas de memoria/contexto entre hilos reutilizados (pool de threads); el propiodynamics-clientno gestiona este ciclo de vida automáticamente.- No existen tests unitarios en el repositorio; el flag
-DskipTestsen 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
getInventDimensionsCombinationsenDynamicsClientusa@PathVariablesobre 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.