Skip to main content

POS Client

1. Descripción general

pos-client es una librería cliente reutilizable (JAR) que encapsula la comunicación con las APIs de Microsoft Dynamics 365 Commerce (Retail/POS) para la gestión de clientes: búsqueda, creación y edición. El proyecto no expone ningún endpoint REST propio; se publica en el registro de artefactos Maven interno y es consumido por otros microservicios del ecosistema Hawkers que necesiten sincronizar datos de clientes con Dynamics 365 Commerce.

La librería gestiona la autenticación OAuth2 mediante el flujo client credentials, obteniendo un token nuevo en cada petición (sin caché), e inyectando además una cabecera oun (Organizational Unit Number) resuelta a partir de un contexto ThreadLocal que el consumidor debe establecer antes de cada llamada — soportando así operar contra múltiples unidades organizativas/tenants de Dynamics desde el mismo proceso.

2. Información técnica

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

3. Arquitectura y diseño

Estructura del proyecto:

com.hawkersco.posclient
├── PosClientApplication.java # Clase @SpringBootApplication (residual, sin lógica)
├── client/
│ ├── PosClient.java # Interfaz @HttpExchange: searchClients, createClient, editClient
│ └── PosTokenClient.java # Interfaz @HttpExchange para el endpoint OAuth2 de Dynamics
├── config/
│ ├── PosClientConfig.java # @AutoConfiguration principal (interceptor de token + cabecera OUN)
│ └── PosTokenClientConfig.java # @AutoConfiguration del cliente de token
├── context/
│ └── DynamicsContext.java # Holder ThreadLocal para la OUN (unidad organizativa)
└── pojo/
├── SearchClientRequest.java / SearchClientResponse.java
├── CreateCustomerRequest.java
├── EditCustomerRequest.java
├── CreateEditCustomerResponse.java
└── PosTokenRequest.java # POJO de request de token; no usado por PosTokenClient (ver sección 13)

Flujo principal

sequenceDiagram
participant Consumidor as Microservicio consumidor
participant Context as DynamicsContext (ThreadLocal)
participant Client as PosClient
participant TokenClient as PosTokenClient
participant Dynamics as Dynamics 365 Commerce

Consumidor->>Context: setOun(oun)
Consumidor->>Client: searchClients / createClient / editClient
Client->>TokenClient: getToken(grant_type, client_id, client_secret, resource, oun)
TokenClient->>Dynamics: POST /{tenant-id}/oauth2/token (form-urlencoded)
Dynamics-->>TokenClient: { "access_token": "..." }
TokenClient-->>Client: token
Client->>Dynamics: request + Authorization: Bearer <token> + oun: <oun>
Dynamics-->>Client: ResponseEntity<String> (JSON crudo)
Client-->>Consumidor: ResponseEntity<String>
Consumidor->>Context: clear() (en bloque finally)

PosTokenClientConfig (@AutoConfiguration, sin condición) registra PosTokenClient con un RestClient cuya baseUrl se construye concatenando dynamics.login.base.url + / + dynamics.login.tenant-id + /oauth2/token.

PosClientConfig (@AutoConfiguration(after = PosTokenClientConfig.class), también sin condición) registra PosClient con un RestClient cuyo requestInterceptor, en cada petición:

  1. Construye los parámetros de token (grant_type, client_id, client_secret, resource, oun — este último leído de DynamicsContext.getOun()).
  2. Llama a PosTokenClient.getToken(...) para obtener un access token fresco.
  3. Si la respuesta es 2xx, extrae access_token del JSON con org.json.JSONObject e inyecta las cabeceras Authorization: Bearer <token>, Content-Type: application/json, Accept: application/json y oun: <oun>.

DynamicsContext es un holder ThreadLocal<String> para la OUN: el consumidor debe llamar a DynamicsContext.setOun(valor) antes de invocar cualquier método de PosClient y a DynamicsContext.clear() en un bloque finally, para evitar fugas de OUN entre hilos en entornos con pool de threads.

El registro de ambas autoconfiguraciones se realiza mediante el fichero estándar de Spring Boot:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

4. Dependencias principales

DependenciaVersiónPropósito
spring-boot-starter(gestionada SB4)Base de Spring Boot (contexto, autoconfiguración)
spring-web(gestionada SB4)RestClient + @HttpExchange / HttpServiceProxyFactory
org.json:json20250107Extracción del campo access_token de la respuesta JSON de token
com.google.code.gson:gson(gestionada SB4)Anotaciones @SerializedName en los POJOs (soporte dual con Jackson)
com.fasterxml.jackson.core:jackson-annotations(gestionada SB4)Anotaciones @JsonProperty en los POJOs
org.projectlombok:lombok(gestionada; annotationProcessorPath fija 1.18.46)Generación de boilerplate en los POJOs
spring-boot-starter-test(gestionada SB4)Testing (scope test)

5. API / Endpoints

No aplica a este proyecto. pos-client es una librería cliente JAR que no expone endpoints REST propios. Las operaciones que encapsula sobre la API de Dynamics 365 Commerce se detallan en la sección 6.

6. Integraciones externas

API de gestión de clientes — Dynamics 365 Commerce (PosClient)

Método clienteHTTPRuta remotaDescripción
searchClientsPOST/Commerce/Customers/SearchByFields?%24top=80&api-version=7.3Busca clientes por uno o varios criterios de campo (Criteria)
createClientPOST/Commerce/CustomersCrea un nuevo cliente en Dynamics Commerce
editClientPATCH/Commerce/Customers('{customer}')Actualiza parcialmente un cliente existente por AccountNumber

Endpoint OAuth2 (PosTokenClient)

MétodoHTTPRuta remotaDescripción
getTokenPOST{dynamics.login.base.url}/{tenant-id}/oauth2/tokenObtiene un access token OAuth2 (grant type client credentials)

Ejemplo de payload searchClients (SearchClientRequest):

{
"CustomerSearchByFieldCriteria": {
"Criteria": [
{ "SearchTerm": "juan@example.com", "SearchField": { "Name": "Email", "Value": "juan@example.com" } }
],
"DataLevelValue": "Public"
}
}

Ejemplo de respuesta searchClients (SearchClientResponse, resumida):

{
"@odata.context": "...",
"value": [
{
"PartyNumber": "P-000123",
"RecordId": 456789,
"AccountNumber": "C-000123",
"FullName": "Juan Pérez",
"Email": "juan@example.com",
"Phone": "+34000000000",
"IsB2b": false
}
]
}

Ejemplo de payload createClient/editClient (CreateCustomerRequest/EditCustomerRequest, resumido — comparten prácticamente el mismo esquema de campos de Dynamics):

{
"FirstName": "Juan",
"LastName": "Pérez",
"Language": "es",
"CustomerGroup": "RETAIL",
"CurrencyCode": "EUR",
"Email": "juan@example.com",
"Addresses": [
{ "Street": "Calle Ejemplo", "StreetNumber": "1", "City": "Madrid", "ZipCode": "28001", "IsPrimary": true }
]
}

Protocolo: HTTPS REST (JSON para la API de clientes; application/x-www-form-urlencoded para el endpoint de token). Autenticación: OAuth2 client credentials (Azure AD / Dynamics), token obtenido en cada petición (sin caché) e inyectado como Authorization: Bearer, junto con la cabecera adicional oun para multi-tenant.

7. Configuración

application.properties solo define spring.application.name=pos-client; el resto de propiedades deben ser inyectadas por la aplicación consumidora.

Propiedades requeridas

PropiedadDescripciónEjemplo de valor
dynamics.base.urlURL base de la API Dynamics Commerce (usada por PosClient)${DYNAMICS_BASE_URL}
dynamics.login.base.urlURL base del servicio de login/OAuth2 (Azure AD)${DYNAMICS_LOGIN_BASE_URL}
dynamics.login.tenant-idIdentificador de tenant de Azure AD${DYNAMICS_TENANT_ID}
dynamics.login.grant-typeGrant type OAuth2 (p. ej. client_credentials)client_credentials
dynamics.login.client-idClient ID de la aplicación registrada en Azure AD${DYNAMICS_CLIENT_ID}
dynamics.login.client-secretClient secret de la aplicación registrada en Azure AD${DYNAMICS_CLIENT_SECRET}
dynamics.login.resourceRecurso/audiencia del token OAuth2 (URL del recurso Dynamics)${DYNAMICS_RESOURCE}

Importante — sin activación condicional: A diferencia de todos los demás clientes del ecosistema documentados hasta ahora, ni PosClientConfig ni PosTokenClientConfig usan @ConditionalOnProperty. Ambos beans se registran incondicionalmente; si el consumidor no define las propiedades dynamics.* requeridas, el arranque del contexto Spring fallará (inyección de @Value sin valor disponible), en lugar de omitir silenciosamente el registro del bean como ocurre en el resto de clientes.

Variables de entorno recomendadas

VariablePropiedad mapeada
DYNAMICS_BASE_URLdynamics.base.url
DYNAMICS_LOGIN_BASE_URLdynamics.login.base.url
DYNAMICS_TENANT_IDdynamics.login.tenant-id
DYNAMICS_CLIENT_IDdynamics.login.client-id
DYNAMICS_CLIENT_SECRETdynamics.login.client-secret
DYNAMICS_RESOURCEdynamics.login.resource

8. Persistencia

No aplica a este proyecto. La librería no accede a ninguna base de datos. No implementa ninguna caché de token (a diferencia de auro-client, hk-timeslogistics-client o meli-client); el único estado en memoria es el valor de OUN mantenido en DynamicsContext (ThreadLocal, por hilo).

9. Procesos programados y mensajería

No aplica a este proyecto. No existen jobs @Scheduled, listeners de colas/topics ni runners batch.

10. Ejecución en local

pos-client es una librería JAR, no una aplicación ejecutable. No tiene servidor embebido ni endpoint de health.

Requisitos previos

  • Java 25
  • Maven 3.x
  • Acceso al registro de artefactos Maven interno (europe-west3-maven.pkg.dev/pi-saldum/pi-repo-maven) para resolver/publicar dependencias.

Compilar e instalar en repositorio local

# Compilar e instalar
mvn clean install

# Compilar sin tests (como en CI)
mvn -B -DskipTests install

# Ejecutar tests
mvn test

Uso como dependencia en un microservicio consumidor

<dependency>
<groupId>com.hawkersco.posclient</groupId>
<artifactId>pos-client</artifactId>
<version>1.0.25-SNAPSHOT</version>
</dependency>

El consumidor debe declarar todas las propiedades dynamics.* requeridas (ver sección 7) y establecer/limpiar la OUN alrededor de cada llamada:

try {
DynamicsContext.setOun(oun);
posClient.searchClients(request);
} finally {
DynamicsContext.clear();
}

11. Despliegue

El pipeline de Jenkins (Jenkinsfile) consta de dos etapas:

  1. Checkout — descarga el código del repositorio.
  2. Publish to Artifact Registry — ejecuta mvn deploy -DskipTests para publicar el JAR en Google Artifact Registry.

CLAUDE.md documenta un pipeline de cuatro etapas (Build, KICS, SonarQube, Clean), que no se corresponde con el Jenkinsfile actual del repositorio (dos etapas: Checkout y Publish to Artifact Registry). Se documenta el Jenkinsfile realmente presente.

ParámetroValor
JDKJDK25 (tool Jenkins)
MavenMaven3 (tool Jenkins)
Repositorioeurope-west3-maven.pkg.dev/pi-saldum/pi-repo-maven (Artifact Registry GCP)

No existe Dockerfile ni despliegue como servicio independiente; el artefacto es un JAR publicado en el registro Maven.

Job de Jenkins:

https://jenkins-pi.hawkersco.net/job/pos-client/

12. Manejo de errores y logging

La librería no implementa ninguna estrategia propia de manejo de excepciones ni logging estructurado. Las excepciones de red o HTTP propagadas por RestClient (como RestClientResponseException) son responsabilidad del servicio consumidor.

En PosClientConfig, si la respuesta del endpoint de token no es 2xx, el interceptor no establece ninguna cabecera de autenticación y continúa ejecutando la petición original sin token — resultando en una llamada a Dynamics sin Authorization, que probablemente fallará con un 401 en lugar de fallar explícitamente en el punto de obtención del token. No hay configuración de logback ni de niveles de log específicos en la librería.

13. Notas y consideraciones

  • Sin @ConditionalOnProperty en ninguna autoconfiguración: A diferencia de todos los demás clientes del ecosistema documentados, PosClientConfig y PosTokenClientConfig no condicionan su activación a la presencia de propiedades — ambos beans siempre se registran, por lo que las aplicaciones consumidoras que incluyan esta dependencia deben definir todas las propiedades dynamics.*, o el contexto de Spring fallará al arrancar.

  • Sin caché de token: A diferencia de auro-client, hk-timeslogistics-client, meli-client o liverpool-client, pos-client solicita un token OAuth2 nuevo en cada llamada a PosClient (no hay CacheStore ni TTL). Esto implica una petición HTTP adicional al endpoint de token por cada operación de negocio, incrementando la latencia y la carga sobre el servidor de autenticación de Dynamics/Azure AD.

  • Fallo silencioso si el token falla: Si PosTokenClient.getToken(...) no devuelve un 2xx, el interceptor de PosClientConfig simplemente omite añadir las cabeceras de autenticación y dispara igualmente la petición de negocio — el fallo se manifestará como un error HTTP de Dynamics (probablemente 401), no como un fallo explícito en la obtención del token.

  • PosTokenRequest sin uso: Existe un POJO PosTokenRequest con los mismos campos (grant_type, client_id, client_secret, resource, oun) que se envían al endpoint de token, pero PosTokenClient.getToken recibe en su lugar un MultiValueMap<String, String> construido manualmente en PosClientConfig — el POJO no está cableado a ninguna llamada real. Mismo patrón de "modelo huérfano" observado en otros clientes del ecosistema (p. ej. MeliShipmentRequest en meli-client).

  • Uso de org.json.JSONObject para extraer el token: En lugar de usar Jackson o Gson (ya presentes como dependencias del proyecto y usados en el resto de POJOs), la extracción de access_token de la respuesta de token se hace con org.json.JSONObject, introduciendo una tercera librería de parseo JSON en el mismo proyecto.

  • DynamicsContext como responsabilidad manual del consumidor: El uso correcto de la librería exige que el consumidor llame a setOun/clear de forma disciplinada (idealmente en un bloque try/finally), ya que un olvido puede filtrar la OUN de una petición anterior a otra en entornos con reutilización de hilos (thread pools de servlets, @Async, etc.).

  • Ausencia de @ConditionalOnProperty combinada con dependencia entre autoconfiguraciones: PosClientConfig declara @AutoConfiguration(after = PosTokenClientConfig.class) para garantizar el orden de inicialización (necesita inyectar el bean PosTokenClient), pero al no haber condición de activación en ninguna de las dos, ambas siempre intentan registrarse — coherente con el punto anterior sobre fallo de arranque si faltan propiedades.

  • Sin tests implementados: No existe directorio src/test/ en el proyecto, confirmado por CLAUDE.md ("No tests currently exist").

  • PosClientApplication.java: Clase principal de Spring Boot en el paquete raíz, sin funcionalidad operativa. Artefacto residual de la generación inicial del proyecto con Spring Initializr, mismo patrón observado en otros clientes del ecosistema.