SFCC Services Client
1. Descripción general
sfcc-services-client es una librería cliente reutilizable (JAR) que encapsula la comunicación con la API REST de Salesforce (/services/data/vXX.0/...) usada como backend de servicios (distinto de OCAPI/Shop, cubiertas por sfcc-client, y de Marketing Cloud, cubierta por sfcc-marketing-client). 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 gestionar productos, cuentas de cliente, pedidos, entradas de price book y envíos/líneas de envío en Salesforce, así como ejecutar consultas SOQL en lote (batch/bulk API).
A diferencia del resto de clientes del ecosistema, este proyecto incluye además una capa de lógica de negocio (SfccServicesClientUtils) que construye peticiones compuestas (composite API) para el alta transaccional de cuentas, gestiona flags de consentimiento GDPR específicos por marca, y orquesta la creación de pedidos con sus líneas, direcciones y entradas de price book — funcionalidad que va más allá de un simple envoltorio HTTP.
2. Información técnica
| Propiedad | Valor |
|---|---|
artifactId | sfcc-services-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.sfccservicesclient
├── SfccServicesClientApplication.java # Clase @SpringBootApplication (residual, sin lógica)
├── client/
│ ├── SfccServicesClient.java # Producto, Cuenta, Pedido, PricebookEntry, Envío/Línea de envío (API REST v57.0)
│ ├── SfccServicesTokenClient.java # Obtención de token OAuth2 (form-urlencoded)
│ └── SfccServicesBatchClient.java # Consultas SOQL (queryAll/queryNextRecord) e ingestión bulk (jobs/ingest)
├── config/
│ ├── SfccServicesAutoConfiguration.java # @AutoConfiguration principal (registra los 3 clientes)
│ ├── SfccServicesClientConfig.java # Credenciales OAuth (@Value) + caché de token (100 min) + resolveToken()
│ ├── CacheStore.java # Cache genérica en memoria (Guava)
│ └── CacheStoreBeans.java # Bean CacheStore<String> adicional (TTL 1 min), no usado por resolveToken() (ver sección 13)
├── pojo/ # ~17 POJOs de request/response por dominio (Account, Order, Product, Shipment, PricebookEntry, *Batch*)
└── utils/
└── SfccServicesClientUtils.java # Lógica de negocio: alta de cuenta compuesta, GDPR, creación de pedido, mapeo de tienda por marca/región
Flujo principal de autenticación y llamada
sequenceDiagram
participant Consumidor as Microservicio consumidor
participant Client as SfccServicesClient
participant Config as SfccServicesClientConfig
participant TokenClient as SfccServicesTokenClient
participant SFCC as API REST Salesforce
Consumidor->>Client: getProduct / sendAccount / postOrder / ...
Client->>Config: resolveToken(tokenClient)
Config->>Config: cache.get("token")
alt Token en caché (< 100 min)
Config-->>Client: token válido
else Token ausente o expirado
Config->>TokenClient: getToken(grant_type=password, username, password, client_id, client_secret)
TokenClient->>SFCC: POST /services/oauth2/token
SFCC-->>TokenClient: { "access_token": "..." }
TokenClient-->>Config: token
Config->>Config: cache.add("token", token)
end
Client->>SFCC: request + Authorization: Bearer <token>
SFCC-->>Client: ResponseEntity<T>
Client-->>Consumidor: ResponseEntity<T>
SfccServicesAutoConfiguration se activa condicionalmente con @ConditionalOnProperty(prefix = "sfccservices.credentials", name = {"token.url", "url"}) e importa explícitamente (@Import) SfccServicesClientConfig y CacheStoreBeans. Registra tres beans (SfccServicesTokenClient, SfccServicesClient, SfccServicesBatchClient), los dos últimos con el mismo requestInterceptor que resuelve el token vía SfccServicesClientConfig.resolveToken(...) (grant type password, es decir, autenticación con usuario/contraseña + client_id/secret, no client credentials).
El registro de la autoconfiguración se realiza mediante el fichero estándar de Spring Boot:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
SfccServicesClientUtils no es un bean Spring: es una clase de utilidad plana que el consumidor instancia directamente, pasándole el SfccServicesClient ya configurado. Construye internamente peticiones a la composite API de Salesforce para crear cuenta + contacto + propietario en una sola llamada transaccional, resuelve el mapeo sourceId → tienda para las marcas Hawkers, Bratleboro, Northweek y Misshamptons (regiones EU, AU, CO, MX, GB, USA), y gestiona la creación dinámica de entradas de price book faltantes al procesar un pedido.
4. Dependencias principales
| Dependencia | Versión | Propósito |
|---|---|---|
spring-boot-starter | (gestionada SB4) | Base de Spring Boot (contexto, autoconfiguración) |
spring-boot-starter-web | (gestionada SB4) | RestClient + @HttpExchange / HttpServiceProxyFactory |
com.google.guava:guava | 33.4.0-jre | Implementación de caché en CacheStore (CacheBuilder) |
org.json:json | 20240303 | Parseo/construcción de JSON en SfccServicesClientUtils y extracción de access_token |
com.fasterxml.jackson.core:jackson-databind / jackson-annotations | (gestionada SB4) | Serialización/deserialización Jackson en los POJOs y en SfccServicesClientUtils |
com.google.code.gson:gson | (gestionada SB4) | Serialización Gson usada puntualmente en utilidades (coexiste con Jackson) |
org.projectlombok:lombok | 1.18.42 (dependencia; annotationProcessorPath del compilador usa 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. sfcc-services-client es una librería cliente JAR que no expone endpoints REST propios. Las operaciones que encapsula sobre la API REST de Salesforce se detallan en la sección 6.
6. Integraciones externas
API REST de Salesforce (SfccServicesClient, v57.0)
| Dominio | Método | HTTP | Ruta remota (resumida) |
|---|---|---|---|
| Product | getProduct / getProductNew / getProductById | GET | /sobjects/Product2/ExternalId__c/{externalId} | /sobjects/Product2/{productId} |
| Product | sendProduct | PATCH | /sobjects/Product2/ExternalId__c/{id} |
| Account | sendAccount | POST | /composite/ (alta transaccional compuesta) |
| Account | getAccount / getAccountNew | GET | /sobjects/Account/ExternalId__c/{externalId} | /sobjects/Account/{accountId} |
| Account | patchAccount | POST | /sobjects/Account/{id}?_HttpMethod=PATCH (patch vía POST, ver sección 13) |
| Order | postOrder | POST | /commerce/sale/order |
| Order | getOrder / getOrderByName | GET | /sobjects/Order/ExternalId__c/{externalId} | /sobjects/Order/SFCC_Order_Number__c/{name} |
| PricebookEntry | getPricebookEntry / postPricebookEntry / getPricebook | GET/POST | /sobjects/PricebookEntry/... | /sobjects/Pricebook2/Name/{name} |
| Shipment | getShipmentIdByOrder | GET | /query/?q=select Id from Shipment__c where Order__c='{orderId}' (SOQL embebido) |
| Shipment | getShipmentById / createShipment / updateShipment | GET/POST/PATCH | /sobjects/Shipment__c/{shipmentId} |
| Shipment Line | getShipmentLinesIdByShipment | GET | /query/?q=select Id from Shipment_Line__c where Shipment__c='{shipmentId}' |
| Shipment Line | getShipmentLineById / postShipmentLine / updateShipmentLine | GET/POST/PATCH | /sobjects/Shipment_Line__c/{shipmentLineId} |
Batch/Bulk API (SfccServicesBatchClient, v57.0)
| Método | HTTP | Ruta remota | Descripción |
|---|---|---|---|
getOrdersAndShipments | GET | /query/?q= (SOQL generado a partir de una lista de números de pedido) | Consulta pedidos y sus envíos anidados |
getShipmentsLinesByShipments | GET | /query/?q= (SOQL generado a partir de una lista de IDs de envío) | Consulta líneas de envío por lista de envíos |
getProductsBySkus | GET | /query/?q= (SOQL generado a partir de una lista de SKUs) | Consulta productos por lista de SKUs |
createJobBatch | POST | /jobs/ingest | Crea un job de ingestión bulk (Bulk API 2.0) |
inserDataBatch | PUT | /jobs/ingest/{idJob}/batches | Sube el CSV de datos para el job de ingestión |
executeDataBatch | PATCH | /jobs/ingest/{idJob} | Marca el job como listo para procesar (UploadComplete) |
getInfoJobBatch | GET | /jobs/ingest/{idJob} | Consulta el estado del job de ingestión |
queryAll | GET | /queryAll?q={query} | Ejecuta una consulta SOQL arbitraria (incluye registros eliminados) |
queryNextRecord | GET | /queryAll/{locator} | Continúa la paginación de una consulta SOQL |
Ejemplo de payload sendAccount (composite API, construido por SfccServicesClientUtils.createAccount):
{
"compositeRequest": [
{ "method": "POST", "url": "/services/data/v48.0/sobjects/Account", "referenceId": "NewAccount", "body": { "Name": "Cliente Final" } },
{ "method": "POST", "url": "/services/data/v48.0/sobjects/Contact", "referenceId": "NewContact", "body": { "LastName": "Final", "AccountId": "@{NewAccount.id}" } },
{ "method": "POST", "url": "/services/data/v48.0/sobjects/User", "referenceId": "NewOwner", "body": { "...": "..." } }
]
}
Protocolo: HTTPS REST (JSON; text/csv para la subida de datos bulk). Autenticación: OAuth2 grant type password (usuario/contraseña + client_id/secret), Bearer token cacheado 100 minutos.
7. Configuración
No se incluye ningún application.properties/application.yml en la librería. Las propiedades deben ser inyectadas por la aplicación consumidora.
Propiedades requeridas (prefijo sfccservices.credentials)
| Propiedad | Descripción | Ejemplo de valor |
|---|---|---|
sfccservices.credentials.url | URL base de la API REST de Salesforce (activa la autoconfiguración) | ${SFCC_SERVICES_URL} |
sfccservices.credentials.token.url | URL base del endpoint de token (activa la autoconfiguración) | ${SFCC_SERVICES_TOKEN_URL} |
sfccservices.credentials.token.granttype | Grant type OAuth2 (típicamente password) | password |
sfccservices.credentials.token.username | Usuario de la integración en Salesforce | ${SFCC_SERVICES_USERNAME} |
sfccservices.credentials.token.password | Contraseña de la integración en Salesforce | ${SFCC_SERVICES_PASSWORD} |
sfccservices.credentials.token.clientid | Client ID de la conexión conectada (Connected App) | ${SFCC_SERVICES_CLIENT_ID} |
sfccservices.credentials.token.clientsecret | Client secret de la conexión conectada | ${SFCC_SERVICES_CLIENT_SECRET} |
Importante: Solo
sfccservices.credentials.urlysfccservices.credentials.token.urlcondicionan la activación de la autoconfiguración (@ConditionalOnProperty); las cinco propiedades de credenciales de token son inyectadas por@Valuesin condición explícita enSfccServicesClientConfig— si faltan, el arranque del contexto fallará al construir ese bean.
Variables de entorno recomendadas
| Variable | Propiedad mapeada |
|---|---|
SFCC_SERVICES_URL | sfccservices.credentials.url |
SFCC_SERVICES_TOKEN_URL | sfccservices.credentials.token.url |
SFCC_SERVICES_USERNAME | sfccservices.credentials.token.username |
SFCC_SERVICES_PASSWORD | sfccservices.credentials.token.password |
SFCC_SERVICES_CLIENT_ID | sfccservices.credentials.token.clientid |
SFCC_SERVICES_CLIENT_SECRET | sfccservices.credentials.token.clientsecret |
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 100 minutos, dentro de SfccServicesClientConfig).
9. Procesos programados y mensajería
No aplica a este proyecto. No existen jobs @Scheduled, listeners de colas/topics ni runners batch. Las operaciones de "batch" (SfccServicesBatchClient) son llamadas HTTP síncronas bajo demanda del consumidor hacia la Bulk API de Salesforce, no un mecanismo de mensajería propio.
10. Ejecución en local
sfcc-services-client es fundamentalmente una librería JAR (según CLAUDE.md, "no web layer, no REST controllers"), aunque, a diferencia de otros 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 con tests
mvn clean install
# Compilar sin tests (como en CI)
mvn -B -DskipTests clean install
# Usando el wrapper
./mvnw clean install
# Ejecutar un test concreto
mvn test -Dtest=ClassName
# Build de imagen Docker (ver advertencia en sección 13)
docker build -t sfcc-services-client .
Uso como dependencia en un microservicio consumidor
<dependency>
<groupId>com.hawkersco</groupId>
<artifactId>sfcc-services-client</artifactId>
<version>1.0.25-SNAPSHOT</version>
</dependency>
La autoconfiguración se activa automáticamente al declarar sfccservices.credentials.url y sfccservices.credentials.token.url (más las credenciales de token) en la aplicación consumidora. Para la lógica de negocio de alta de cuentas/pedidos, el consumidor debe instanciar SfccServicesClientUtils directamente.
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 con etapas Build → Clean vía scripts (jenkins/scripts/mvn.sh/clean.sh), que no se corresponde con el Jenkinsfile actual del repositorio (dos etapas: Checkout y Publish to Artifact Registry, sin scripts externos). 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) |
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/sfcc-services-client/
12. Manejo de errores y logging
Los métodos de los tres clientes @HttpExchange no declaran throws explícito; las excepciones HTTP de RestClient (RestClientException/RestClientResponseException) se propagan sin envolver. En SfccServicesClientUtils, los métodos de negocio (createAccount, updateGDPR, createOrder) capturan explícitamente JsonProcessingException, JSONException y RestClientException, registran el error con un Logger pasado como parámetro (nivel SEVERE) y devuelven Boolean.FALSE en lugar de propagar la excepción — el consumidor solo obtiene un booleano de éxito/fracaso, sin detalle del error salvo lo que se haya logueado. No hay configuración de logback específica en la librería (se usa java.util.logging en SfccServicesClientUtils, distinto del resto del ecosistema que no registra explícitamente ningún framework de logging).
13. Notas y consideraciones
-
Dockerfilepresente pero inconsistente y no usado en CI: El repositorio incluye unDockerfileque construye la imagen a partir deopenjdk:8-jre-alpine(Java 8), en clara contradicción con el resto del proyecto, que usa Java 25 (pom.xml,JenkinsfileconJDK25). ElJenkinsfileactual no referencia ni construye esta imagen. Es casi con certeza un artefacto obsoleto de una versión muy anterior del proyecto, previo a la migración a Java 25 — no debería usarse para construir una imagen funcional de este componente sin actualizarlo primero. -
Desalineación de versión de API entre
SfccServicesClientySfccServicesClientUtils: Las rutas hardcodeadas enSfccServicesClient(interfaz@HttpExchange) usan la API de Salesforce v57.0, mientras que las constantes de ruta usadas internamente porSfccServicesClientUtils(SFCC_ACCOUNT_URL,SFCC_CONTACT_URL,SFCC_USER_URL, usadas al construir la petición composite de alta de cuenta) apuntan a la API v48.0. Aunque ambas versiones probablemente coexisten en Salesforce sin incompatibilidad práctica, es una inconsistencia de mantenimiento: cualquier actualización de versión de API debe aplicarse en dos lugares distintos del código. -
CacheStoreBeans.token()no utilizado: El beanCacheStore<String>(TTL 1 minuto) expuesto porCacheStoreBeansno es el que usaSfccServicesClientConfig.resolveToken(...), que en su lugar mantiene su propio campo privadoCacheStore<String>con TTL de 100 minutos. La caracterización de "caché en dos niveles" enCLAUDE.mdes, por tanto, engañosa: en la práctica solo la caché de 100 minutos está activa en el flujo de autenticación; el bean de 1 minuto queda disponible en el contexto Spring sin ningún consumidor interno. -
patchAccountusa POST con_HttpMethod=PATCH: En lugar de@PatchExchange, este método usa@PostExchange("/sobjects/Account/{id}?_HttpMethod=PATCH")— patrón de "method override" vía query param, típico de integraciones con Salesforce que no soportan bien el verboPATCHa través de determinados proxies/firewalls corporativos. -
SOQL embebido directamente en la URL de
@GetExchange:getShipmentIdByOrderygetShipmentLinesIdByShipmentincrustan una consulta SOQL codificada como parte literal de la ruta (%20,%3D), en lugar de construirla dinámicamente con@RequestParam. Esto significa que el{orderId}/{shipmentId}se interpola directamente dentro de una cadena SOQL — el consumidor debe garantizar que estos valores no contengan caracteres que puedan alterar la consulta (riesgo de inyección SOQL si el valor proviene de una fuente no confiable sin sanear). Los métodos equivalentes enSfccServicesBatchClient(_getOrdersAndShipments, etc.) construyen el SOQL en un métododefaultde Java concatenando strings de forma similar, con el mismo riesgo si las listas de IDs no están saneadas antes de llamar al cliente. -
Manejo de errores que colapsa a
Boolean: Los métodos de negocio deSfccServicesClientUtilsdevuelventrue/falseen lugar de propagar o envolver la excepción original, lo que dificulta al consumidor distinguir entre distintos tipos de fallo (red, autenticación, validación de Salesforce) sin inspeccionar los logs. -
Doble librería JSON activa (Jackson +
org.json+ Gson puntual):SfccServicesClientUtilsusa unObjectMapperde Jackson para serializar los composite requests, pero manipula el JSON de entrada (orderJsonObject,accountJsonObject) conorg.json.JSONObject/JSONArray, y los POJOs usan anotaciones Gson además de Jackson — tres enfoques de JSON coexistiendo en el mismo proyecto. -
Constante
EXCLUDED_SKUhardcodeada:SfccServicesClientUtilsexcluye explícitamente el SKU"NA0100155"en la lógica de creación de pedido — una regla de negocio específica sin explicación en el código; pendiente de verificar su propósito (posible SKU de servicio/gastos de envío que no debe tratarse como línea de producto). -
Sin tests implementados: No se ha encontrado directorio
src/test/en el proyecto. -
SfccServicesClientApplication.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.