Skip to main content

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

PropiedadValor
artifactIdzalando-client
groupIdcom.hawkersco
version1.0.25-SNAPSHOT
Java25
Spring Boot4.0.6
Tipo de artefactoJAR (librería; incluye además un Dockerfile, ver sección 13)
MódulosProyecto ú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:

  1. Resuelve el Bearer token (caché propia de 30 minutos, o solicita uno nuevo a ZalandoHaEuLoginClient), con reintento tras 10 segundos si la respuesta es 429 (rate limit).
  2. Reescribe la URI de la petición sustituyendo el placeholder literal merchantId (presente tal cual en las rutas declaradas de ZalandoHaEuClient, 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

DependenciaVersiónPropósito
spring-boot-autoconfigure(gestionada SB4)@AutoConfiguration / @SpringBootApplication
spring-web(gestionada SB4)RestClient + @HttpExchange / HttpServiceProxyFactory
org.json:json20251224Parseo de respuestas de token y merchant (JSONObject/JSONArray)
com.google.guava:guava33.6.0-jreImplementació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:lombok1.18.46Generació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étodoHTTPRuta remotaDescripción
zalandoLoginInformationGET/auth/meConsulta información del token/merchant autenticado
zalandoProductByEanGET/products/identifiers/{ean}Consulta un producto por EAN
zalandoMerchantOutlinesGET/merchants/merchantId/outlinesLista los "outlines" (categorías) del merchant
zalandoMerchantOutlinesDetailGET/merchants/merchantId/outlines/{outline}Detalle de un outline concreto
zalandoAttributeTypeGET/merchants/merchantId/attribute-types/{attribute-type}Consulta un tipo de atributo
zalandoValuesByAttributeTypeGET/merchants/merchantId/attribute-types/{attribute-type}/attributesLista los valores de un tipo de atributo
zalandoValuesByAttributeTypeDetailGET/merchants/merchantId/attribute-types/{attribute-type}/attributes/{attribute-type-label}Detalle de un valor de atributo
zalandoProductSubmissionsPOST/merchants/merchantId/product-submissionsEnvía un catálogo de productos (body JSON crudo)
zalandoUpdatePricesPOST/merchants/merchantId/pricesActualiza precios de productos (body JSON crudo)
zalandoOrdersGET/merchants/merchantId/orders?page[size]=&order_status=&created_after=&created_before=Lista pedidos filtrados por estado y rango de fechas
zalandoGetProductSubmissionsGET/merchants/merchantId/product-submissionsConsulta el estado de envíos de catálogo (método GET con @RequestBody, ver sección 13)
zalandoGetOthersGET{othersPath}Comodín genérico: cualquier ruta relativa pasada dinámicamente

Endpoint OAuth2 — ZalandoHaEuLoginClient

MétodoHTTPRuta remotaDescripción
zalandoLoginPOST/auth/tokenObtiene 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)

PropiedadDescripciónEjemplo de valor
zalando.base.urlURL base de la API de Zalando (activa ambas autoconfiguraciones)${ZALANDO_BASE_URL}
zalando.base.keyUsuario de la aplicación privada Zalando (Basic Auth del login)${ZALANDO_APP_KEY}
zalando.base.passContraseña de la aplicación privada Zalando (Basic Auth del login)${ZALANDO_APP_PASSWORD}

Importante: ZalandoClientHaEuLoginConfiguration requiere las tres propiedades (url, key, pass); ZalandoClientHaEuConfiguration solo requiere url, pero además depende en tiempo de ejecución de que el bean ZalandoHaEuLoginClient exista (garantizado por el orden @AutoConfiguration(after = ...), aunque sin @ConditionalOnBean explícito — si ZalandoHaEuLoginClient no se registra por faltar key/pass, la inyección @Autowired ZalandoHaEuLoginClient en ZalandoClientHaEuConfiguration fallará al arrancar).

Variables de entorno recomendadas

VariablePropiedad mapeada
ZALANDO_BASE_URLzalando.base.url
ZALANDO_APP_KEYzalando.base.key
ZALANDO_APP_PASSWORDzalando.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:

  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.
ParámetroValor
JDKJDK25 (tool Jenkins)
MavenMaven3 (tool Jenkins)
Repositorioeurope-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 merchantId con un valor fijo, ignorando el merchant real del token: El interceptor de ZalandoClientHaEuConfiguration sustituye el placeholder merchantId de 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/me para 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.

  • ZalandoLoginService es una implementación paralela no conectada: El paquete service/ 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 de ZalandoHaEuClient implementa su propia lógica de token independiente (CacheStore de Guava) y no invoca ZalandoLoginService en ningún punto. Además, no se ha encontrado ninguna anotación @EnableCaching en el proyecto, por lo que las anotaciones @Cacheable de ZalandoLoginService son 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.

  • Dockerfile presente pero con versión de Java inconsistente: El Dockerfile construye la imagen a partir de eclipse-temurin:17-jdk-alpine (Java 17), mientras que el pom.xml y el Jenkinsfile (JDK25) usan Java 25. El Jenkinsfile no referencia ni construye esta imagen. Mismo patrón de artefacto Docker obsoleto/desalineado observado en sfcc-services-client (allí con Java 8).

  • zalandoGetProductSubmissions es un GET con @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ón GET), mismo caso observado en pim-client.updatePim. Pendiente de verificar si la API de Zalando realmente admite/requiere un body en este GET.

  • LoginForm no utilizado: Existe un DTO LoginForm con campos grant_type/scope, pero tanto ZalandoHaEuLoginClient.zalandoLogin como el interceptor de ZalandoClientHaEuConfiguration construyen manualmente un LinkedMultiValueMap<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: Tanto ZalandoClientHaEuConfiguration.handleRateLimit como ZalandoLoginService.retryAfterRateLimit bloquean el hilo actual durante 10 segundos ante un 429, 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 el Dockerfile a ejecutarse como proceso (sin que haga nada útil, al no haber controladores). Artefacto residual de la generación inicial del proyecto con Spring Initializr.