API Connectors
1. Descripción general
api-connectors es una librería Java pura (sin Spring Boot, sin contexto de aplicación, sin servidor embebido) que encapsula los clientes HTTP necesarios para comunicarse con las principales APIs externas del ecosistema de e-commerce y marketplaces de Hawkers.
Actúa como capa de abstracción sobre las APIs de Amazon, MercadoLibre, Trustpilot, Zalando, Salesforce Commerce Cloud y Google Analytics, proporcionando:
- Autenticación automática y refresco de tokens (OAuth2, AWS SigV4, service account).
- Reintentos con backoff y gestión de rate limiting (
Retry-After, 429). - Paginación transparente (scroll, cursor, offset) que devuelve colecciones completas.
- Modelos tipados (POJOs con Lombok y Gson/JAXB) listos para usar en los microservicios consumidores.
No es un microservicio desplegable. Se publica como artefacto Maven en el Artifact Registry de Google Cloud y los microservicios de la plataforma PI lo incluyen como dependencia.
2. Información técnica
| Propiedad | Valor |
|---|---|
artifactId | api-connectors |
groupId | com.hawkersco.connectors |
versión | 1.0.25-SNAPSHOT |
| Java | 25 |
| Spring Boot | No aplica (librería Java pura) |
| Tipo de artefacto | JAR |
| Módulos | Proyecto simple (no multi-módulo) |
| Repositorio Maven | europe-west3-maven.pkg.dev/pi-saldum/pi-repo-maven |
3. Arquitectura y diseño
La librería está organizada por vendedor/plataforma bajo el paquete raíz com.hawkersco.connectors:
com.hawkersco.connectors/
├── amazon/
│ └── AmazonRequestSigner # Firma AWS SigV4 (utilidad estática)
├── google/
│ └── analytics/
│ └── AnalyticsClient # Google Analytics Reporting API v4
├── meli/
│ ├── commons/
│ │ ├── MeliUtils # OAuth2 token refresh (sincronizado)
│ │ └── deserializers/
│ │ └── OffsetDateTimeDeserializer
│ ├── orders/
│ │ ├── MeliOrdersClient # Órdenes con scroll-based pagination
│ │ └── model/ # Order, OrderItem, Payment, Buyer...
│ ├── shipments/
│ │ ├── MeliShipmentsClient # Envíos + costes
│ │ └── model/
│ ├── visits/
│ │ ├── MeliVisitsClient # Visitas a items por vendedor
│ │ └── model/
│ └── campaigns/
│ ├── MeliCampaignsClient # Campañas publicitarias + métricas
│ └── model/
├── salesforce/
│ └── commerce/
│ └── catalog/
│ ├── CustomCatalogFeedClient # SFTP + JAXB XML
│ └── model/
├── trustpilot/
│ ├── commons/
│ │ └── TrustpilotClient # Clase base con auth OAuth2
│ ├── reviews/
│ │ ├── TrustpilotProductReviewsClient
│ │ ├── TrustpilotServiceReviewsClient
│ │ └── model/
│ ├── products/
│ │ ├── TrustpilotProductsClient
│ │ └── model/
│ ├── invitations/
│ │ ├── TrustpilotInvitationsClient
│ │ └── model/
│ └── deserializers/
└── zalando/
├── orders/
│ ├── ZalandoOrdersClient # Órdenes con población recursiva
│ └── model/
└── products/
└── ZalandoProductsClient # Productos vía GraphQL
Patrones compartidos
Todos los clientes siguen las mismas convenciones de diseño:
-
Configuración mediante setters encadenados (
@Accessors(chain = true)). El consumidor instancia el cliente pasando las credenciales por constructor y ajusta URLs/timeouts mediante fluent API. -
Caché de token con refresco sincronizado. Cada cliente mantiene
authTokenyauthTokenLastUpdate. El refresco solo se ejecuta si han transcurrido más deminMillisBetweenTokenUpdates(por defecto 5000 ms) para evitar tormentas de refresh bajo concurrencia. -
Reintentos con backoff. Todos los métodos HTTP utilizan un bucle de hasta
requestMaxRetries(por defecto 3) intentos:- 401/403 → refresco de token antes del siguiente intento.
- 429 → respeta cabecera
Retry-After; si no está presente, esperadefaultSleepMillisAfterTooManyRequests(20 s por defecto). - Otros errores → espera
sleepAfterRequestRetry(10 s por defecto).
-
Paginación transparente. Los métodos de listado consumen todas las páginas/cursores antes de devolver la colección completa al llamador.
flowchart TD
A[Microservicio consumidor] -->|new Client(credentials)| B[Cliente conector]
B -->|1ª llamada sin token| C{Auth API}
C -->|access_token| B
B -->|Bearer token| D[API externa]
D -->|200 OK| B
D -->|401/403| E[Refresco token]
E --> B
D -->|429| F[Sleep Retry-After]
F --> B
B -->|List completa| A
4. Dependencias principales
| Dependencia | Versión | Propósito |
|---|---|---|
com.konghq:unirest-java | 3.14.5 | Cliente HTTP fluent para todas las llamadas REST |
com.google.code.gson:gson | 2.14.0 | Serialización/deserialización JSON |
org.projectlombok:lombok | 1.18.46 | @Getter, @Setter, @Accessors(chain=true) en modelos y clientes |
com.google.apis:google-api-services-analyticsreporting | v4-rev174-1.25.0 | SDK oficial Google Analytics Reporting API v4 |
com.google.http-client:google-http-client-gson | 1.44.1 | Transporte HTTP para el SDK de Google |
com.jcraft:jsch | 0.1.55 | SFTP para descarga del catálogo de Salesforce Commerce |
jakarta.xml.bind:jakarta.xml.bind-api | 4.0.2 | JAXB para deserialización del feed XML de Salesforce |
org.glassfish.jaxb:jaxb-runtime | 4.0.5 | Implementación de JAXB en tiempo de ejecución |
io.swagger.core.v3:swagger-annotations | 2.2.49 | Anotaciones de documentación de modelos |
5. API / Endpoints
No aplica a este proyecto. Es una librería sin servidor HTTP propio.
La tabla siguiente describe los métodos públicos de cada cliente (la interfaz que expone la librería a sus consumidores):
Amazon — AmazonRequestSigner
| Método | Descripción |
|---|---|
static signRequest(HttpRequest, region, serviceName, secret, keyId, timestamp) | Firma en el lugar un HttpRequest de Unirest añadiendo la cabecera Authorization con AWS SigV4. No realiza la llamada HTTP. |
Uso:
var timestamp = ZonedDateTime.now(ZoneOffset.UTC)
.format(DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'"));
var request = Unirest.get("https://sellingpartnerapi-eu.amazon.com/orders/v0/orders?...")
.header("X-Amz-Date", timestamp)
.header("x-amz-access-token", lwaToken);
AmazonRequestSigner.signRequest(request, "eu-west-1", "execute-api", awsSecret, awsKeyId, timestamp);
var response = request.asString();
Google Analytics — AnalyticsClient
| Método | Descripción |
|---|---|
getReports(from, to, viewId, filtersExpression, metrics, dimensions) | Devuelve todos los informes paginados (10 000 filas/página por defecto) para el rango de fechas y vista indicados. |
Construcción:
var client = new AnalyticsClient(
new File("/path/to/service-account.p12"),
"service-account@project.iam.gserviceaccount.com",
"MyApp"
);
List<Report> reports = client.getReports(
LocalDate.of(2025, 1, 1), LocalDate.of(2025, 1, 31),
"123456789",
"ga:medium==organic",
List.of("ga:sessions", "ga:transactions"),
List.of("ga:date", "ga:country")
);
MercadoLibre — MeliOrdersClient
| Método | Descripción |
|---|---|
getOrders(Filters) | Descarga todas las órdenes del vendedor usando scroll-based pagination. Acepta null para traer todas sin filtro. |
Filtros disponibles (MeliOrdersClient.Filters):
| Campo | Parámetro API | Tipo |
|---|---|---|
item | item | String |
tags | tags | String (coma-separado) |
tagsNot | tags.not | String |
q | q | String |
orderStatus | order.status | String |
lastUpdatedFrom/To | order.date_last_updated.from/to | OffsetDateTime |
createdFrom/To | order.date_created.from/to | OffsetDateTime |
closedFrom/To | order.date_closed.from/to | OffsetDateTime |
mediationsStage | mediations.stage | String |
mediationsStatus | mediations.status | String |
feedbackStatus | feedback.status | String |
Construcción:
var client = new MeliOrdersClient(appId, clientSecret, sellerId);
var filters = new MeliOrdersClient.Filters()
.setOrderStatus("paid")
.setLastUpdatedFrom(OffsetDateTime.now().minusDays(7));
List<Order> orders = client.getOrders(filters);
MercadoLibre — MeliShipmentsClient
| Método | Descripción |
|---|---|
getShipment(String id) | Devuelve el envío con id indicado, incluyendo su desglose de costes (dos llamadas: /shipments/{id} + /shipments/{id}/costs). |
MercadoLibre — MeliVisitsClient
| Método | Descripción |
|---|---|
getTotalVisits(sellerId, dateFrom, dateTo) | Devuelve el total de visitas a los items de un vendedor en un rango de fechas. |
MercadoLibre — MeliCampaignsClient
| Método | Descripción |
|---|---|
getCampaigns(vendorId, from, to) | Devuelve las campañas de un vendedor con sus métricas para el rango de fechas indicado (dos llamadas por campaña: listado + métricas individuales). |
Salesforce Commerce — CustomCatalogFeedClient
| Método | Descripción |
|---|---|
getProducts() | Conecta por SFTP, localiza el fichero customcatalogfeed_YYYYMMDD.xml más reciente y lo deserializa con JAXB. |
static getProducts(String xml) | Parsea un Products desde un XML en String. |
static getProduct(String xml) | Parsea un Product desde un XML en String. |
Trustpilot — TrustpilotProductReviewsClient
| Método | Descripción |
|---|---|
getProductReviews() | Descarga todas las reseñas de producto (100 por página) para la business unit configurada. |
Trustpilot — TrustpilotServiceReviewsClient
| Método | Descripción |
|---|---|
getServiceReviews() | Descarga todas las reseñas de servicio (100 por página) para la business unit configurada. |
Trustpilot — TrustpilotProductsClient
| Método | Descripción |
|---|---|
getProducts() | Obtiene el catálogo de productos de la business unit. |
upsertProducts(Products) | Crea o actualiza productos en lote único. |
upsertProductsInBatches(Products, batchSize) | Envía los productos en lotes de batchSize, lanzando RuntimeException si algún lote falla. |
Trustpilot — TrustpilotInvitationsClient
| Método | Descripción |
|---|---|
createInvitation(Invitation) | Envía una invitación de reseña por email a un cliente. |
Zalando — ZalandoOrdersClient
| Método | Descripción |
|---|---|
getOrders(Filters) | Descarga todas las órdenes paginadas (500 por página) y las enriquece con ítems, líneas y transiciones mediante llamadas recursivas. |
Filtros disponibles (ZalandoOrdersClient.Filters):
| Campo | Parámetro API | Tipo |
|---|---|---|
createdAfter | created_after | OffsetDateTime |
createdBefore | created_before | OffsetDateTime |
lastUpdatedAfter | last_updated_after | OffsetDateTime |
lastUpdatedBefore | last_updated_before | OffsetDateTime |
orderStatus | order_status | String |
orderNumber | order_number | String |
salesChannelId | sales_channel_id | String |
locale | locale | String |
exported | exported | Boolean |
orderType | order_type | String (PartnerFulfilled/ZalandoFulfilled) |
Zalando — ZalandoProductsClient
| Método | Descripción |
|---|---|
getProductJsons() | Descarga todos los productos del merchant vía GraphQL con cursor-based pagination. Devuelve List<String> con cada producto en JSON crudo. |
6. Integraciones externas
| Sistema | Protocolo | Dirección | Notas |
|---|---|---|---|
| Amazon Selling Partner API | HTTPS + AWS SigV4 | Saliente | Región eu-west-1, endpoint sellingpartnerapi-eu.amazon.com |
| MercadoLibre API | HTTPS + OAuth2 | Saliente | api.mercadolibre.com; órdenes, envíos, visitas, campañas |
| Trustpilot API | HTTPS + OAuth2 | Saliente/Entrante | api.trustpilot.com/v1; invitaciones (saliente), lectura de reseñas (entrante) |
| Zalando Merchant API | HTTPS + OAuth2 | Saliente | api.merchants.zalando.com; REST + GraphQL |
| Salesforce Commerce Cloud | SFTP | Entrante | Descarga del feed de catálogo customcatalogfeed_YYYYMMDD.xml |
| Google Analytics Reporting API v4 | HTTPS + Service Account | Saliente | Autenticación por fichero P12 |
7. Configuración
Todos los clientes se configuran por inyección en el constructor (credenciales) y por setters (parámetros operativos). No hay application.properties ni variables de entorno definidas en la librería; la configuración es responsabilidad del microservicio consumidor.
Parámetros comunes a todos los clientes HTTP
| Setter / Getter | Descripción | Valor por defecto |
|---|---|---|
apiUrl | URL base de la API | (específico de cada cliente) |
authUrl | Ruta del endpoint de autenticación | (específico de cada cliente) |
requestMaxRetries | Número máximo de reintentos en error | 3 |
sleepAfterRequestRetry | Milisegundos de espera entre reintentos | 10000 |
minMillisBetweenTokenUpdates | Tiempo mínimo entre refrescos de token | 5000 |
defaultSleepMillisAfterTooManyRequests | Espera por defecto ante HTTP 429 (sin Retry-After) | 20000 |
Parámetros por cliente
AnalyticsClient
| Setter | Descripción | Valor por defecto |
|---|---|---|
pageSize | Resultados por página | 10000 |
maxRetries | Reintentos | 3 |
sleepMillisAfterRetry | Espera entre reintentos (ms) | 10000 |
Constructor:
AnalyticsClient(File keyFile, String analyticsAccountId, String applicationName)
keyFile: fichero P12 de la service account de Google → no incluir en el repositorioanalyticsAccountId: email de la service account (xxx@yyy.iam.gserviceaccount.com) →${GOOGLE_SA_ACCOUNT_ID}applicationName: nombre descriptivo de la aplicación
MeliOrdersClient / MeliShipmentsClient / MeliVisitsClient / MeliCampaignsClient
| Parámetro constructor | Descripción |
|---|---|
appId / clientId | Client ID de la app MercadoLibre → ${MELI_CLIENT_ID} |
clientSecret | Client Secret → ${MELI_CLIENT_SECRET} |
sellerId | ID del vendedor en MELI (solo MeliOrdersClient) |
ZalandoOrdersClient / ZalandoProductsClient
| Parámetro constructor | Descripción |
|---|---|
username | Usuario OAuth2 de Zalando → ${ZALANDO_USERNAME} |
password | Contraseña → ${ZALANDO_PASSWORD} |
merchantId | ID del merchant en Zalando → ${ZALANDO_MERCHANT_ID} |
pageSize (setter) | Órdenes por página |
TrustpilotClient (y subclases)
| Parámetro constructor | Descripción |
|---|---|
username | Usuario Trustpilot → ${TRUSTPILOT_USERNAME} |
password | Contraseña → ${TRUSTPILOT_PASSWORD} |
apiKey | API key de la app Trustpilot → ${TRUSTPILOT_API_KEY} |
apiSecret | API secret → ${TRUSTPILOT_API_SECRET} |
businessUnitId | ID de la business unit → ${TRUSTPILOT_BUSINESS_UNIT_ID} |
CustomCatalogFeedClient
| Parámetro constructor | Descripción |
|---|---|
ftpHost | Host SFTP → ${SFCC_SFTP_HOST} |
ftpUser | Usuario SFTP → ${SFCC_SFTP_USER} |
ftpPassword | Contraseña SFTP → ${SFCC_SFTP_PASSWORD} |
ftpPath | Ruta remota del directorio de catálogos |
port (setter) | Puerto SFTP |
8. Persistencia
No aplica a este proyecto. Es una librería sin capa de persistencia propia.
9. Procesos programados y mensajería
No aplica a este proyecto. No hay @Scheduled, listeners de colas ni runners batch.
10. Ejecución en local
Esta librería no se ejecuta directamente. Se consume como dependencia Maven.
Publicar en Artifact Registry (requiere credenciales GCP)
# Build + publicación en el registro Maven de Google Cloud
mvn deploy -DskipTests
Compilar y empaquetar localmente (sin publicar)
# Compile
mvn compile
# Package (genera target/api-connectors-1.0.25.jar)
mvn -B -DskipTests clean install
Usar la librería como dependencia en otro proyecto
<dependency>
<groupId>com.hawkersco.connectors</groupId>
<artifactId>api-connectors</artifactId>
<version>1.0.25-SNAPSHOT</version>
</dependency>
Con el repositorio configurado en el pom.xml del consumidor apuntando a europe-west3-maven.pkg.dev/pi-saldum/pi-repo-maven.
11. Despliegue
No hay despliegue de servicio. El artefacto se publica en el Artifact Registry de Google Cloud (entorno PI).
El pipeline de Jenkins realiza un único stage que ejecuta mvn deploy -DskipTests usando JDK 25 y Maven 3, y publica el JAR en el registro Maven interno.
Job de Jenkins:
https://jenkins-pi.hawkersco.net/job/api-connectors/
12. Manejo de errores y logging
Estrategia de reintentos
Todos los clientes implementan reintentos inline en el método executeHttpRequest. Los errores se silencian con catch (Exception _) {} (unnamed pattern variable de Java 21+), lo que implica que ninguna excepción de transporte se propaga al llamador. Si tras requestMaxRetries intentos la respuesta sigue siendo null o no exitosa, se devuelve el último HttpResponse recibido (puede ser null).
El llamador es responsable de comprobar si la respuesta es null o no exitosa antes de deserializar el cuerpo.
Logging
No existe logging interno en la librería. No se utiliza SLF4J, Logback ni ningún framework de logging. Los errores de transporte son silenciados. El microservicio consumidor debe implementar su propio logging alrededor de las llamadas.
Excepciones propagadas
| Clase | Cuándo se lanza |
|---|---|
UnsupportedEncodingException | Al construir URLs con filtros (MeliOrdersClient, ZalandoOrdersClient) |
IllegalAccessException | Al introspeccionar filtros mediante reflexión |
IOException | Si AnalyticsClient agota todos los reintentos |
IllegalStateException | Si ZalandoOrdersClient recibe respuesta no exitosa al poblar órdenes; si CustomCatalogFeedClient no encuentra ficheros en el SFTP |
JSchException / SftpException | Si la conexión SFTP falla en CustomCatalogFeedClient |
JAXBException | Si el XML del catálogo de Salesforce no puede deserializarse |
RuntimeException | Si TrustpilotProductsClient.upsertProductsInBatches recibe error en algún lote |
Exception | AmazonRequestSigner.signRequest puede lanzar Exception genérica si falla el HMAC |
13. Notas y consideraciones
Deuda técnica y comportamientos no obvios
-
MeliUtils.getAuthToken()—returnen bloquefinally: El método tiene unreturn authTokendentro del bloquefinally, lo que provoca que cualquier excepción lanzada en eltryquede suprimida silenciosamente y siempre se retornenull. Este es un antipatrón de Java conocido. -
MeliVisitsClient— 429 tratado como error de autenticación: A diferencia del resto de clientes,MeliVisitsClienttrata HTTP 429 como señal de refresco de token (no como rate limit). Esto está documentado en un comentario en el código:// Adding status 429 since it is the only one that is returned when unauthenticated. -
MeliShipmentsClient— 400 tratado como error de autenticación: El cliente trata HTTP 400 (Bad Request) como si fuera un 401, lo que puede enmascarar peticiones mal formadas. -
MeliCampaignsClient— paginación no implementada: El listado de campañas usalimit=2000sin bucle de paginación. El propio código incluye el comentario:// The API allows high limits, so we do not need to worry about pagination. This however might change in the future, so a proper implementation should be done. -
CustomCatalogFeedClient—StrictHostKeyChecking=no: La conexión SFTP desactiva la verificación de clave del host, lo que supone un riesgo ante ataques MITM en entornos no controlados. La conexión se establece con password en texto plano a nivel de configuración del cliente. -
ZalandoProductsClient.getProductJsons()— devuelve JSON crudo: A diferencia del resto de clientes, este método devuelveList<String>en lugar de objetos tipados, dejando la deserialización al microservicio consumidor. -
AmazonRequestSigner.main()— método de prueba con credenciales de ejemplo: La clase incluye un métodomaincon valores de ejemplo hardcodeados (token LWA y claves AWS). Estos valores son de ejemplo/prueba y no deben usarse en producción. No hay mecanismo de configuración en la librería para Amazon; el consumidor debe gestionar la obtención del LWA token. -
AnalyticsClient— TODO en la clase: Existe un comentario// TODO: Incremental sleeps on retries and optional loggerque indica que los reintentos no implementan backoff incremental (siempre esperan el mismosleepMillisAfterRetry). -
Sin tests: No existe directorio
src/test. La librería no tiene cobertura de pruebas automatizadas. -
src/main/resources/application.propertiesy directorioauth/en.gitignore: Las credenciales y la configuración específica de entorno viven fuera del repositorio. Cada microservicio consumidor aporta su propia configuración al instanciar los clientes.