sfcc-abandoned-cart
1. Descripción general
Según el pom.xml, el proyecto se describe como "SFCC API to receive abandonded carts". Es un servicio híbrido (API REST + tarea programada) que recibe eventos de carrito abandonado desde la web (Hawkers/Northweek), los deposita como ficheros en Google Cloud Storage, y cada 2 minutos los normaliza (locale, moneda, longitud de campos) y los envía a Salesforce.
2. Información técnica
| Campo | Valor |
|---|---|
artifactId | sfcc-abandoned-cart |
groupId | com.hawkersco |
version | 1.0.25 |
| Java | 25 |
| Spring Boot | 4.0.6 |
| Tipo de artefacto | jar (ejecutable, API web de larga duración) |
| Módulos | No aplica (proyecto de módulo único) |
3. Arquitectura y diseño
.controller—SfccAbandonedCartInputController(recepción de eventos)..component—SfccAbandonedCartOutputCronJob(procesamiento programado, lógica principal)..config—SecurityConfig(HTTP Basic + CORS),RestClientsConfig(@HttpExchange),SfccAbandonedCartConfig,FilterConfig..restclient—AbandonedTokenCartClient(obtención de token OAuth de Salesforce),AbandonedCartClient(envío del carrito)..entity—AbandonedCartsRequest,AbandonedCartsResponse,SfccAbandonedCartEvent..dao—UserOauth,RoleOauth(usuarios de Spring Security leídos desde PostgreSQL)..filter—MaliciousRequestFilter(ver hallazgo de seguridad en la sección 13).
flowchart TD
A["POST /push (Basic Auth, ROLE_ADMIN)"] -->|valida JSON| B[(GCS hawkersco_web_events<br/>topics/sfcc_abandoned_cart/)]
C["@Scheduled cada 2 min<br/>readCSVGoogleStorage"] -->|lee| D[(GCS send_carts_pending/)]
E["@Scheduled cada 2 min<br/>readJsonErrorGoogleStorage"] -->|lee| F[(GCS send_carts_errors/)]
C -->|normaliza locale/moneda/líneas/ciudad| G[AbandonedCartClient]
E --> G
G -->|POST con token Bearer| H[Salesforce]
G -->|éxito| I[Borra blob]
G -->|error clasificado| J[send_carts_errors_mail/ + Slack]
C -->|tras procesar| K[archiva en send_carts_processed/]
4. Dependencias principales
| Dependencia | Propósito |
|---|---|
spring-boot-starter-web + spring-boot-starter-security | API REST con HTTP Basic |
spring-boot-starter-data-jpa + postgresql | Usuarios/roles de Spring Security (UserOauth, RoleOauth) |
org.apache.commons:commons-csv | Parseo de los ficheros CSV de carritos pendientes |
com.sun.xml.bind:jaxb-core/impl | Soporte JAXB |
org.json:json | Validación de JSON de entrada |
com.hawkersco:pi-function-commons | StorageUtils, DirectoryUtils, DateUtils |
com.hawkersco:slack-client | Notificaciones de error |
spring-boot-starter-test (test) | JUnit 5 + Spring Test |
5. API / Endpoints
| Método | Ruta | Descripción | Autenticación |
|---|---|---|---|
GET | /push/check | Health check | Pública |
POST | /push | Recibe un evento de carrito abandonado (JSON), lo valida y sube a GCS | HTTP Basic, ROLE_ADMIN |
POST | /push/dev | Endpoint equivalente para pruebas/desarrollo | No configurado explícitamente en SecurityConfig (ver hallazgo en la sección 13) |
6. Integraciones externas
| Sistema | Protocolo | Dirección | Detalle |
|---|---|---|---|
Google Cloud Storage (bucket hawkersco_web_events) | API de GCS | Entrante/Saliente | Almacenamiento de eventos entrantes, ficheros pendientes/error/procesados |
| Salesforce | OAuth password grant + REST | Saliente | Autenticación y envío del carrito abandonado |
| Slack | HTTP (SlackClient) | Saliente | Notificación de errores clasificados (campo demasiado largo, moneda inválida, picklist inválido) |
PostgreSQL (oauth) | JDBC | Entrante | Usuarios/roles de Spring Security para el endpoint /push |
7. Configuración
En producción (application-pro.properties) las credenciales llegan por variables de entorno inyectadas como Secret de Kubernetes; en local (application.properties) el repositorio contiene actualmente valores reales hardcodeados (ver alerta de seguridad).
| Clave | Descripción |
|---|---|
gcs.bucket.name | Bucket de GCS (hawkersco_web_events) |
spring.datasource.* | Credenciales de la BD oauth (usuarios de Spring Security) |
salesforce.credentials.token.* | Credenciales OAuth password grant de Salesforce (usuario, contraseña, client-id/secret) |
sfccabandonedcart.username / .password | Credenciales HTTP Basic del endpoint /push |
slack.client.url / .auth.token / .channel.id | Configuración de Slack |
logging.level.org.springframework.security | Nivel TRACE en el perfil local (ver hallazgo en la sección 13) |
⚠️ Alerta de seguridad
El fichero src/main/resources/application.properties (perfil local) contiene actualmente credenciales reales en texto plano: contraseña de la base de datos PostgreSQL oauth, credenciales OAuth completas de Salesforce (usuario, contraseña, client-id y client-secret), las credenciales HTTP Basic del propio endpoint /push, y un token de bot de Slack. Ninguno de estos valores se ha reproducido en este documento. Se recomienda:
- Rotar las credenciales de Salesforce, la contraseña de BD, las credenciales HTTP Basic de
/pushy el token de Slack. - Sustituir los valores hardcodeados de
application.propertiespor credenciales de un entorno de desarrollo aislado. - Revisar el historial de control de versiones, ya que estas credenciales pueden seguir expuestas en commits anteriores.
8. Persistencia
Base de datos PostgreSQL oauth, acceso vía JPA. spring.jpa.hibernate.ddl-auto=none. Entidades: UserOauth, RoleOauth (usadas por Spring Security para autenticar /push). No hay Flyway/Liquibase en este repositorio.
9. Procesos programados y mensajería
SfccAbandonedCartOutputCronJob — dos métodos @Scheduled(cron = "0 */2 * ? * *") (cada 2 minutos):
readCSVGoogleStorage: descarga los ficheros CSV pendientes (send_carts_pending/, ignorando directorios de metadatos de Spark y una carpeta legacyyear=2020/), construye el request de carrito desde cada fila (mapeo posicional de columnas, con\Ninterpretado como valor vacío), normaliza el carrito (locale, moneda, máximo 5 líneas de producto, ciudades truncadas a 40 caracteres) y lo envía a Salesforce; tras procesar, archiva el blob ensend_carts_processed/{fecha}/.readJsonErrorGoogleStorage: reprocesa los ficheros previamente movidos asend_carts_errors/(reintento de errores).normalizeAndSendCart: en éxito borra el blob; en error, clasifica el mensaje (DUPLICATE_VALUE→ borra sin más;STRING_TOO_LONG/moneda inválida/picklist inválido → notifica Slack y sube copia asend_carts_errors_mail/; cualquier otro caso también borra el blob tras el log).
10. Ejecución en local
Requisitos previos: JDK 25, Maven, acceso a la BD oauth, credenciales de aplicación por defecto de Google (GCS) y credenciales OAuth válidas de Salesforce.
# Compilar sin tests
mvn -B -DskipTests clean install
# Compilar con tests
mvn clean install
# Ejecutar la aplicación localmente (perfil dev, puerto 8080)
mvn spring-boot:run
# Ejecutar un test concreto
mvn test -Dtest=SfccAbandonedCartApplicationTests
# Ejecutar el JAR de producción
java -jar target/sfcc-abandoned-cart-*.jar
Verificación de que el servicio está operativo: GET /push/check (público).
11. Despliegue
- Imagen: construida con
jib-maven-plugin(baseeclipse-temurin:25-jre,containerizingMode=packaged), publicada eneurope-west3-docker.pkg.dev/pi-saldum/pi-repo/sfcc-abandoned-cart:<tag>. ElCLAUDE.mdmenciona una imagen baseeclipse-temurin:25-jdk-alpine, que no coincide con la configuración real vía Jib. - Orquestación: Kubernetes
Deployment(noCronJob— la periodicidad la gestiona el@Scheduledinterno) en el clúster GKEpi-cluster-hw, namespacepi. - CI/CD (Jenkins): pipeline que sustituye
application-pro.propertiesporapplication.properties.
Job de Jenkins: https://jenkins-pi.hawkersco.net/job/sfcc-abandoned-cart/
12. Manejo de errores y logging
No hay un @RestControllerAdvice centralizado. processJsonFile/processCSVFile capturan excepciones genéricas por línea/registro y registran Level.SEVERE, sin interrumpir el resto del fichero. normalizeAndSendCart captura específicamente RestClientResponseException para notificar a Slack. Logging mediante java.util.logging.Logger estándar (consola), salvo el controlador de entrada que usa SLF4J.
13. Notas y consideraciones
MaliciousRequestFilterno bloquea nada, solo registra: el filtro comprueba si la URI contiene caracteres sospechosos ([,],\) y, si los encuentra, únicamente hacelogger.log(Level.SEVERE, ...)— nunca interrumpe la cadena de filtros ni rechaza la petición; siempre llama afilterChain.doFilter(...)a continuación, coincida o no el patrón. ElCLAUDE.mdlo describe como que "blocks suspicious URI characters", lo cual no es cierto en el código actual: el filtro es puramente informativo y no aporta ninguna protección real frente a las peticiones que dice detectar./push/devsin regla de seguridad explícita:SecurityConfigsolo define reglas para/push/check(público) y/push(ROLE_ADMIN), pero el controlador expone tambiénPOST /push/devsin que exista una reglarequestMatchersespecífica para esa ruta ni unanyRequest()explícito al final de la cadena. Conviene verificar en tiempo de ejecución si esta ruta queda protegida por defecto o si permanece accesible sin autenticación.- Nivel de log
TRACEpara Spring Security en el perfil local:logging.level.org.springframework.security=TRACEpuede volcar en el log detalles sensibles del proceso de autenticación (cabeceras, credenciales en tránsito); apropiado solo para depuración puntual, no como valor por defecto en el repositorio. - Ver alerta de seguridad en la sección 7 sobre credenciales reales expuestas en
application.properties.