gio-pos-sync-service
1. Descripción general
Según el pom.xml, el proyecto se describe como "Gio and Pos sync Service". Es un microservicio batch (runner, spring.main.web-application-type=none) que sincroniza los datos de clientes del sistema de gestión de ópticas GIO hacia el POS (Dynamics 365 Commerce/Retail) y hacia la base de datos de relaciones de Dynamics. Consume las notificaciones de actualización/creación de cliente que deja gio-api-notifications en un bucket de GCS, y decide si crear o editar el cliente correspondiente en POS según exista ya una relación registrada en Dynamics.
2. Información técnica
| Campo | Valor |
|---|---|
artifactId | gio-pos-sync-service |
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 sin servidor web) |
| Módulos | No aplica (proyecto de módulo único) |
3. Arquitectura y diseño
No es una API REST (spring.main.web-application-type=none): es una aplicación Spring Boot CLI con un único CommandLineRunner activo (GioPosSyncServiceRunner).
Paquetes principales:
com.hawkersco.giopossyncservice— clase principal (GioPosSyncServiceApplication) y los dos runners..Utils—GioPosSyncServiceUtils(nótese laUmayúscula del paquete, inconsistente con la convención habitual en minúscula del resto del ecosistema)..alerting—AlertingService,AlertsConfig,SyncRunMetrics,ErrorCategory: subsistema de alertado agregado por ejecución.
flowchart TD
A[GioPosSyncServiceRunner] -->|lista blobs pendientes| B[(GCS hawkers-gio<br/>update-client-gio-pending/)]
B --> C{Óptica española?}
C -- No --> D[Marca procesado sin llamar a GIO]
C -- Sí --> E[GioClient.readPaciente]
E --> F{Existe relación en Dynamics?}
F -- Sí --> G[PosClient.editClient]
F -- No --> H[PosClient.createClient + registra relación en Dynamics]
G --> I[SyncRunMetrics.recordSuccess]
H --> I
G -.->|error| J[ErrorCategory.classify]
H -.->|error| J
J -->|mueve blob a errors-dni / errors-country / errors-pos| K[(GCS)]
I --> L[AlertingService.evaluateAndNotify]
J --> L
L -->|umbral superado| M[Slack]
GioPosNotificationsRunner es una clase adicional presente en el código que solo lista y descarga los blobs pendientes sin procesarlos; está desactivada (@Component comentado) y corresponde a una versión temprana/prototipo del flujo, tal como confirma el CLAUDE.md.
4. Dependencias principales
| Dependencia | Propósito |
|---|---|
spring-boot-starter | Núcleo de Spring Boot (sin web, es un runner CLI) |
spring-orm | Soporte JPA (aunque este proyecto no declara ningún datasource propio, ver sección 13) |
com.hawkersco:gio-client:1.0.25-SNAPSHOT | Cliente @HttpExchange (GioClient) para la API de GIO (lectura de pacientes/clientes) |
com.hawkersco.posclient:pos-client:1.0.25-SNAPSHOT | Cliente @HttpExchange (PosClient) para crear/editar clientes en el POS (Dynamics Commerce) |
com.hawkersco:dynamics-client:1.0.25-SNAPSHOT | Cliente @HttpExchange (DynamicsDataClient) para las tablas de relación cliente GIO↔POS en Dynamics |
com.hawkersco:slack-client:1.0.25-SNAPSHOT | Notificaciones agregadas de alerta a Slack |
com.hawkersco:pi-function-commons:1.0.25-SNAPSHOT | Utilidades comunes (sin uso directo detectado en este proyecto) |
spring-boot-starter-test (test) | JUnit 5 + Spring Test (sin suite de tests real; jenkins/scripts/test.sh es un placeholder vacío según CLAUDE.md) |
5. API / Endpoints
No aplica a este proyecto. Es un batch/runner sin capa REST (spring.main.web-application-type=none).
6. Integraciones externas
| Sistema | Protocolo | Dirección | Detalle |
|---|---|---|---|
GCS (bucket hawkers-gio) | API de GCS | Entrante/Saliente | Lee notificaciones pendientes de gio-api-notifications/update-client-gio-pending/, mueve a .../update-client-gio-processed/{año}/{mes}/{día}/ o a la carpeta de error correspondiente |
GIO (developer.deipe.com) | HTTP (@HttpExchange vía GioClient) | Entrante | readPaciente — lectura de datos de cliente por código |
POS / Dynamics 365 Commerce (dynamics.base.url) | HTTP (@HttpExchange vía PosClient) | Saliente | Creación/edición de cliente (createClient/editClient), autenticación OAuth client_credentials |
Dynamics 365 F&O — PI Customer Setup (dynamics.picustomersetup.url) | HTTP (@HttpExchange vía DynamicsDataClient) | Entrante/Saliente | Lectura/creación de registros de relación cliente GIO↔POS, autenticación OAuth client_credentials independiente |
| Slack | HTTP (SlackClient) | Saliente | Alerta agregada al final de cada ejecución si se superan los umbrales configurados |
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 credenciales reales de múltiples entornos (ver alerta de seguridad, especialmente grave en este proyecto).
| Clave | Descripción | Ejemplo (producción) |
|---|---|---|
dynamics.login.* | OAuth client_credentials para Dynamics 365 Commerce (retail) | ${dynamicsLoginClientId}, etc. |
dynamics.base.url | URL base del entorno de Dynamics Commerce | ${dynamicsBaseUrl} |
dynamics.picustomersetup.* | OAuth client_credentials independiente para Dynamics 365 F&O (PI Customer Setup) | ${dynamicsPiClientId}, etc. |
gio.auth.client.* | Credenciales de autenticación con GIO | ${gioAuthClientUrl}, etc. |
slack.client.url / .auth.token / .channel.id | Configuración del cliente Slack | ${slackClientUrl}, etc. |
pos.oun | Código de unidad operativa (OUN) del POS | 60STO303 (hardcoded también en el runner, ver sección 13) |
alerts.enabled | Interruptor global del subsistema de alertas | ${alertsEnabled:true} |
alerts.minBatchSize | Tamaño mínimo de lote para evaluar fallo sistémico | ${alertsMinBatchSize:5} |
alerts.failureRateThreshold | Umbral de tasa de fallo para alerta sistémica | ${alertsFailureRateThreshold:0.5} |
alerts.unknownErrorThreshold | Nº de errores no clasificados que dispara alerta | ${alertsUnknownErrorThreshold:1} |
⚠️ Alerta de seguridad (severidad alta)
El fichero src/main/resources/application.properties (perfil local) contiene credenciales OAuth reales de tres entornos distintos de Dynamics 365 (UAT y GOLD comentados, PRO activo), incluyendo client-id/client-secret de aplicaciones registradas en Azure AD con acceso a los tenants de Dynamics Commerce y Dynamics F&O de Hawkers, además de la contraseña real del usuario administrador de GIO (SUPERADMIN) y el token de bot de Slack (xoxb-...). Ninguno de estos valores se ha reproducido en este documento. Dado que se trata de credenciales de aplicaciones OAuth con acceso a sistemas ERP/POS de producción (no solo a una base de datos), se recomienda con prioridad alta:
- Rotar inmediatamente los
client-secretde las tres aplicaciones Azure AD (UAT, GOLD y PRO) y la contraseña del usuario GIOSUPERADMIN. - Revocar/regenerar el token de Slack expuesto.
- Eliminar por completo los bloques UAT/GOLD comentados del fichero (no solo dejarlos comentados: siguen siendo secretos reales versionados).
- Revisar el historial de control de versiones, ya que estas credenciales pueden seguir expuestas en commits anteriores, y auditar el registro de aplicaciones Azure AD por si se han usado indebidamente.
8. Persistencia
No aplica directamente: este proyecto no declara ningún DataSource propio ni entidad JPA local, pese a incluir la dependencia spring-orm y a que GioPosSyncServiceApplication registra un bean PersistenceManagedTypes que escanea com.hawkersco.logisticscommons.dao — un paquete que no pertenece a ninguna de las dependencias declaradas en el pom.xml de este proyecto (logistics-commons no está entre las dependencias). Es probable que sea un resto de copiar la clase principal desde otro proyecto de la familia (ver sección 13). El estado de sincronización real se gestiona a través de las tablas de relación en Dynamics (vía DynamicsDataClient), no mediante persistencia local.
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 * * * *", zona horaria Europe/Madrid, concurrencyPolicy: Forbid). Flujo de GioPosSyncServiceRunner:
- Lista los blobs pendientes bajo
gio-api-notifications/update-client-gio-pendingen el buckethawkers-gio. - Para cada notificación, si la tienda (
sucursalId) no está en la lista de ópticas españolas permitidas (ID_SPAIN_OPTICS:0-5, 7-8, 10-13, 16-21), se marca como procesada sin llamar a GIO. - Si es una óptica española, consulta el paciente en GIO (
gioClient.readPaciente); si el cliente no existe o está marcado como"[ CLIENTE DESACTIVADO ]", se marca la notificación como "no existe" y se archiva igualmente. - Para cada dato de cliente devuelto por GIO, consulta la tabla de relaciones en Dynamics (
getRecordRelationsOfDynamicsDb): si ya existe una relación, edita el cliente en POS (editClient); si no, lo crea (createClient) y registra tanto el cliente externo como la relación en Dynamics. - Cualquier error en la creación/edición se clasifica (
ErrorCategory.classify, por coincidencia de texto en el mensaje de excepción: DNI inválido, país no especificado, dirección/POS inválida, o "otros"/UNKNOWN) y el blob se mueve a la carpeta de error correspondiente; no se envía alerta individual por error. - Al finalizar el lote,
AlertingService.evaluateAndNotifyenvía como máximo un mensaje agregado a Slack por ejecución, solo si: la tasa de fallo global supera el umbral sobre un lote suficientemente grande (fallo sistémico), o aparece al menos un errorUNKNOWN(posible regresión). Los errores de calidad de dato conocidos (DNI, país, dirección) no generan ruido en Slack por sí solos.
10. Ejecución en local
Requisitos previos: JDK 25, Maven, credenciales de aplicación por defecto de Google con acceso al bucket hawkers-gio, y credenciales OAuth válidas de Dynamics/GIO/Slack.
# Compilar sin tests
mvn -B -DskipTests clean install
# Ejecutar con el perfil de desarrollo (application.properties)
mvn spring-boot:run
# Ejecutar con el perfil de producción
mvn spring-boot:run -Dspring.profiles.active=pro
No hay suite de tests real (jenkins/scripts/test.sh es un placeholder vacío, según el propio CLAUDE.md). Al ser un CommandLineRunner, no expone Actuator/health: la verificación se hace revisando el log de consola, el contenido de las carpetas processed/errors-* en GCS, o el mensaje agregado de Slack si se supera algún umbral.
11. Despliegue
- Imagen: construida con
jib-maven-plugin(baseeclipse-temurin:25-jre,containerizingMode=packaged), publicada eneurope-west3-docker.pkg.dev/pi-saldum/pi-repo/gio-pos-sync-service:<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→Deploy to GKE. ElCLAUDE.mddescribe además una etapa de test (vacía) y una etapa de "clean" no visibles como stages explícitos en elJenkinsfileactual (posiblemente implícitas en los scripts). - Las variables sensibles se inyectan en el pod mediante un
Secretde Kubernetes llamado igual que la app (gio-pos-sync-service), con un número elevado de claves (OAuth de dos entornos Dynamics distintos + GIO + Slack).
Job de Jenkins: https://jenkins-pi.hawkersco.net/job/gio-pos-sync-service/
12. Manejo de errores y logging
No hay una estrategia de excepciones centralizada de tipo REST (no aplica, es un runner). El bucle principal está envuelto en un try/catch a nivel de todo el run() que registra con Level.SEVERE y no interrumpe la evaluación de alertas posterior. Por cada cliente, los errores de creación/edición se capturan individualmente en handleClientOperationError, que clasifica el error, actualiza las métricas (SyncRunMetrics) y mueve el blob a la carpeta de error correspondiente — un fallo al mover/borrar el blob se registra con Level.WARNING sin relanzar. El subsistema de alertado (AlertingService) decide al final de la ejecución si se envía un único mensaje agregado a Slack, evitando el "ruido" de una alerta por cada error de calidad de dato conocido. Logging mediante java.util.logging.Logger estándar (consola).
13. Notas y consideraciones
PersistenceManagedTypesapunta a un paquete de una dependencia no declarada:GioPosSyncServiceApplicationregistra un bean que escaneacom.hawkersco.logisticscommons.dao, perologistics-commonsno aparece entre las dependencias delpom.xmlde este proyecto. Es casi con seguridad un resto de copiar la clase principal desde otro runner de la familia (p. ej.decathlon-create-db) sin eliminar el bean sobrante; no debería tener efecto funcional si el paquete no existe en el classpath, pero conviene limpiarlo.- Búsqueda de cliente en POS por criterio (
createPosSearchClientRequest) no se usa en el flujo actual:GioPosSyncServiceUtilsimplementa la lógica de búsqueda de cliente en POS descrita en elCLAUDE.mdcon prioridadVAT → Email → Phone → Cellphone → Name, pero el runner actual no la invoca: la decisión de crear vs. editar se toma exclusivamente consultando la tabla de relaciones en Dynamics (getRecordRelationsOfDynamicsDb), no mediante una búsqueda directa en POS. El método de búsqueda por criterio es código muerto en el flujo actual (o queda reservado para un fallback no implementado). - Fallo transitorio en la consulta de relaciones puede provocar creación de cliente duplicado:
getRecordRelationsOfDynamicsDbcaptura cualquierException(incluyendo errores de red o timeouts contra Dynamics) y devuelve"0"— el mismo valor que indica "cliente no existe todavía". Un fallo transitorio de la API de Dynamics durante esta consulta hará que el runner intente crear un nuevo cliente en POS en lugar de editar el existente, con riesgo de duplicados si el cliente ya estaba registrado. UNKNOWNreutiliza la carpeta de errores de dirección/POS: enErrorCategory, tantoINVALID_ADDRESScomoUNKNOWNapuntan al mismo directorio GCS (update-client-gio-errors-pos/), a diferencia de lo que sugiere elCLAUDE.md(que enumera 3 carpetas de error como si cada categoría tuviera la suya). En la práctica, los errores no clasificados (los que sí requieren revisión urgente según el propio diseño del sistema de alertas) se archivan mezclados con los errores conocidos de dirección/POS, dificultando su localización manual en GCS.- Nombre de paquete
Utilscon mayúscula inicial:com.hawkersco.giopossyncservice.Utilsrompe la convención de paquetes en minúsculas usada en el resto del ecosistema (utils,util). - Ver alerta de seguridad de severidad alta en la sección 7 sobre credenciales OAuth reales de múltiples entornos de Dynamics expuestas en
application.properties.