Zalando Client
1. Descripción general
zalando-client es una librería cliente reutilizable (JAR) que encapsula la comunicación con la API de Zalando (integración HA-EU — Hawkers/Zalando Europa). 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 consultar productos por EAN, enviar catálogo/precios, gestionar tipos de atributo y consultar/actualizar pedidos en Zalando.
La librería gestiona la autenticación OAuth2 (client credentials) de forma transparente vía interceptor, con caché de token de 30 minutos, y reescribe en tiempo de ejecución el placeholder literal merchantId presente en las rutas declaradas por el cliente, sustituyéndolo por un identificador de merchant fijo (ver detalle y consideración importante en la sección 13).
2. Información técnica
| Propiedad | Valor |
|---|---|
artifactId | zalando-client |
groupId | com.hawkersco |
version | 1.0.25-SNAPSHOT |
| Java | 25 |
| Spring Boot | 4.0.6 |
| Tipo de artefacto | JAR (librería; incluye además un Dockerfile, ver sección 13) |
| Módulos | Proyecto único (no multi-módulo) |
3. Arquitectura y diseño
Estructura del proyecto:
com.hawkersco.zalandoclient
├── ZalandoClientApplication.java # Clase @SpringBootApplication (residual, sin lógica)
├── client/
│ ├── ZalandoHaEuClient.java # Interfaz @HttpExchange con todas las operaciones de negocio
│ └── ZalandoHaEuLoginClient.java # Interfaz @HttpExchange para /auth/token (Basic Auth)
├── config/
│ ├── ZalandoClientHaEuConfiguration.java # @AutoConfiguration principal (interceptor Bearer + reescritura de merchantId)
│ ├── ZalandoClientHaEuLoginConfiguration.java # @AutoConfiguration del cliente de login (Basic Auth)
│ └── CacheStore.java # Cache genérica en memoria (Guava)
├── dto/
│ ├── LoginForm.java # Modelo de formulario de login; no usado por ZalandoHaEuLoginClient (ver sección 13)
│ ├── OrderZalandoData.java, OrderLineZalandoData.java, OrderItemZalandoData.java
├── model/
│ ├── ZalandoCatalogRequest.java, ZalandoPricesRequest.java
└── service/
├── IZalandoLoginService.java # Contrato: getBearerToken() / getMerchantId()
└── ZalandoLoginService.java # Implementación con @Cacheable, independiente y no conectada al interceptor real (ver sección 13)
Flujo principal (autenticación real, vía interceptor de ZalandoHaEuClient)
sequenceDiagram
participant Consumidor as Microservicio consumidor
participant Client as ZalandoHaEuClient
participant Cache as CacheStore (30 min)
participant LoginClient as ZalandoHaEuLoginClient
participant Zalando as API Zalando
Consumidor->>Client: zalandoOrders / zalandoUpdatePrices / zalandoProductSubmissions / ...
Client->>Cache: get("token")
alt Token en caché
Cache-->>Client: token válido
else Token ausente/expirado
Client->>LoginClient: zalandoLogin(grant_type=client_credentials, scope=access_token_only)
LoginClient->>Zalando: POST /auth/token (Basic Auth)
Zalando-->>LoginClient: { "access_token": "..." }
LoginClient-->>Client: token
Client->>Cache: add("token", "Bearer <token>")
end
Client->>Client: reescribe "/merchants/merchantId/" → "/merchants/<UUID fijo>/" en la URI
Client->>Zalando: request + Authorization: Bearer <token>
Zalando-->>Client: ResponseEntity<String>
Client-->>Consumidor: ResponseEntity<String>
ZalandoClientHaEuLoginConfiguration (@ConditionalOnProperty sobre zalando.base.url/key/pass) registra ZalandoHaEuLoginClient con un RestClient que fija Basic Auth (zalando.base.key/zalando.base.pass) como cabecera por defecto.
ZalandoClientHaEuConfiguration (@AutoConfiguration(after = ZalandoClientHaEuLoginConfiguration.class), @ConditionalOnProperty sobre zalando.base.url) registra ZalandoHaEuClient con un RestClient cuyo requestInterceptor:
- Resuelve el Bearer token (caché propia de 30 minutos, o solicita uno nuevo a
ZalandoHaEuLoginClient), con reintento tras 10 segundos si la respuesta es429(rate limit). - Reescribe la URI de la petición sustituyendo el placeholder literal
merchantId(presente tal cual en las rutas declaradas deZalandoHaEuClient, p. ej./merchants/merchantId/orders) por un UUID de merchant fijado como constante en el código (ver sección 13).
El registro de ambas autoconfiguraciones se realiza mediante el fichero estándar de Spring Boot:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
Camino paralelo no conectado (ZalandoLoginService): el paquete service/ contiene una implementación alternativa y completamente independiente de resolución de token y merchant ID (ZalandoLoginService, anotada con @Cacheable), que además consulta dinámicamente /auth/me para resolver el merchant ID real del token autenticado. Esta clase no está conectada al interceptor real de ZalandoClientHaEuConfiguration — ver detalle en la sección 13.
4. Dependencias principales
| Dependencia | Versión | Propósito |
|---|---|---|
spring-boot-autoconfigure | (gestionada SB4) | @AutoConfiguration / @SpringBootApplication |
spring-web | (gestionada SB4) | RestClient + @HttpExchange / HttpServiceProxyFactory |
org.json:json | 20251224 | Parseo de respuestas de token y merchant (JSONObject/JSONArray) |
com.google.guava:guava | 33.6.0-jre | Implementación de caché en CacheStore (CacheBuilder) |
com.google.code.gson:gson | (gestionada SB4) | Anotaciones @SerializedName en los modelos (soporte dual con Jackson) |
com.fasterxml.jackson.core:jackson-annotations | (gestionada SB4) | Anotaciones @JsonProperty en algunos modelos |
org.slf4j:slf4j-api | (gestionada SB4) | Logging (@Slf4j en las clases de configuración/servicio) |
org.projectlombok:lombok | 1.18.46 | Generación de boilerplate en los DTOs/modelos |
5. API / Endpoints
No aplica a este proyecto. zalando-client es una librería cliente JAR que no expone endpoints REST propios. Las operaciones que encapsula sobre la API de Zalando se detallan en la sección 6.
6. Integraciones externas
API de Zalando (HA-EU) — ZalandoHaEuClient
| Método | HTTP | Ruta remota | Descripción |
|---|---|---|---|
zalandoLoginInformation | GET | /auth/me | Consulta información del token/merchant autenticado |
zalandoProductByEan | GET | /products/identifiers/{ean} | Consulta un producto por EAN |
zalandoMerchantOutlines | GET | /merchants/merchantId/outlines | Lista los "outlines" (categorías) del merchant |
zalandoMerchantOutlinesDetail | GET | /merchants/merchantId/outlines/{outline} | Detalle de un outline concreto |
zalandoAttributeType | GET | /merchants/merchantId/attribute-types/{attribute-type} | Consulta un tipo de atributo |
zalandoValuesByAttributeType | GET | /merchants/merchantId/attribute-types/{attribute-type}/attributes | Lista los valores de un tipo de atributo |
zalandoValuesByAttributeTypeDetail | GET | /merchants/merchantId/attribute-types/{attribute-type}/attributes/{attribute-type-label} | Detalle de un valor de atributo |
zalandoProductSubmissions | POST | /merchants/merchantId/product-submissions | Envía un catálogo de productos (body JSON crudo) |
zalandoUpdatePrices | POST | /merchants/merchantId/prices | Actualiza precios de productos (body JSON crudo) |
zalandoOrders | GET | /merchants/merchantId/orders?page[size]=&order_status=&created_after=&created_before= | Lista pedidos filtrados por estado y rango de fechas |
zalandoGetProductSubmissions | GET | /merchants/merchantId/product-submissions | Consulta el estado de envíos de catálogo (método GET con @RequestBody, ver sección 13) |
zalandoGetOthers | GET | {othersPath} | Comodín genérico: cualquier ruta relativa pasada dinámicamente |
Endpoint OAuth2 — ZalandoHaEuLoginClient
| Método | HTTP | Ruta remota | Descripción |
|---|---|---|---|
zalandoLogin | POST | /auth/token | Obtiene un access token OAuth2 (client credentials, Basic Auth) |
Ejemplo de respuesta zalandoOrders (OrderZalandoData, resumida — formato JSON:API):
{
"data": [
{
"type": "order",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"attributes": {
"order_number": "ORD-000123",
"status": "open",
"order_lines_price_amount": 59.90,
"order_lines_price_currency": "EUR",
"shipping_address": { "first_name": "Cliente", "city": "Berlín", "country_code": "DE" }
}
}
],
"links": { "self": "...", "next": "..." },
"meta": { "number_of_results": 1 }
}
Ejemplo de payload zalandoUpdatePrices (ZalandoPricesRequest):
{
"product_prices": [
{
"ean": "8412345678901",
"sku": "SKU-001",
"sales_channel_id": "3fa85f64-...",
"regular_price": { "amount": "29.99", "currency": "EUR" }
}
]
}
Protocolo: HTTPS REST (JSON, con paginación estilo JSON:API en pedidos/productos). Autenticación: OAuth2 client credentials con Basic Auth para el login, Bearer token resultante cacheado 30 minutos e inyectado por el interceptor de ZalandoHaEuClient.
7. Configuración
El fichero src/main/resources/application.properties solo define spring.application.name=zalando-client; el resto de propiedades deben ser inyectadas por la aplicación consumidora.
Propiedades requeridas (prefijo zalando.base)
| Propiedad | Descripción | Ejemplo de valor |
|---|---|---|
zalando.base.url | URL base de la API de Zalando (activa ambas autoconfiguraciones) | ${ZALANDO_BASE_URL} |
zalando.base.key | Usuario de la aplicación privada Zalando (Basic Auth del login) | ${ZALANDO_APP_KEY} |
zalando.base.pass | Contraseña de la aplicación privada Zalando (Basic Auth del login) | ${ZALANDO_APP_PASSWORD} |
Importante:
ZalandoClientHaEuLoginConfigurationrequiere las tres propiedades (url,key,pass);ZalandoClientHaEuConfigurationsolo requiereurl, pero además depende en tiempo de ejecución de que el beanZalandoHaEuLoginClientexista (garantizado por el orden@AutoConfiguration(after = ...), aunque sin@ConditionalOnBeanexplícito — siZalandoHaEuLoginClientno se registra por faltarkey/pass, la inyección@Autowired ZalandoHaEuLoginClientenZalandoClientHaEuConfigurationfallará al arrancar).
Variables de entorno recomendadas
| Variable | Propiedad mapeada |
|---|---|
ZALANDO_BASE_URL | zalando.base.url |
ZALANDO_APP_KEY | zalando.base.key |
ZALANDO_APP_PASSWORD | zalando.base.pass |
8. Persistencia
No aplica a este proyecto. La librería no accede a ninguna base de datos (DataSourceAutoConfiguration excluida según CLAUDE.md). El estado en memoria se limita a la caché del token Bearer (CacheStore, TTL 30 minutos) dentro de ZalandoClientHaEuConfiguration.
9. Procesos programados y mensajería
No aplica a este proyecto. No existen jobs @Scheduled, listeners de colas/topics ni runners batch. El reintento tras 429 (handleRateLimit) es una espera bloqueante (Thread.sleep) dentro del propio hilo de la petición, no un mecanismo de reintento programado.
10. Ejecución en local
zalando-client es fundamentalmente una librería JAR, aunque, a diferencia de la mayoría de clientes del ecosistema, incluye un Dockerfile — ver observación en la sección 13.
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
./mvnw clean install
# Compilar sin tests
./mvnw clean install -DskipTests
# Ejecutar tests
./mvnw test
# Empaquetar
./mvnw clean package
Uso como dependencia en un microservicio consumidor
<dependency>
<groupId>com.hawkersco</groupId>
<artifactId>zalando-client</artifactId>
<version>1.0.25-SNAPSHOT</version>
</dependency>
La autoconfiguración se activa automáticamente al declarar zalando.base.url (y zalando.base.key/pass para el login) en la aplicación consumidora.
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.
| 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) |
El artefacto se publica como JAR en el registro Maven; el Jenkinsfile no construye ni publica ninguna imagen Docker, pese a la presencia de un Dockerfile en el repositorio (ver sección 13).
Job de Jenkins:
https://jenkins-pi.hawkersco.net/job/zalando-client/
12. Manejo de errores y logging
Los métodos de ZalandoHaEuClient y ZalandoHaEuLoginClient declaran throws RestClientResponseException. ZalandoClientHaEuConfiguration.fetchNewToken captura JSONException (logueando el error) y HttpClientErrorException con código 429 (reintentando tras 10 segundos); en cualquier otro caso de fallo no capturado explícitamente, o si la respuesta no contiene access_token, el método devuelve "" — la petición de negocio se enviará con Authorization: vacío en lugar de fallar explícitamente en la fase de autenticación. Ambas clases usan @Slf4j (SLF4J) para logging, a diferencia de la mayoría de otros clientes del ecosistema que no registran logs explícitamente.
13. Notas y consideraciones
-
Reescritura de
merchantIdcon un valor fijo, ignorando el merchant real del token: El interceptor deZalandoClientHaEuConfigurationsustituye el placeholdermerchantIdde la URL por una constante hardcodeada en el código (d4253bd6-bd49-4e96-a84a-134d58c91306), independientemente de qué credenciales/token se estén usando. Esto significa que todas las operaciones sobre/merchants/merchantId/...siempre apuntan al mismo merchant, sin posibilidad de parametrizarlo por configuración ni de resolverlo dinámicamente a partir del token autenticado — pese a que existe una implementación (ZalandoLoginService.getMerchantId()) que sí consulta/auth/mepara resolver el merchant real. Si Hawkers operase con más de un merchant en Zalando, este cliente no lo soportaría sin modificar el código fuente. -
ZalandoLoginServicees una implementación paralela no conectada: El paqueteservice/contiene una segunda estrategia completa de resolución de token (getBearerToken, con@Cacheable) y merchant ID (getMerchantId, también@Cacheable, resolviéndolo dinámicamente vía/auth/me), pero ningún componente de la autoconfiguración activa la usa — el interceptor real deZalandoHaEuClientimplementa su propia lógica de token independiente (CacheStorede Guava) y no invocaZalandoLoginServiceen ningún punto. Además, no se ha encontrado ninguna anotación@EnableCachingen el proyecto, por lo que las anotaciones@CacheabledeZalandoLoginServiceson inertes salvo que la aplicación consumidora habilite explícitamente el soporte de caché de Spring y registre cachés llamadas"token"y"merchantId"— lo que no está documentado como requisito en ningún fichero del proyecto. -
Dockerfilepresente pero con versión de Java inconsistente: ElDockerfileconstruye la imagen a partir deeclipse-temurin:17-jdk-alpine(Java 17), mientras que elpom.xmly elJenkinsfile(JDK25) usan Java 25. ElJenkinsfileno referencia ni construye esta imagen. Mismo patrón de artefacto Docker obsoleto/desalineado observado ensfcc-services-client(allí con Java 8). -
zalandoGetProductSubmissionses unGETcon@RequestBody: El método está anotado@HttpExchange(method = "GET", ...)pero declara un parámetro@RequestBody String json— patrón atípico (enviar un cuerpo en una peticiónGET), mismo caso observado enpim-client.updatePim. Pendiente de verificar si la API de Zalando realmente admite/requiere un body en esteGET. -
LoginFormno utilizado: Existe un DTOLoginFormcon camposgrant_type/scope, pero tantoZalandoHaEuLoginClient.zalandoLogincomo el interceptor deZalandoClientHaEuConfigurationconstruyen manualmente unLinkedMultiValueMap<String, String>en su lugar — el modelo queda huérfano, sin ningún método que lo utilice. -
Espera bloqueante (
Thread.sleep) ante rate limiting: TantoZalandoClientHaEuConfiguration.handleRateLimitcomoZalandoLoginService.retryAfterRateLimitbloquean el hilo actual durante 10 segundos ante un429, en lugar de usar un mecanismo de reintento no bloqueante (backoff asíncrono, cola de reintento, etc.) — en un entorno con pool de hilos limitado, múltiples llamadas concurrentes rate-limitadas podrían agotar el pool de threads del consumidor. -
Sin tests implementados: No se ha encontrado directorio
src/test/en el proyecto. -
ZalandoClientApplication.java: Clase principal de Spring Boot en el paquete raíz, sin funcionalidad operativa propia más allá de habilitar elDockerfilea ejecutarse como proceso (sin que haga nada útil, al no haber controladores). Artefacto residual de la generación inicial del proyecto con Spring Initializr.