donation-update
1. Descripción general
Según el pom.xml, el proyecto se describe como "Donation update". Es un microservicio batch (runner) que procesa las líneas de pedido (OrderLine) de tipo producto DONATION que aún no se han verificado (isCheckDonation), y registra cada donación en la API externa de WorldCoo. Forma parte del ecosistema de runners de Hawkers, reutilizando logistics-commons como capa de persistencia y slack-client para notificaciones de error.
2. Información técnica
| Campo | Valor |
|---|---|
artifactId | donation-update |
groupId | com.hawkersco |
version | 1.0.25 |
| Java | 25 (maven.compiler.release=25) |
| Spring Boot | 4.0.6 |
| Tipo de artefacto | jar (ejecutable, Spring Boot batch/CLI) |
| Módulos | No aplica (proyecto de módulo único) |
3. Arquitectura y diseño
No es una API REST: es una aplicación Spring Boot CLI con un único CommandLineRunner (DonationUpdateRunner, Ordered orden 1) que ejecuta todo el flujo y termina la JVM al finalizar.
Paquetes principales:
com.hawkersco.donationupdate— clase principal (DonationUpdateApplication) y el runner..config—DonationUpdateConfig(declaración manual deOrderService,OrderLineServiceyPersistenceManagedTypesdelogistics-commons).
flowchart TD
A[DonationUpdateRunner] -->|findTop1000ByIsCheckDonationIsNullOrIsCheckDonation false| B[(logistics<br/>OrderLine)]
B --> C{cdProductType == DONATION?}
C -- No --> D[markAsChecked]
C -- Sí --> E[callWorldCooApi]
E -->|2xx| D
E -->|No 2xx sin excepcion| F[notifySlackFailure]
E -->|RestClientResponseException| D
A -->|System.exit| G[Fin del proceso]
DonationUpdateConfig declara manualmente OrderService y OrderLineService como beans (no son @Component en la librería externa logistics-commons), además del bean PersistenceManagedTypes para que JPA escanee com.hawkersco.logisticscommons.dao.
4. Dependencias principales
| Dependencia | Propósito |
|---|---|
spring-boot-starter | Núcleo de Spring Boot (sin web, es un runner CLI) |
com.hawkersco:worldcoo-api:1.0.25-SNAPSHOT | Cliente @HttpExchange (WorldCooApiClient) y modelo (WorldCooDonationRequest) para la API de WorldCoo |
com.hawkersco:logistics-commons:1.0.25-SNAPSHOT | Entidades JPA (Order, OrderLine) y servicios (OrderService, OrderLineService) |
com.hawkersco:slack-client:1.0.25-SNAPSHOT | Notificaciones de error a Slack |
spring-boot-starter-test (test) | JUnit 5 + Spring Test |
5. API / Endpoints
No aplica a este proyecto. Es un batch/runner sin capa REST.
6. Integraciones externas
| Sistema | Protocolo | Dirección | Detalle |
|---|---|---|---|
API WorldCoo (https://api.worldcoo.com) | HTTP (@HttpExchange vía WorldCooApiClient) | Saliente | Envío de donaciones (sendDonation, versión v3) |
PostgreSQL (logistics) | JDBC | Entrante/Saliente | Lectura de OrderLine/Order pendientes y actualización de isCheckDonation |
| Slack | HTTP (SlackClient) | Saliente | Notificación de fallos en el envío de la donación |
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 | Ejemplo (producción) |
|---|---|---|
worldcoo.credentials.url | URL base de la API de WorldCoo | ${worldcooCredUrl} |
worldcoo.credentials.key | Token (JWT) de autenticación de WorldCoo | ${worldcooCredKey} |
spring.datasource.url | URL JDBC de la BD logistics | ${dbLogisitcsUrl} |
spring.datasource.username | Usuario de BD | ${dbLogisitcsUsername} |
spring.datasource.password | Contraseña de BD | ${dbLogisitcsPassword} |
slack.client.url | URL API Slack | ${slackClientUrl} |
slack.auth.token | Token bot de Slack | ${slackAuthToken} |
slack.channel.id | Canal de notificaciones | ${slackChannelId} |
spring.output.ansi.enabled | Colores ANSI en el log de consola | ALWAYS |
⚠️ Alerta de seguridad
El fichero src/main/resources/application.properties (perfil local) contiene actualmente credenciales reales en texto plano: token JWT de producción de WorldCoo, contraseña de la base de datos PostgreSQL y token de bot de Slack (xoxb-...). Además, el fichero conserva comentado un bloque "TEST" con otro token JWT real de sandbox de WorldCoo, que también queda expuesto en el repositorio aunque no se use en tiempo de ejecución. Ninguno de estos valores se ha reproducido en este documento. Se recomienda:
- Rotar el token de producción de WorldCoo, la contraseña de BD y el token de Slack.
- Eliminar el bloque comentado con el token JWT de sandbox, o sustituirlo por un marcador si se quiere conservar como referencia.
- 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 (logistics), acceso vía JPA a través de la librería logistics-commons (@EnableJpaRepositories("com.hawkersco.logisticscommons.repository")). spring.jpa.hibernate.ddl-auto=none: no hay generación ni migración automática del esquema desde este proyecto. Entidades relevantes: Order, OrderLine (campo isCheckDonation). No hay Flyway/Liquibase en este repositorio.
9. Procesos programados y mensajería
No hay @Scheduled ni listeners de colas: la periodicidad la impone el CronJob de Kubernetes (k8s/cronjob.yaml), que ejecuta el contenedor cada 5 minutos (schedule: "0/5 * * * *", concurrencyPolicy: Forbid). Flujo único ejecutado por DonationUpdateRunner:
- Consulta hasta 1000
OrderLinemedianteorderLineService.findTop1000ByIsCheckDonationIsNullOrIsCheckDonationOrderByIdOrderLineAsc(false)(líneas conisCheckDonationnulo ofalse, ordenadas por ID). - Para cada línea:
- Si
cdProductTypeno es"DONATION"(comparación insensible a mayúsculas), se marca directamente como verificada (markAsChecked) sin llamar a la API. - Si es
"DONATION", se construye unWorldCooDonationRequest(campaña = SKU, importe = precio unitario redondeado a entero conRoundingMode.HALF_DOWN, moneda y código de pedido delOrderpadre) y se llama aworldCooApiClient.sendDonation("v3", request).
- Si
- Si la llamada devuelve una respuesta con código no 2xx (sin lanzar excepción), se notifica el fallo a Slack y la línea queda sin marcar (se reintentará en la siguiente ejecución).
- Si la llamada devuelve 2xx, o si lanza
RestClientResponseException, la línea se marca como verificada (ver hallazgo crítico en la sección 13). - Al terminar, cierra la JVM (
System.exit(SpringApplication.exit(context))).
10. Ejecución en local
Requisitos previos: JDK 25, Maven, acceso a la BD logistics y credenciales válidas de WorldCoo y Slack en un application.properties local.
# Compilar sin tests (igual que en CI)
./mvnw -B -DskipTests clean install
# Compilar con tests
./mvnw clean install
# Ejecutar un test concreto
./mvnw test -Dtest=DonationUpdateApplicationTests
# Ejecutar la aplicación localmente
./mvnw spring-boot:run
Al ser un CommandLineRunner, no expone Actuator/health: la forma de verificar la ejecución es revisar el log de consola o consultar el estado de isCheckDonation en la tabla order_line tras la ejecución.
11. Despliegue
- Imagen: construida con
jib-maven-plugin(baseeclipse-temurin:25-jre,containerizingMode=packaged), publicada eneurope-west3-docker.pkg.dev/pi-saldum/pi-repo/donation-update:<tag>. - Orquestación: Kubernetes
CronJob(k8s/cronjob.yaml) en el clúster GKEpi-cluster-hw(zonaeurope-west3-a, proyectopi-saldum), namespacepi, ejecutándose cada 5 minutos. - CI/CD (Jenkins): pipeline con 3 etapas —
Checkout→Build & Push(sustituyeapplication-pro.propertiesporapplication.propertiesantes demvn clean package jib:build) →Deploy to GKE(borra elCronJobexistente con--ignore-not-foundy aplica el manifiesto templado víased). - Las variables sensibles se inyectan en el pod mediante un
Secretde Kubernetes llamado igual que la app (donation-update).
Job de Jenkins: https://jenkins-pi.hawkersco.net/job/donation-update/
12. Manejo de errores y logging
No hay una estrategia de excepciones centralizada (no hay @ControllerAdvice, es un runner). callWorldCooApi captura específicamente RestClientResponseException (lanzada por RestClient ante respuestas HTTP de error) y solo la registra en el log (LOGGER.error) sin propagarla ni notificar a Slack — ver hallazgo crítico en la sección 13. El resto de errores no capturados explícitamente propagarían y detendrían la ejecución del runner, quedando registrados como fallo del Job de Kubernetes. Logging mediante SLF4J estándar (consola).
13. Notas y consideraciones
- Bug crítico: las donaciones fallidas por excepción se marcan como verificadas sin notificar:
callWorldCooApicapturaRestClientResponseException(el caso más habitual cuandoRestClientrecibe una respuesta HTTP de error, p. ej. 4xx/5xx) y devuelveOptional.empty()— el mismo valor que se devuelve en caso de éxito. Como consecuencia,processOrderLineinterpreta cualquier error HTTP capturado como excepción como si fuera un envío correcto y llama amarkAsChecked(orderLine), marcando la línea como verificada. Esto contradice tanto el comportamiento esperado como lo descrito en elCLAUDE.mddel proyecto ("On failure → sends a Slack alert ... and leaves the line unchecked for retry"): en la práctica, solo el caso de respuesta no-2xx que no lance excepción llega a notificar por Slack y deja la línea pendiente; cualquier error HTTP que WorldCoo devuelva como excepción se pierde silenciosamente, sin alerta y sin reintento, ya que la donación queda marcada como "verificada" sin haberse registrado realmente. Se recomienda revisarcallWorldCooApipara que el bloquecatch (RestClientResponseException e)devuelvaOptional.of(e.getMessage())en lugar deOptional.empty(). - Mensaje de error vacío en la notificación de Slack: en el camino de respuesta no-2xx sin excepción,
callWorldCooApidevuelveOptional.of("")(cadena vacía) en lugar de incluir el código de estado o el cuerpo de la respuesta, por lo que el mensaje de Slack resultante (SLACK_ERROR_TEMPLATE) siempre muestra el campoerrorvacío en este camino. CLAUDE.mddescribe el comportamiento "deseado" pero no el real: la arquitectura general (dependencias, configuración, infraestructura) coincide con el código, pero la descripción del flujo de fallo ("on failure sends Slack alert and leaves unchecked") no se corresponde con el comportamiento real explicado arriba.- Ver alerta de seguridad en la sección 7 sobre credenciales reales expuestas en
application.properties(incluyendo un token comentado de sandbox de WorldCoo).