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
| Propiedad | Valor |
|---|---|
artifactId | pos-client |
groupId | com.hawkersco.posclient |
version | 1.0.25-SNAPSHOT |
| Java | 25 |
| Spring Boot | 4.0.6 |
| Tipo de artefacto | JAR (librería, no ejecutable) |
| Módulos | Proyecto ú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:
- Construye los parámetros de token (
grant_type,client_id,client_secret,resource,oun— este último leído deDynamicsContext.getOun()). - Llama a
PosTokenClient.getToken(...)para obtener un access token fresco. - Si la respuesta es 2xx, extrae
access_tokendel JSON conorg.json.JSONObjecte inyecta las cabecerasAuthorization: Bearer <token>,Content-Type: application/json,Accept: application/jsonyoun: <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
| Dependencia | Versión | Propósito |
|---|---|---|
spring-boot-starter | (gestionada SB4) | Base de Spring Boot (contexto, autoconfiguración) |
spring-web | (gestionada SB4) | RestClient + @HttpExchange / HttpServiceProxyFactory |
org.json:json | 20250107 | Extracció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 cliente | HTTP | Ruta remota | Descripción |
|---|---|---|---|
searchClients | POST | /Commerce/Customers/SearchByFields?%24top=80&api-version=7.3 | Busca clientes por uno o varios criterios de campo (Criteria) |
createClient | POST | /Commerce/Customers | Crea un nuevo cliente en Dynamics Commerce |
editClient | PATCH | /Commerce/Customers('{customer}') | Actualiza parcialmente un cliente existente por AccountNumber |
Endpoint OAuth2 (PosTokenClient)
| Método | HTTP | Ruta remota | Descripción |
|---|---|---|---|
getToken | POST | {dynamics.login.base.url}/{tenant-id}/oauth2/token | Obtiene 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
| Propiedad | Descripción | Ejemplo de valor |
|---|---|---|
dynamics.base.url | URL base de la API Dynamics Commerce (usada por PosClient) | ${DYNAMICS_BASE_URL} |
dynamics.login.base.url | URL base del servicio de login/OAuth2 (Azure AD) | ${DYNAMICS_LOGIN_BASE_URL} |
dynamics.login.tenant-id | Identificador de tenant de Azure AD | ${DYNAMICS_TENANT_ID} |
dynamics.login.grant-type | Grant type OAuth2 (p. ej. client_credentials) | client_credentials |
dynamics.login.client-id | Client ID de la aplicación registrada en Azure AD | ${DYNAMICS_CLIENT_ID} |
dynamics.login.client-secret | Client secret de la aplicación registrada en Azure AD | ${DYNAMICS_CLIENT_SECRET} |
dynamics.login.resource | Recurso/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
PosClientConfigniPosTokenClientConfigusan@ConditionalOnProperty. Ambos beans se registran incondicionalmente; si el consumidor no define las propiedadesdynamics.*requeridas, el arranque del contexto Spring fallará (inyección de@Valuesin valor disponible), en lugar de omitir silenciosamente el registro del bean como ocurre en el resto de clientes.
Variables de entorno recomendadas
| Variable | Propiedad mapeada |
|---|---|
DYNAMICS_BASE_URL | dynamics.base.url |
DYNAMICS_LOGIN_BASE_URL | dynamics.login.base.url |
DYNAMICS_TENANT_ID | dynamics.login.tenant-id |
DYNAMICS_CLIENT_ID | dynamics.login.client-id |
DYNAMICS_CLIENT_SECRET | dynamics.login.client-secret |
DYNAMICS_RESOURCE | dynamics.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:
- Checkout — descarga el código del repositorio.
- Publish to Artifact Registry — ejecuta
mvn deploy -DskipTestspara 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ámetro | Valor |
|---|---|
| JDK | JDK25 (tool Jenkins) |
| Maven | Maven3 (tool Jenkins) |
| Repositorio | europe-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
@ConditionalOnPropertyen ninguna autoconfiguración: A diferencia de todos los demás clientes del ecosistema documentados,PosClientConfigyPosTokenClientConfigno 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 propiedadesdynamics.*, o el contexto de Spring fallará al arrancar. -
Sin caché de token: A diferencia de
auro-client,hk-timeslogistics-client,meli-clientoliverpool-client,pos-clientsolicita un token OAuth2 nuevo en cada llamada aPosClient(no hayCacheStoreni 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 dePosClientConfigsimplemente 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. -
PosTokenRequestsin uso: Existe un POJOPosTokenRequestcon los mismos campos (grant_type,client_id,client_secret,resource,oun) que se envían al endpoint de token, peroPosTokenClient.getTokenrecibe en su lugar unMultiValueMap<String, String>construido manualmente enPosClientConfig— el POJO no está cableado a ninguna llamada real. Mismo patrón de "modelo huérfano" observado en otros clientes del ecosistema (p. ej.MeliShipmentRequestenmeli-client). -
Uso de
org.json.JSONObjectpara 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 deaccess_tokende la respuesta de token se hace conorg.json.JSONObject, introduciendo una tercera librería de parseo JSON en el mismo proyecto. -
DynamicsContextcomo responsabilidad manual del consumidor: El uso correcto de la librería exige que el consumidor llame asetOun/clearde forma disciplinada (idealmente en un bloquetry/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
@ConditionalOnPropertycombinada con dependencia entre autoconfiguraciones:PosClientConfigdeclara@AutoConfiguration(after = PosTokenClientConfig.class)para garantizar el orden de inicialización (necesita inyectar el beanPosTokenClient), 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 porCLAUDE.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.