Skip to main content

Bradery Create DB

1. Descripción general

bradery-create-db es un microservicio batch (según el pom.xml, "The Bradery Create Db") que ingiere en la base de datos logística de Hawkers los pedidos del marketplace The Bradery. No es un servicio HTTP: es una aplicación Spring Boot basada en CommandLineRunner que se ejecuta hasta completar su trabajo y termina, orquestada como un CronJob de Kubernetes.

El flujo completo consta de dos fases secuenciales: primero descarga los ficheros Excel de pedidos adjuntos en correos de Gmail y los sube a Google Cloud Storage (GCS); después parsea esos ficheros y persiste los pedidos (cliente, líneas, envío) en la base de datos PostgreSQL de logística (logistics-commons). Dentro del ecosistema Hawkers, cumple el mismo rol que otros runners *-create-db (p. ej. showroom-create-db, privalia-create-db): puente entre un marketplace externo sin API REST estructurada y el modelo de datos interno de pedidos.

2. Información técnica

PropiedadValor
artifactIdbradery-create-db
groupIdcom.hawkersco
version1.0.25
Java25
Spring Boot4.0.6
Tipo de artefactoJAR ejecutable (imagen de contenedor vía Jib)
MódulosProyecto único (no multi-módulo)

3. Arquitectura y diseño

Estructura del proyecto:

com.hawkersco.braderycreatedb
├── BraderyCreateDbApplication.java # @SpringBootApplication + @EnableJpaRepositories (repos en logistics-commons)
├── TheBraderyGetMailInfoRunner.java # CommandLineRunner, orden 1: lee Gmail, sube XLSX a GCS
├── TheBraderyCreateDbRunner.java # CommandLineRunner, orden 2: parsea XLSX de GCS y persiste pedidos
├── config/
│ └── TheBraderyCreateDbConfig.java # Declara manualmente los @Bean de servicios de logistics-commons
└── utils/
├── BraderyMailReaderUtils.java # Lectura IMAP de Gmail, extracción de adjuntos .xlsx, subida a GCS
└── TheBraderyCreateDbUtils.java # Parseo XLSX (Apache POI), transformación a entidades de logistics-commons

Flujo principal (dos runners ordenados)

sequenceDiagram
participant CronJob as CronJob K8s
participant R1 as TheBraderyGetMailInfoRunner (orden 1)
participant Gmail as Gmail (IMAP)
participant GCS as GCS (pi-logistics-segment)
participant R2 as TheBraderyCreateDbRunner (orden 2)
participant DB as PostgreSQL (logistics)

CronJob->>R1: run()
R1->>Gmail: conecta IMAP, lee carpeta "TheBradery-Pending"
Gmail-->>R1: mensajes con adjuntos .xlsx
R1->>GCS: sube cada .xlsx a bradery-create-db/files-pending/<yyyy/MM/dd>/
R1->>Gmail: mueve mensajes a "TheBradery-Processed"

CronJob->>R2: run()
R2->>GCS: lista blobs en bradery-create-db/files-pending/
loop por cada .xlsx
R2->>R2: parsea hojas con Apache POI → líneas agrupadas por "commande"
R2->>DB: si el pedido (HWBRAD+número) no existe, persiste OrderMarketplace, Customer, Order, OrderLine, Shipment, OrderShipmentStatus
R2->>GCS: mueve el blob a bradery-create-db/files-processed/
end

BraderyCreateDbApplication usa @EnableJpaRepositories(basePackages = "com.hawkersco.logisticscommons.repository") porque los repositorios JPA viven en la librería externa logistics-commons, fuera del árbol de paquetes de esta aplicación. TheBraderyCreateDbConfig declara manualmente cada bean de servicio de logistics-commons (no hay @ComponentScan para ellos) y expone PersistenceManagedTypes escaneando com.hawkersco.logisticscommons.dao — cualquier nuevo servicio de logistics-commons que se necesite requiere añadir su @Bean aquí.

TheBraderyCreateDbUtils.setOrderLineField usa reflexión (Field.setAccessible) para mapear las cabeceras normalizadas del XLSX directamente a los campos de TheBraderyOrder.TheBraderyOrderLine; la columna prixunitaireht se remapea explícitamente a price.

4. Dependencias principales

DependenciaVersiónPropósito
spring-boot-starter(gestionada SB4)Base de Spring Boot (contexto, CommandLineRunner)
spring-boot-starter-mail(gestionada SB4)Cliente IMAP (JavaMail) para leer los correos de Gmail
com.hawkersco:logistics-commons1.0.25-SNAPSHOTEntidades JPA (Order, OrderLine, Customer, Shipment, etc.) y sus servicios
com.hawkersco:bradery-client1.0.25-SNAPSHOTPOJO TheBraderyOrder y su clase anidada TheBraderyOrderLine
com.hawkersco:pi-function-commons1.0.25-SNAPSHOTUtilidad DateUtils
org.apache.poi:poi / poi-ooxml5.3.0Parseo de ficheros .xlsx (Apache POI)
com.google.code.gson:gson(gestionada SB4)Serialización del pedido crudo (Order.rawData)
org.apache.commons:commons-lang3(gestionada SB4)Utilidades de manejo de cadenas
org.projectlombok:lombok(gestionada; annotationProcessorPath fija 1.18.46)Generación de boilerplate
spring-boot-starter-test(gestionada SB4)Testing (scope test)

La dependencia hacia Google Cloud Storage (com.google.cloud.storage.*, usada en ambos runners y en BraderyMailReaderUtils) no aparece como dependencia directa en el pom.xml — llega transitivamente, probablemente a través de logistics-commons o pi-function-commons; pendiente de verificar el árbol de dependencias exacto.

CLAUDE.md menciona una dependencia hacia slack-client para notificaciones y fija las librerías internas en la versión 1.0.17; ninguna de las dos afirmaciones se corresponde con el pom.xml actual (no hay slack-client en las dependencias, ni se ha encontrado ninguna referencia a Slack en el código, y las librerías internas están en 1.0.25-SNAPSHOT). Se documenta el pom.xml realmente presente.

5. API / Endpoints

No aplica a este proyecto. bradery-create-db es un batch CommandLineRunner sin servidor HTTP ni controladores REST.

6. Integraciones externas

SistemaProtocoloDirecciónDescripción
Gmail (buzón de The Bradery)IMAP sobre SSL (puerto 993)EntranteLectura de correos con pedidos adjuntos en la carpeta TheBradery-Pending
Google Cloud Storage (pi-logistics-segment)API GCSSaliente/EntranteAlmacenamiento intermedio de los .xlsx (pendientes → procesados)
PostgreSQL (logistics, vía logistics-commons)JDBCSalientePersistencia de pedidos, clientes, líneas y envíos

Convenciones de negocio relevantes:

  • Todos los pedidos de The Bradery se prefijan con HWBRAD y se asocian al Source ID 67 (constante SOURCE_ID_BRADERY, hardcodeada en ambos runners y en TheBraderyCreateDbUtils).
  • Para los países del "ISO Group 1" (FR, ES, IT, BE, NL, LX, GB), se asigna automáticamente el método de envío con ID 1 y el tipo de servicio de pedido con ID 19.
  • Detección de duplicados: un pedido se omite silenciosamente si orderService.findByDsOrder(nameOrder) ya devuelve resultados.

7. Configuración

El proyecto usa dos ficheros de propiedades: application.properties (desarrollo local, con valores reales) y application-pro.properties (producción, con placeholders ${VAR} inyectados por Kubernetes). El Jenkinsfile sustituye application.properties por application-pro.properties antes de empaquetar (mv src/main/resources/application-pro.properties src/main/resources/application.properties).

Propiedades de configuración

PropiedadDescripciónValor en producción
spring.datasource.urlURL JDBC de la base de datos PostgreSQL de logística${dbLogisticsUrl}
spring.datasource.usernameUsuario de la base de datos${dbLogisticsUsername}
spring.datasource.passwordContraseña de la base de datos${dbLogisticsPassword}
spring.datasource.driver-class-nameDriver JDBC (org.postgresql.Driver)org.postgresql.Driver
spring.jpa.hibernate.ddl-autoEstrategia de esquema Hibernate (none, no gestiona el esquema)none
spring.datasource.hikari.*Ajustes del pool de conexiones HikariCP (tamaño máx. 5, idle mín. 2, etc.)(valores fijos, ver application-pro.properties)
email.utils.bradery.usernameUsuario IMAP de Gmail para el buzón de The Bradery${braderyEmailUsername}
email.utils.bradery.passwordContraseña/App Password IMAP de Gmail${braderyEmailPassword}
email.utils.bradery.validation.order.prefixPrefijo de validación de pedido (HWBRAD)HWBRAD
gcs.bucket.nameNombre del bucket GCS usado como almacenamiento intermedio${bucketName}

Alerta de seguridad: El fichero src/main/resources/application.properties (perfil de desarrollo local) contiene actualmente credenciales reales en texto plano — contraseña de la base de datos PostgreSQL y contraseña/App Password de la cuenta de Gmail usada para IMAP. Este documento no reproduce esos valores, pero su presencia en el repositorio es un riesgo de seguridad real: deberían rotarse y sustituirse por variables de entorno o un gestor de secretos también en el perfil local, igual que ya se hace en application-pro.properties.

Variables de entorno (perfil de producción, inyectadas por Kubernetes vía Secret)

VariablePropiedad mapeada
dbLogisticsUrlspring.datasource.url
dbLogisticsUsernamespring.datasource.username
dbLogisticsPasswordspring.datasource.password
braderyEmailUsernameemail.utils.bradery.username
braderyEmailPasswordemail.utils.bradery.password
bucketNamegcs.bucket.name

8. Persistencia

Base de datos: PostgreSQL (logistics, instancia noctua-instance.hawkersco.net), accedida vía JPA/Hibernate a través de las entidades y repositorios de la librería externa logistics-commons (no definidos en este proyecto).

Entidades principales manipuladas por este runner:

EntidadRol
OrderMarketplaceRegistro de vínculo entre el pedido interno (HWBRAD...) y el pedido de marketplace original
Customer / CustomerSourceCliente asociado al pedido, con su origen registrado para The Bradery (Source ID 67)
OrderPedido logístico completo (direcciones, importes, país, estado)
OrderLineLíneas de producto del pedido (SKU, cantidad, precio unitario)
Shipment / ShipmentLineEnvío generado a partir del pedido y sus líneas
OrderShipmentStatusEstado inicial de envío (PENDING_SHIPMENT)

No se han encontrado migraciones Flyway/Liquibase en este proyecto; el esquema se gestiona externamente (spring.jpa.hibernate.ddl-auto=none), presumiblemente por logistics-commons o por gestión manual de la base de datos compartida.

9. Procesos programados y mensajería

No hay @Scheduled dentro de la aplicación Java: la programación periódica se delega completamente a Kubernetes. El manifiesto k8s/cronjob.yaml define un CronJob con la expresión 0 8-11 * * * (ejecuta en el minuto 0 de cada hora entre las 8:00 y las 11:00, todos los días), que lanza un Job con activeDeadlineSeconds: 3600 (se cancela si supera 1 hora de ejecución) y restartPolicy: OnFailure.

Dentro de la propia ejecución del proceso, los dos CommandLineRunner (TheBraderyGetMailInfoRunner, orden 1; TheBraderyCreateDbRunner, orden 2) se ejecutan secuencialmente según su Ordered.getOrder() al arrancar el contexto de Spring, y el proceso termina al finalizar ambos.

10. Ejecución en local

Requisitos previos

  • Java 25
  • Maven 3.x
  • Acceso a la base de datos PostgreSQL de logística y credenciales IMAP de Gmail válidas (ver advertencia de seguridad en la sección 7)
  • Credenciales de Google Cloud (Application Default Credentials) con acceso al bucket gcs.bucket.name

Comandos

# Build sin tests (como en CI)
./mvnw -B -DskipTests clean install

# Build con tests
./mvnw clean verify

# Ejecutar un test concreto
./mvnw -Dtest=ClassName test

# Ejecutar localmente (requiere BD y credenciales GCS/Gmail configuradas en application.properties)
./mvnw spring-boot:run

# Build de imagen Docker/Jib tras "mvn package"
docker build -t bradery-create-db .

CLAUDE.md advierte explícitamente no replicar manualmente en desarrollo local la sustitución de application-pro.properties que hace Jenkins (jenkins/scripts/mvn.sh).

Al no tener servidor HTTP, no existe endpoint de health (/actuator/health); la verificación de que el proceso se ejecutó correctamente se hace revisando los logs (TheBraderyGetMailInfoRunner - END, TheBraderyCreateDbRunner - END) o el estado del Job en Kubernetes.

11. Despliegue

A diferencia de los clientes del ecosistema (JAR publicado en el registro Maven), bradery-create-db se despliega como imagen de contenedor en Google Kubernetes Engine (GKE), orquestada por un CronJob.

Pipeline de Jenkins (Jenkinsfile):

  1. Checkout — descarga el código del repositorio.
  2. Build & Push — sustituye application.properties por application-pro.properties, compila con mvn clean package jib:build -DskipTests -U -Dimage.tag=${BUILD_NUMBER} (plugin Jib, sin necesidad de Docker daemon) y publica la imagen en europe-west3-docker.pkg.dev/pi-saldum/pi-repo/bradery-create-db:${BUILD_NUMBER}.
  3. Deploy to GKE — obtiene credenciales del clúster pi-cluster-hw (zona europe-west3-a, proyecto pi-saldum), elimina el CronJob existente (kubectl delete cronjob ... --ignore-not-found) y aplica el manifiesto k8s/cronjob.yaml (con ${APP_NAME}/${IMAGE_TAG} sustituidos vía sed) en el namespace pi.

La imagen base para el runtime, según el pom.xml (plugin jib-maven-plugin), es eclipse-temurin:25-jre — consistente con Java 25, a diferencia de los Dockerfile obsoletos encontrados en algunos clientes del ecosistema (sfcc-services-client, zalando-client).

CLAUDE.md documenta un pipeline con etapas Build → KICS → SonarQube → Test → Docker push → Deploy → Cleanup, que no se corresponde con el Jenkinsfile actual (tres etapas: Checkout, Build & Push, Deploy to GKE, sin KICS/SonarQube/Cleanup). Se documenta el Jenkinsfile realmente presente.

Job de Jenkins:

https://jenkins-pi.hawkersco.net/job/bradery-create-db/

12. Manejo de errores y logging

Ambos runners usan java.util.logging.Logger (no SLF4J/Logback) para registrar el progreso (INIT/END) y los errores.

  • TheBraderyGetMailInfoRunner.run() no captura ninguna excepción: si braderyMailReaderUtils.getBraderyMailAndLoadOnGC() falla (p. ej. error IMAP), la excepción se propaga fuera del runner — dado que Spring Boot ejecuta los CommandLineRunner durante el arranque, esto probablemente hace que la aplicación termine con código de error, lo que en un CronJob con restartPolicy: OnFailure provocaría un reintento del Job completo (repitiendo también, potencialmente, el runner de creación de BD si ya se llegó a ejecutar parcialmente).
  • TheBraderyCreateDbRunner.run() envuelve el bucle principal en un único try/catch(Exception) que registra el error (LOGGER.severe) y continúa sin relanzar — un fallo al procesar un blob concreto no detiene el procesamiento de los siguientes, pero tampoco queda reflejado como fallo del Job de Kubernetes.
  • moveToProcessed captura sus propias excepciones y solo registra un warning si no puede mover el blob — un archivo podría quedar sin mover a files-processed/ sin que ello se considere un error del proceso, con riesgo de reprocesarlo (aunque la detección de duplicados por dsOrder mitigaría la re-inserción en BD).
  • setOrderLineField (reflexión sobre columnas XLSX) captura NoSuchFieldException/IllegalAccessException y solo registra un warning — una columna del Excel no reconocida se ignora silenciosamente en lugar de fallar.

13. Notas y consideraciones

  • Credenciales reales en application.properties (desarrollo): ver alerta detallada en la sección 7. Es el hallazgo de seguridad más relevante de este proyecto.

  • Documentación previa (CLAUDE.md) desalineada con el código y la infraestructura actuales: menciona una dependencia slack-client inexistente, fija las librerías internas en la versión 1.0.17 (el pom.xml real usa 1.0.25-SNAPSHOT) y describe un pipeline de Jenkins con etapas (KICS, SonarQube, Test, Cleanup) que no están en el Jenkinsfile actual. Ver detalle en las secciones 4 y 11.

  • Manejo de errores asimétrico entre los dos runners: TheBraderyGetMailInfoRunner no captura ninguna excepción (fallo = caída de la aplicación), mientras que TheBraderyCreateDbRunner captura todo en un único bloque y continúa — un fallo aislado al leer un correo concreto detiene toda la ejecución del primer runner (y, por tanto, nunca se llega a ejecutar el segundo en esa invocación), mientras que un fallo al procesar un blob XLSX concreto no detiene el resto. Pendiente de verificar si esta asimetría es intencional.

  • Uso de reflexión para mapear columnas Excel a campos Java: TheBraderyCreateDbUtils.setOrderLineField usa Field.setAccessible(true) y getDeclaredField(header) para asignar valores directamente a los campos de TheBraderyOrderLine según el nombre de columna normalizado del Excel. Esto acopla fuertemente el nombre de los campos Java (en bradery-client) al nombre de las columnas del fichero que envía The Bradery — un cambio en el nombrado de columnas del proveedor requeriría o bien un remapeo explícito (como ya ocurre con prixunitairehtprice) o renombrar el campo Java correspondiente.

  • Constantes de negocio duplicadas entre clases: SOURCE_ID_BRADERY (67L), ORDER_PREFIX ("HWBRAD") y STATUS_PENDING_SHIPMENT están declaradas como constantes privadas independientes tanto en TheBraderyCreateDbRunner como en TheBraderyCreateDbUtils, en lugar de compartir una única fuente de verdad — riesgo de divergencia si se actualiza una sin la otra.

  • saveShipment no aborta la persistencia del pedido si falla: Si logisticCommonsUtils.saveShipmentFromOrder lanza ParseException, el error se registra como warning pero el pedido y sus líneas ya quedaron guardados sin envío asociado — un pedido podría quedar en base de datos sin Shipment ni OrderShipmentStatus inicial.

  • Dependencia de Google Cloud Storage no declarada directamente: El código importa com.google.cloud.storage.* en varios puntos, pero no hay ninguna dependencia google-cloud-storage explícita en el pom.xml — llega transitivamente desde alguna de las librerías internas de Hawkers. Pendiente de verificar el árbol de dependencias exacto (mvn dependency:tree) si se necesita fijar o actualizar esa versión de forma independiente.

  • Sin tests implementados: No se ha encontrado directorio src/test/ en el proyecto, consistente con que el Jenkinsfile actual no incluye ninguna etapa de test.