Skip to main content

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

CampoValor
artifactIddonation-update
groupIdcom.hawkersco
version1.0.25
Java25 (maven.compiler.release=25)
Spring Boot4.0.6
Tipo de artefactojar (ejecutable, Spring Boot batch/CLI)
MódulosNo 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.
  • .configDonationUpdateConfig (declaración manual de OrderService, OrderLineService y PersistenceManagedTypes de logistics-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

DependenciaPropósito
spring-boot-starterNúcleo de Spring Boot (sin web, es un runner CLI)
com.hawkersco:worldcoo-api:1.0.25-SNAPSHOTCliente @HttpExchange (WorldCooApiClient) y modelo (WorldCooDonationRequest) para la API de WorldCoo
com.hawkersco:logistics-commons:1.0.25-SNAPSHOTEntidades JPA (Order, OrderLine) y servicios (OrderService, OrderLineService)
com.hawkersco:slack-client:1.0.25-SNAPSHOTNotificaciones 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

SistemaProtocoloDirecciónDetalle
API WorldCoo (https://api.worldcoo.com)HTTP (@HttpExchange vía WorldCooApiClient)SalienteEnvío de donaciones (sendDonation, versión v3)
PostgreSQL (logistics)JDBCEntrante/SalienteLectura de OrderLine/Order pendientes y actualización de isCheckDonation
SlackHTTP (SlackClient)SalienteNotificació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).

ClaveDescripciónEjemplo (producción)
worldcoo.credentials.urlURL base de la API de WorldCoo${worldcooCredUrl}
worldcoo.credentials.keyToken (JWT) de autenticación de WorldCoo${worldcooCredKey}
spring.datasource.urlURL JDBC de la BD logistics${dbLogisitcsUrl}
spring.datasource.usernameUsuario de BD${dbLogisitcsUsername}
spring.datasource.passwordContraseña de BD${dbLogisitcsPassword}
slack.client.urlURL API Slack${slackClientUrl}
slack.auth.tokenToken bot de Slack${slackAuthToken}
slack.channel.idCanal de notificaciones${slackChannelId}
spring.output.ansi.enabledColores ANSI en el log de consolaALWAYS

⚠️ 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:

  1. Rotar el token de producción de WorldCoo, la contraseña de BD y el token de Slack.
  2. Eliminar el bloque comentado con el token JWT de sandbox, o sustituirlo por un marcador si se quiere conservar como referencia.
  3. Sustituir los valores hardcodeados de application.properties por credenciales de un entorno de desarrollo aislado.
  4. 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:

  1. Consulta hasta 1000 OrderLine mediante orderLineService.findTop1000ByIsCheckDonationIsNullOrIsCheckDonationOrderByIdOrderLineAsc(false) (líneas con isCheckDonation nulo o false, ordenadas por ID).
  2. Para cada línea:
    • Si cdProductType no es "DONATION" (comparación insensible a mayúsculas), se marca directamente como verificada (markAsChecked) sin llamar a la API.
    • Si es "DONATION", se construye un WorldCooDonationRequest (campaña = SKU, importe = precio unitario redondeado a entero con RoundingMode.HALF_DOWN, moneda y código de pedido del Order padre) y se llama a worldCooApiClient.sendDonation("v3", request).
  3. 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).
  4. 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).
  5. 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 (base eclipse-temurin:25-jre, containerizingMode=packaged), publicada en europe-west3-docker.pkg.dev/pi-saldum/pi-repo/donation-update:<tag>.
  • Orquestación: Kubernetes CronJob (k8s/cronjob.yaml) en el clúster GKE pi-cluster-hw (zona europe-west3-a, proyecto pi-saldum), namespace pi, ejecutándose cada 5 minutos.
  • CI/CD (Jenkins): pipeline con 3 etapas — CheckoutBuild & Push (sustituye application-pro.properties por application.properties antes de mvn clean package jib:build) → Deploy to GKE (borra el CronJob existente con --ignore-not-found y aplica el manifiesto templado vía sed).
  • Las variables sensibles se inyectan en el pod mediante un Secret de 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: callWorldCooApi captura RestClientResponseException (el caso más habitual cuando RestClient recibe una respuesta HTTP de error, p. ej. 4xx/5xx) y devuelve Optional.empty() — el mismo valor que se devuelve en caso de éxito. Como consecuencia, processOrderLine interpreta cualquier error HTTP capturado como excepción como si fuera un envío correcto y llama a markAsChecked(orderLine), marcando la línea como verificada. Esto contradice tanto el comportamiento esperado como lo descrito en el CLAUDE.md del 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 revisar callWorldCooApi para que el bloque catch (RestClientResponseException e) devuelva Optional.of(e.getMessage()) en lugar de Optional.empty().
  • Mensaje de error vacío en la notificación de Slack: en el camino de respuesta no-2xx sin excepción, callWorldCooApi devuelve Optional.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 campo error vacío en este camino.
  • CLAUDE.md describe 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).