Dafiti Client
1. Descripción general
dafiti-client (description del pom.xml: Dafiti client) es una librería JAR compartida que encapsula la integración con la API del marketplace Dafiti. Envuelve los endpoints de pedidos, ítems de pedido, cambio de estado a "listo para enviar", exportación de documentos y descarga de ficheros usando los clientes HTTP declarativos nativos de Spring (@HttpExchange + HttpServiceProxyFactory, Spring Framework 7), sin depender de Spring Cloud OpenFeign.
El proyecto no es un microservicio desplegable de cara a negocio: aunque contiene una clase @SpringBootApplication (DafitiClientApplication), su función real es la de auto-configuración de Spring Boot (@AutoConfiguration, registrada vía META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports) que se activa automáticamente en cualquier microservicio consumidor que declare la dependencia y las propiedades dafiti.api.*.
Resuelve el problema de que cada microservicio de Hawkers que necesita sincronizar pedidos y su estado logístico con Dafiti tenga que reimplementar el cliente HTTP, la autenticación OAuth2 (client_credentials vía formulario) con caché de token, y los DTOs de request/response. Dentro del ecosistema de microservicios de Hawkers, actúa como capa de integración transversal con este canal de marketplace, de forma análoga a otros clientes similares (coppel-client, cubbo-client, etc.).
2. Información técnica
| Propiedad | Valor |
|---|---|
artifactId | dafiti-client |
groupId | com.hawkersco |
version | 1.0.25-SNAPSHOT |
| Java | 25 |
| Spring Boot | 4.0.6 (Spring Framework 7) |
| Tipo de artefacto | JAR (librería con auto-configuración Spring Boot; incluye clase @SpringBootApplication de soporte, sin despliegue independiente) |
| Módulos | Proyecto mono-módulo |
| Repositorio Maven | Google Artifact Registry — europe-west3-maven.pkg.dev/pi-saldum/pi-repo-maven |
3. Arquitectura y diseño
Paquetes principales
com.hawkersco.dafiticlient
├── client/ # Interfaces @HttpExchange — DafitiClient (API negocio), DafitiTokenClient (auth)
├── config/ # DafitiClientAutoConfiguration, CacheStore (Guava), CacheStoreBeans, DafitiConfig
├── model/ # DTOs Lombok con anotaciones dobles Jackson/Gson
└── DafitiClientApplication.java # Clase @SpringBootApplication de soporte
Componentes clave
client/DafitiClient— interfaz@HttpExchangecon las operaciones de negocio: pedidos pendientes, pedido por ID, cambio a estado "ready to ship", ítems de pedido, exportación de documentos y descarga de ficheros por UUID.client/DafitiTokenClient— interfaz@HttpExchangepara/oauth/client-credentials(form-urlencoded); de uso interno exclusivo deDafitiClientAutoConfiguration.config/DafitiClientAutoConfiguration— clase@AutoConfiguration, condicionada a que existan las propiedadesdafiti.api.{url,grantType,clientId,clientSecret}(@ConditionalOnProperty). Registra los beansDafitiTokenClientyDafitiClient, este último con unClientHttpRequestInterceptorque resuelve el token Bearer en cada petición usando una caché con TTL de 1 hora.config/CacheStore<T>— envoltorio genérico sobrecom.google.common.cache.Cache(Guava) con expiración configurable (expireAfterWrite).config/CacheStoreBeans— clase@Configurationque expone un beanCacheStore<String>adicional con TTL de 1 hora, disponible para inyección fuera del contexto deDafitiClientAutoConfiguration(uso externo no localizado en este proyecto).config/DafitiConfig— clase de constantes, actualmente soloTXT_TOKEN = "token"(clave de caché).model/— DTOs Lombok (@Getter/@Setter/@NoArgsConstructor/@AllArgsConstructor/@ToString) con anotaciones dobles@SerializedName(Gson) y@JsonProperty(Jackson).
Patrón de autenticación con caché
El interceptor de DafitiClient consulta primero la caché Guava (tokenCache, TTL 1 hora); si no hay token vigente, lo solicita a /oauth/client-credentials con formulario grant_type/client_id/client_secret vía DafitiTokenClient:
.requestInterceptor((request, body, execution) -> {
String token = tokenCache.get(DafitiConfig.TXT_TOKEN);
if (token == null) {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("grant_type", grantType);
formData.add("client_id", clientId);
formData.add("client_secret", clientSecret);
var response = dafitiTokenClient.getToken(formData);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
token = response.getBody().getAccessToken();
tokenCache.add(DafitiConfig.TXT_TOKEN, token);
}
}
request.getHeaders().set("Authorization", "Bearer " + (token != null ? token : ""));
return execution.execute(request, body);
})
A diferencia de coppel-client (token estático sin caché) y de forma similar a cubbo-client, dafiti-client sí implementa reutilización de token entre peticiones (aquí con TTL de 1 hora frente a los 45 minutos de Cubbo).
Flujo principal
sequenceDiagram
participant MS as Microservicio consumidor
participant DC as DafitiClient (proxy HttpExchange)
participant Cache as CacheStore (Guava, TTL 1h)
participant DTC as DafitiTokenClient
participant Dafiti as API Dafiti
MS->>DC: getOrdersPending(limit, offset) / getOrderItems(...) / ...
DC->>Cache: get("token")
alt Token en caché
Cache-->>DC: token vigente
else Sin token o expirado
DC->>DTC: getToken(grant_type, client_id, client_secret)
DTC->>Dafiti: POST /oauth/client-credentials (form-urlencoded)
Dafiti-->>DTC: access_token + expires_in
DC->>Cache: add("token", token)
end
DC->>Dafiti: petición real (GET/POST /v2/...) + Authorization: Bearer <token>
Dafiti-->>DC: respuesta
DC-->>MS: ResponseEntity<...>
4. Dependencias principales
| Dependencia | Propósito |
|---|---|
org.springframework.boot:spring-boot-starter | Núcleo de Spring Boot (auto-configuración, contexto). |
org.springframework:spring-web | RestClient, HttpServiceProxyFactory y soporte @HttpExchange. |
com.google.guava:guava (33.4.0-jre) | Cache/CacheBuilder usado por CacheStore para el TTL del token. |
com.google.code.gson:gson | Anotaciones @SerializedName en los DTOs (uso dual junto a Jackson). |
com.fasterxml.jackson.core:jackson-databind | Deserialización de las respuestas de la API de Dafiti y anotaciones @JsonProperty. |
org.projectlombok:lombok (1.18.42) | Generación de getters/setters/constructores/toString en los DTOs. |
org.springframework.boot:spring-boot-starter-test (test) | Soporte de test de Spring Boot. |
Extensión de build relevante: com.google.cloud.artifactregistry:artifactregistry-maven-wagon (2.2.1) — necesaria para publicar/resolver contra el Google Artifact Registry corporativo.
5. API / Endpoints
No expone API REST propia (no hay @RestController). En su lugar, define clientes HTTP declarativos que consumen la API de Dafiti:
DafitiClient (operaciones de negocio)
| Método HTTP | Ruta | Descripción | Request | Response |
|---|---|---|---|---|
| GET | /v2/orders?section=status_pending | Lista paginada de pedidos pendientes (getOrdersPending). | Query limit, offset | OrdersDafitiResponse |
| GET | /v2/orders/{orderId} | Obtiene un pedido por ID (getOrderById). | Path orderId | OrdersDafitiResponse.OrdersDafitiResponseItem |
| POST | /v2/orders/statuses/set-to-ready-to-ship | Marca pedidos como listos para enviar (setStatusReadyToShip). | String (JSON crudo) | String (respuesta cruda de Dafiti) |
| GET | /v2/order-items | Lista paginada de ítems de pedido por lista de IDs. | Query orderIds[] (array), limit, offset | OrderItemsDafitiResponse |
| POST | /v2/orders/export-document | Exporta documentos (factura/etiqueta) para pedidos/ítems. | String (JSON crudo) | DafitiExportDocumentResponse |
| GET | /filemanager/v1/files/download/{uuid} | Descarga un fichero exportado por su UUID. | Path uuid | byte[] |
DafitiTokenClient (uso interno)
| Método HTTP | Ruta | Descripción | Request | Response |
|---|---|---|---|---|
| POST | /oauth/client-credentials | Obtiene el token de acceso OAuth2 (form-urlencoded). | grant_type, client_id, client_secret (MultiValueMap) | DafitiTokenResponse (token_type, expires_in, access_token) |
Ejemplo de payload esperado para exportDocument (según los campos de DafitiExportDocumentRequest, aunque el método recibe un String crudo — ver sección 13):
{
"orderIds": [1001, 1002],
"orderItemIds": [5001, 5002],
"documentType": "invoice",
"format": "pdf"
}
Ejemplo de payload esperado para setStatusReadyToShip (según los campos de DafitiReadyToShipRequest, aunque el método recibe un String crudo — ver sección 13):
{
"orderItems": [{ "id": 5001 }, { "id": 5002 }],
"deliveryType": "dropship",
"shippingProvider": "own"
}
OrdersDafitiResponse.OrdersDafitiResponseItem y OrderItemsDafitiResponse.Item son los DTOs de mayor complejidad, modelando la estructura completa del pedido/ítem devuelta por Dafiti (cliente, dirección, envío, estado, precios, etc.).
6. Integraciones externas
| Sistema | Protocolo / Mecanismo | Dirección del flujo |
|---|---|---|
| Dafiti (marketplace) | HTTP/REST vía RestClient + @HttpExchange (JSON y application/x-www-form-urlencoded para el token), autenticación OAuth2 client_credentials con token cacheado 1 hora | Saliente: el microservicio consumidor llama a Dafiti para leer pedidos/ítems, marcar pedidos como listos para enviar, exportar documentos y descargar ficheros. |
| Microservicios Hawkers consumidores | Dependencia Maven (com.hawkersco:dafiti-client) + auto-configuración Spring Boot | Entrante como librería: se activa automáticamente al declarar la dependencia y configurar dafiti.api.*. |
7. Configuración
El proyecto no incluye application.properties/application.yml propio de servicio (es una librería auto-configurable). Los microservicios consumidores deben declarar las siguientes propiedades para que DafitiClientAutoConfiguration se active (@ConditionalOnProperty exige las cuatro):
| Clave | Descripción | Ejemplo de valor |
|---|---|---|
dafiti.api.url | URL base de la API de Dafiti. | https://${DAFITI_API_HOST} |
dafiti.api.grantType | Grant type OAuth2 usado en /oauth/client-credentials. | client_credentials |
dafiti.api.clientId | Client ID OAuth2. | ${DAFITI_CLIENT_ID} |
dafiti.api.clientSecret | Client Secret OAuth2. | ${DAFITI_CLIENT_SECRET} |
Nunca deben commitearse valores reales de clientId/clientSecret; se recomienda inyectarlos vía variables de entorno o gestor de secretos del entorno de despliegue.
8. Persistencia
No aplica. La librería no accede a ninguna base de datos; todo el estado relevante de pedidos reside en Dafiti. La única forma de "estado" en memoria es la caché Guava del token de acceso (CacheStore), no persistente entre reinicios.
9. Procesos programados y mensajería
No aplica. No se han encontrado @Scheduled, @KafkaListener ni @RabbitListener en el proyecto.
10. Ejecución en local
Como librería auto-configurable, no se "ejecuta" de forma independiente en producción, aunque incluye una clase @SpringBootApplication (DafitiClientApplication) para pruebas locales del contexto de auto-configuración.
Requisitos previos: JDK 25, Maven (o el wrapper ./mvnw incluido).
Build e instalación en repositorio Maven local (omitiendo tests, como en CI):
./mvn clean install -DskipTests
Build con tests:
./mvn clean install
Ejecutar una clase de test concreta:
./mvn test -Dtest=MyTestClass
Ejecutar un método de test concreto:
./mvn test -Dtest=MyTestClass#myMethod
No aplica verificación vía Actuator/health al no ser un servicio desplegable de cara a producción. Los microservicios consumidores deben declarar com.hawkersco:dafiti-client:<version> en su pom.xml y configurar las propiedades dafiti.api.* descritas en la sección 7.
11. Despliegue
El despliegue consiste en la publicación del artefacto Maven al Google Artifact Registry corporativo (no hay despliegue de contenedor/servicio propio).
Pipeline Jenkins (Jenkinsfile, agente any, JDK25 + Maven3):
- Checkout del repositorio.
- Publish to Artifact Registry:
mvn deploy -DskipTests, publicando enartifactregistry://europe-west3-maven.pkg.dev/pi-saldum/pi-repo-maven(definido endistributionManagementdelpom.xml).
CLAUDE.md indica que el pipeline usa mvn -B -DskipTests clean install para producir target/*.jar, mientras que el Jenkinsfile presente ejecuta mvn deploy -DskipTests directamente — ambos comandos son coherentes con el objetivo de publicar el artefacto, sin discrepancia relevante.
Job de Jenkins:
https://jenkins-pi.hawkersco.net/job/dafiti-client/
12. Manejo de errores y logging
No se ha encontrado estrategia de excepciones propia (no hay @ControllerAdvice, excepciones custom ni códigos de error definidos en el proyecto). Las respuestas de error de la API de Dafiti se propagan como RestClientResponseException estándar de Spring en las llamadas a DafitiClient; el consumidor es responsable de capturarlas.
En el interceptor de autenticación, si la respuesta del token no es 2xx o el cuerpo es null, token permanece null y la petición continúa con Authorization: Bearer vacío en lugar de lanzar una excepción — un fallo silencioso equivalente al observado en auth0-client y cubbo-client.
No se ha encontrado configuración de logging propia (logback.xml/log4j2.xml); el logging queda delegado a la configuración del microservicio que integra la librería.
13. Notas y consideraciones
- DTOs de request definidos pero no usados por el cliente HTTP:
DafitiReadyToShipRequestyDafitiExportDocumentRequestmodelan de forma tipada los payloads desetStatusReadyToShipyexportDocumentrespectivamente, pero ambos métodos deDafitiClientreciben un@RequestBody Stringen lugar de estos DTOs. Esto obliga al consumidor a serializar manualmente el JSON (o a ignorar estos DTOs y construir el string por su cuenta), perdiendo la seguridad de tipos que sí aportan el resto de DTOs del proyecto. Es candidato a refactor: cambiar la firma de ambos métodos para aceptar los DTOs tipados directamente. CacheStoreBeansexpone una segunda caché de token independiente: el beanCacheStore<String> token()deCacheStoreBeansno está conectado con eltokenCacheinterno deDafitiClientAutoConfiguration(este último se instancia como campo privado, no como bean). Si algún consumidor inyecta el beanCacheStore<String>esperando compartir el token cacheado por el interceptor, encontrará una caché distinta y vacía — Pendiente de verificar el propósito real de este bean adicional.- Fallo silencioso en la obtención de token: igual que en
auth0-clientycubbo-client, un fallo al obtener el token no se propaga como excepción, lo que retrasa la detección del error hasta que la API de Dafiti rechaza la petición con un headerAuthorizationvacío o inválido. - Numerosos campos
Objecten los DTOs de respuesta: tantoOrdersDafitiResponsecomoOrderItemsDafitiResponsetienen múltiples campos tipados comoObject(nationalRegistrationNumber,source,extraAttributes,invoiceNumber,shippingServiceCost,manifestStatus, entre otros), reflejando previsiblemente valores que la API de Dafiti puede devolver comonullo en formatos variables — obliga al consumidor a inspeccionar/castear manualmente su contenido real. - Estructuras
Item/Shipment/Product/Purchaseduplicadas:OrdersDafitiResponse.OrdersDafitiResponseItem.ItemyOrderItemsDafitiResponse.Itemmodelan estructuras casi idénticas (mismoShipment,Product,Purchase,FailureReason) de forma independiente en lugar de compartir un tipo común — coherente con que ambas provienen de endpoints distintos de Dafiti (/v2/ordersvs/v2/order-items), pero implica mantenimiento duplicado si la API cambia su esquema. - Sin tests: no se ha localizado
src/test/javaen el proyecto pese a queCLAUDE.mddocumenta comandos de test (./mvn clean install,./mvn test -Dtest=...) — Pendiente de verificar si los tests existen en una rama distinta o si la documentación está desactualizada.