Skip to main content

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

CampoValor
artifactIdgio-pos-sync-service
groupIdcom.hawkersco
version1.0.25
Java25 (maven.compiler.release=25)
Spring Boot4.0.6
Tipo de artefactojar (ejecutable, Spring Boot batch/CLI sin servidor web)
MódulosNo 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.
  • .UtilsGioPosSyncServiceUtils (nótese la U mayúscula del paquete, inconsistente con la convención habitual en minúscula del resto del ecosistema).
  • .alertingAlertingService, 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

DependenciaPropósito
spring-boot-starterNúcleo de Spring Boot (sin web, es un runner CLI)
spring-ormSoporte JPA (aunque este proyecto no declara ningún datasource propio, ver sección 13)
com.hawkersco:gio-client:1.0.25-SNAPSHOTCliente @HttpExchange (GioClient) para la API de GIO (lectura de pacientes/clientes)
com.hawkersco.posclient:pos-client:1.0.25-SNAPSHOTCliente @HttpExchange (PosClient) para crear/editar clientes en el POS (Dynamics Commerce)
com.hawkersco:dynamics-client:1.0.25-SNAPSHOTCliente @HttpExchange (DynamicsDataClient) para las tablas de relación cliente GIO↔POS en Dynamics
com.hawkersco:slack-client:1.0.25-SNAPSHOTNotificaciones agregadas de alerta a Slack
com.hawkersco:pi-function-commons:1.0.25-SNAPSHOTUtilidades 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

SistemaProtocoloDirecciónDetalle
GCS (bucket hawkers-gio)API de GCSEntrante/SalienteLee 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)EntrantereadPaciente — lectura de datos de cliente por código
POS / Dynamics 365 Commerce (dynamics.base.url)HTTP (@HttpExchange vía PosClient)SalienteCreació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/SalienteLectura/creación de registros de relación cliente GIO↔POS, autenticación OAuth client_credentials independiente
SlackHTTP (SlackClient)SalienteAlerta 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).

ClaveDescripciónEjemplo (producción)
dynamics.login.*OAuth client_credentials para Dynamics 365 Commerce (retail)${dynamicsLoginClientId}, etc.
dynamics.base.urlURL 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.idConfiguración del cliente Slack${slackClientUrl}, etc.
pos.ounCódigo de unidad operativa (OUN) del POS60STO303 (hardcoded también en el runner, ver sección 13)
alerts.enabledInterruptor global del subsistema de alertas${alertsEnabled:true}
alerts.minBatchSizeTamaño mínimo de lote para evaluar fallo sistémico${alertsMinBatchSize:5}
alerts.failureRateThresholdUmbral de tasa de fallo para alerta sistémica${alertsFailureRateThreshold:0.5}
alerts.unknownErrorThresholdNº 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:

  1. Rotar inmediatamente los client-secret de las tres aplicaciones Azure AD (UAT, GOLD y PRO) y la contraseña del usuario GIO SUPERADMIN.
  2. Revocar/regenerar el token de Slack expuesto.
  3. Eliminar por completo los bloques UAT/GOLD comentados del fichero (no solo dejarlos comentados: siguen siendo secretos reales versionados).
  4. 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:

  1. Lista los blobs pendientes bajo gio-api-notifications/update-client-gio-pending en el bucket hawkers-gio.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Al finalizar el lote, AlertingService.evaluateAndNotify enví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 error UNKNOWN (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 (base eclipse-temurin:25-jre, containerizingMode=packaged), publicada en europe-west3-docker.pkg.dev/pi-saldum/pi-repo/gio-pos-sync-service:<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 & PushDeploy to GKE. El CLAUDE.md describe además una etapa de test (vacía) y una etapa de "clean" no visibles como stages explícitos en el Jenkinsfile actual (posiblemente implícitas en los scripts).
  • Las variables sensibles se inyectan en el pod mediante un Secret de 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

  • PersistenceManagedTypes apunta a un paquete de una dependencia no declarada: GioPosSyncServiceApplication registra un bean que escanea com.hawkersco.logisticscommons.dao, pero logistics-commons no aparece entre las dependencias del pom.xml de 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: GioPosSyncServiceUtils implementa la lógica de búsqueda de cliente en POS descrita en el CLAUDE.md con prioridad VAT → 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: getRecordRelationsOfDynamicsDb captura cualquier Exception (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.
  • UNKNOWN reutiliza la carpeta de errores de dirección/POS: en ErrorCategory, tanto INVALID_ADDRESS como UNKNOWN apuntan al mismo directorio GCS (update-client-gio-errors-pos/), a diferencia de lo que sugiere el CLAUDE.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 Utils con mayúscula inicial: com.hawkersco.giopossyncservice.Utils rompe 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.