Una empresa utiliza dos sistemas de software que no se comunican entre sí:
- **Dolibarr** es el ERP (sistema de gestión empresarial). Aquí se gestiona el catálogo de productos, el stock real del almacén, los pedidos internos y la contabilidad.
- **PrestaShop** es la tienda online. Los clientes navegan aquí, ven productos, los añaden al carrito y realizan sus pedidos.
El problema es que cualquier cambio en uno de los sistemas hay que replicarlo manualmente en el otro. Si se añade un producto nuevo en Dolibarr, hay que crearlo también en PrestaShop. Si un cliente compra en la tienda, hay que registrar el pedido también en Dolibarr. Este proceso manual es lento, propenso a errores y genera inconsistencias: por ejemplo, un cliente puede comprar un producto que ya no tiene stock porque el ERP no había actualizado la tienda.
Todo esto ocurre de forma automática, periódica, y sin intervención humana. El personal puede monitorizar el estado desde una app de escritorio (Windows) o desde el móvil.
### Lo que el proyecto NO hace
- No modifica Dolibarr ni PrestaShop. Solo los consulta a través de sus APIs oficiales.
- No reemplaza ninguno de los dos sistemas. Los dos siguen siendo independientes.
- No es una integración en tiempo real (cada operación tiene su intervalo de ejecución).
**Capa 1 — Apps cliente:** Interfaces de usuario para el personal de la empresa. Permiten ver el estado de la sincronización, revisar mappings de productos y pedidos, y lanzar sincronizaciones manuales cuando sea necesario. Hay dos implementaciones: JavaFX para escritorio Windows y Android para móvil.
**Capa 2 — sync-service:** El núcleo del sistema. Es un servidor Spring Boot desplegado en la nube que:
**Capa 3 — Sistemas externos:** Dolibarr y PrestaShop ya existían. El proyecto no modifica ninguno de los dos; solo los consulta mediante sus APIs oficiales.
La autenticación oficial del Webservice de PS usa HTTP Basic Auth (la clave como nombre de usuario, sin contraseña). El problema es que el servidor nginx del hosting compartido **elimina el header `Authorization` antes de que llegue a PHP**, lo que hace que PS no reciba nunca la clave y rechace todas las peticiones con 401.
La solución es pasar la clave como **query parameter** en la URL:
PrestaShop Webservice usa **XML para escrituras** y puede devolver JSON para lecturas. Esto significa que crear o actualizar un producto requiere construir un documento XML. No es posible enviar JSON en las peticiones POST/PUT.
<languageid="1">Descripción del producto</language>
</description>
<associations>
<categories>
<category><id>2</id></category>
</categories>
</associations>
</product>
</prestashop>
```
Este XML se construye programáticamente en `PrestashopClient.java` usando `StringBuilder`. No se usa ninguna librería XML porque la estructura es simple y fija.
**¿Por qué PS usa XML?** El Webservice de PrestaShop es antiguo (diseñado alrededor de 2010) y XML era el estándar de la época para intercambio de datos. PS ha añadido soporte JSON para lecturas, pero las escrituras siguen requiriendo XML por compatibilidad hacia atrás.
**Recursos de la API utilizados:**
| Método | Endpoint | Para qué se usa |
|---|---|---|
| GET | `/api/products?filter[reference]=SKU` | Buscar producto por SKU |
| POST | `/api/products` | Crear producto (XML) |
| PUT | `/api/products/{id}` | Actualizar producto (XML) |
| GET | `/api/stock_availables?filter[id_product]=ID` | Obtener stock |
| PUT | `/api/stock_availables/{id}` | Actualizar stock (XML) |
| GET | `/api/orders?filter[date_add]=[desde,hasta]` | Obtener pedidos recientes |
| GET | `/api/customers/{id}` | Obtener datos de un cliente |
El mayor reto técnico de la integración es que Dolibarr y PrestaShop tienen IDs de producto completamente independientes y sin ninguna relación entre sí.
| Campo de referencia | `ref = "CAMISETA-001"` | `reference = "CAMISETA-001"` |
| Precio | 19.99 | 19.99 |
El ID 15 de Dolibarr y el ID 42 de PrestaShop representan el mismo producto físico. La única forma de saberlo es que ambos tienen la misma referencia: `CAMISETA-001`. Este campo es el **SKU**.
El sync-service mantiene una tabla `product_mapping` que actúa como diccionario de traducción:
- **WebClient / Reactor**: programación reactiva non-blocking. Añade complejidad sin beneficio cuando no hay miles de peticiones concurrentes. Este sistema hace llamadas síncronas secuenciales.
- **Docker Compose**: innecesario. Railway gestiona el contenedor. En local, H2 en memoria elimina la necesidad de un contenedor de base de datos.
- **Kafka / RabbitMQ**: para comunicación asíncrona entre microservicios. Este proyecto es un monolito bien estructurado en capas; no hay microservicios.
- **RestTemplate**: deprecated en Spring 6. Reemplazado por RestClient.
- **CQRS / Event Sourcing / DDD táctico**: patrones válidos en sistemas grandes con equipos numerosos. Aquí añadirían complejidad sin valor real.
Las tres tablas son **independientes** entre sí (sin claves foráneas entre ellas). Esto es intencional: si se borra un mapping de producto, los logs de sincronización de ese producto siguen existiendo como historial.
`Instant` representa un punto exacto en el tiempo en UTC, sin zona horaria. Es el tipo correcto para timestamps del sistema. Al guardarlo en MySQL, Hibernate lo convierte a `DATETIME`. Al leerlo, reconstruye el `Instant`. No hay ambigüedades de zona horaria.
`@Data` genera `equals()` y `hashCode()` basados en todos los campos del objeto. En entidades JPA esto es problemático: si hay relaciones bidireccionales, `equals()` puede llamar recursivamente al `equals()` de la entidad relacionada, generando un bucle infinito. Se usan `@Getter`, `@Setter`, `@Builder` de forma explícita para evitar esto.
Añadir el header una sola vez en la configuración evita repetirlo en cada llamada. Si la clave cambia, se cambia en un solo lugar (la variable de entorno `DOLIBARR_API_KEY`).
En cada sincronización, antes de crear un producto en PS, se busca si ya existe uno con ese SKU. Si existe, se actualiza. Si no, se crea. Esto hace el proceso idempotente: ejecutarlo dos veces produce el mismo resultado que ejecutarlo una vez.
**¿Por qué no usar el mapping local para decidir si crear o actualizar?**
Si alguien borra un producto desde el admin de PS, el mapping local seguirá teniendo el antiguo ID de PS. El sync-service intentaría hacer PUT a ese ID y PS devolvería 200, pero el producto ya no existiría en la tienda (ver Bug 6 en sección 8). Consultar PS directamente por SKU es más fiable.
PrestaShop gestiona el stock en una tabla separada `ps_stock_available`, no en `ps_product`. Por eso el stock tiene su propio flujo: no basta con actualizar el producto.
Dolibarr llama "tercero" (thirdparty) a cualquier persona u organización externa: cliente, proveedor, etc. Cuando llega un pedido de PS, hay que crear la comanda a nombre de un tercero que exista en Dolibarr. El flujo es:
1. Buscar en Dolibarr si existe un tercero con el email del cliente de PS.
2. Si existe → usar su ID.
3. Si no existe → crear un nuevo tercero con los datos del cliente de PS (nombre, email, dirección).
Un pedido completado en PrestaShop significa que el cliente ha pagado (o se ha comprometido a pagar). En la contabilidad de Dolibarr, esto tiene dos registros:
- **Comanda** (`commande`): el compromiso de compra, el pedido.
- **Factura** (`facture`): el documento contable que certifica la transacción.
El bloque `finally` garantiza que **siempre** se escribe el resultado en el `SyncLog`, aunque el sistema explote con una `OutOfMemoryError`. Sin esto, si algo falla inesperadamente, no quedaría ningún registro.
**¿Por qué no `@Transactional` en los servicios batch?**
Si se anotaran con `@Transactional`, un fallo en el ítem 50 desharía todo el trabajo de los ítems 1 a 49 (rollback). En un sistema de sincronización, procesar correctamente 49 de 50 ítems es mucho mejor que no procesar ninguno. Cada `save()` es un commit independiente.
El `initialDelayString = "PT30S"` hace que los tres flujos esperen 30 segundos antes de su primera ejecución, dando tiempo al servidor a arrancar completamente antes de hacer llamadas a APIs externas.
**`@ConditionalOnProperty`:** El scheduler solo existe si `sync.scheduling.enabled=true`. En el perfil `dev` esta propiedad es `false`, lo que significa que en local no se ejecutan syncs automáticas. Hay que lanzarlas manualmente desde Swagger o la API.
**Intervalos en notación ISO-8601:** `PT15M` = 15 minutos, `PT5M` = 5 minutos. Los dos puntos son los valores por defecto en el código. Los intervalos reales en producción se configuran como variables de entorno en Railway.
SpringDoc genera automáticamente una interfaz web de documentación y pruebas en `/swagger-ui.html`. No hay que mantener documentación manual; se genera desde las anotaciones del código (`@RestController`, `@Operation`, `@Tag`, etc.).
Todos los endpoints requieren autenticación excepto `/actuator/health` (para que Railway pueda verificar que el servicio está vivo) y `/h2-console/**` (solo disponible en dev con H2).
Cada petición incluye el header `Authorization: Basic BASE64(usuario:contraseña)`. Es el esquema más simple y suficiente para una API interna usada por apps propias.
CSRF (Cross-Site Request Forgery) es un ataque que roba la sesión de un usuario mediante cookies. Como esta API no usa sesiones (es `STATELESS`, sin cookies), no existe superficie de ataque CSRF. Desactivarlo es correcto y necesario; de lo contrario, Spring Security rechazaría todas las peticiones POST.
**Sesiones:** `SessionCreationPolicy.STATELESS` indica que el servidor no crea ni almacena sesiones HTTP. Cada petición es completamente independiente y debe incluir sus credenciales. Esto es el comportamiento correcto para una API REST.
Esto garantiza que la base de datos siempre tenga el esquema correcto, independientemente de si es la primera vez que arranca o el sistema lleva meses en producción.
Con `MODE=MySQL`, H2 acepta la misma sintaxis SQL que MySQL (tipos de datos, funciones, etc.). El mismo `V1__init.sql` funciona en desarrollo (H2) y en producción (MySQL) sin ninguna modificación.
Si `apiKey` está vacío o no está configurado, la aplicación **falla al arrancar** con un mensaje claro. Esto evita el caso silencioso de que el servicio arranque pero falle en todas las peticiones a las APIs externas.
Formulario de conexión al sync-service con campos para URL del servidor, usuario y contraseña. Las credenciales se guardan automáticamente al hacer login exitoso mediante `java.util.prefs.Preferences` (registro de Windows) y se pre-rellenan en los siguientes arranques. Un checkbox permite cambiar entre servidor Railway (producción) y `localhost:8080` (desarrollo local).
- Tres tarjetas de sincronización (Productos, Stock, Pedidos), cada una con el resultado de la última ejecución: cuándo fue, cuántos ítems procesó y si hubo errores.
- Botón "Sincronizar todo" que lanza los tres flujos en secuencia mostrando el progreso paso a paso.
- Cada tarjeta tiene su propio botón "Sincronizar" para lanzar solo ese flujo.
- Actualización automática cada 30 segundos via `Timeline` de JavaFX.
- Tabla con el historial de todas las ejecuciones de sync.
- Filter chips por tipo: Todos / Productos / Stock / Pedidos.
- Icono de estado en cada fila: ⏱ (en proceso), ✓ (éxito), ✗ (con errores).
- Click en una fila abre un **diálogo modal** con el detalle completo: fechas, duración calculada, ítems procesados/fallidos, y el texto de error si lo hubo.
└── AlertUtil ← Diálogos de información/error/advertencia
```
**Threading:** JavaFX tiene un único hilo de UI (el Application Thread). Hacer una petición HTTP en ese hilo congela la interfaz hasta que termina. Todas las llamadas al servidor se hacen en hilos de background:
```java
Thread t = new Thread(() -> {
// Esto corre en background (hilo separado del UI)
**Persistencia de ajustes:** `java.util.prefs.Preferences` guarda datos en el registro de Windows (en `HKCU\Software\JavaSoft\Prefs`). Es la API estándar de Java para preferencias de usuario, sin dependencias adicionales.
La app Android ofrece exactamente las mismas funcionalidades que la app JavaFX, adaptadas a pantalla táctil:
**Login:** Formulario de conexión con URL, usuario y contraseña. Los datos se guardan con `DataStore` (equivalente Android a `Preferences` de Java) para no tener que introducirlos cada vez.
**Dashboard:** Tarjetas de sincronización con información del último log por tipo. Botón "Sincronizar todo". Actualización pull-to-refresh y automática periódica.
**Productos:** Lista con mappings, búsqueda por SKU, filtros por estado (chips). Navegación a detalle al pulsar un ítem.
**Pedidos:** Lista con mappings importados, estado, fechas.
**Historial:** Lista de logs de sincronización con indicadores visuales de estado. Tap en un ítem muestra el detalle completo.
**Ajustes:** Formulario para cambiar la configuración del servidor.
#### Arquitectura Android
La app sigue la arquitectura recomendada por Google para Android moderno:
- **ViewModel + LiveData / StateFlow**: gestión del estado de la UI separada de la lógica de negocio.
- **Repository pattern**: capa de acceso a datos que abstrae la fuente (la API REST del sync-service).
- **Retrofit**: cliente HTTP para Android. Hace las llamadas a la API del sync-service.
- **Coroutines**: para llamadas asíncronas sin bloquear el hilo principal de UI.
- **Navigation Component**: navegación entre pantallas.
El enunciado del TFG establece que Dolibarr y PrestaShop están en un **hosting compartido** bajo `httpdocs`. Este tipo de hosting solo ejecuta PHP bajo petición HTTP; **no permite mantener procesos Java activos en segundo plano**. Por tanto, el sync-service no puede ejecutarse en el mismo servidor que los sistemas que integra.
Railway es una plataforma PaaS (Platform as a Service) que permite desplegar aplicaciones sin gestionar servidores. Se eligió por:
- **Plan gratuito** suficiente para el volumen de este proyecto.
- **Integración con GitHub**: cada push a `main` dispara un redeploy automático.
- **Plugin MySQL** gestionado, con red privada interna.
- **Variables de entorno** seguras para credenciales.
- **Sin gestión de infraestructura**: sin Dockerfile, sin configuración de nginx, sin certificados SSL manuales.
Todo este proceso tarda aproximadamente 2-3 minutos. Si el build falla (error de compilación, test fallido), el deploy anterior sigue activo; no hay downtime por builds rotos.
`mysql.railway.internal:3306` es la dirección de red privada interna de Railway. El sync-service y la base de datos están en la misma red virtual, por lo que la comunicación es directa y más rápida que pasar por la red pública. Además, la base de datos no está expuesta al exterior.
**Lección aprendida:** Railway tiene "Shared Variables" que en teoría se comparten entre servicios del mismo proyecto. En la práctica, estas variables **no se inyectan automáticamente** como variables de entorno en los servicios. Todas las variables hay que configurarlas directamente en el servicio `ProyectoIntermodular`.
---
## 7. Dificultades encontradas
Esta sección describe los problemas más relevantes que surgieron durante el desarrollo, más allá de los bugs técnicos puntuales de PrestaShop (que tienen su propia sección).
### 7.1 Autenticación de PrestaShop en hosting con nginx
**Descripción:** El Webservice de PrestaShop usa HTTP Basic Auth. Al implementar el cliente, las peticiones devolvían siempre 401 Unauthorized, aunque las credenciales eran correctas.
**Diagnóstico:** El servidor de hosting usa nginx como proxy inverso delante de Apache/PHP. nginx, por seguridad, **elimina el header `Authorization` antes de reenviar la petición a PHP**. PrestaShop nunca recibe la clave de autenticación.
**Solución:** La documentación de PrestaShop menciona un mecanismo alternativo: pasar la clave como query parameter `ws_key`. Este parámetro sí sobrevive al proxy nginx porque va en la URL, no en los headers. Se modificó `PrestashopClient` para añadir `ws_key` a cada URL.
**Impacto:** Todos los métodos de `PrestashopClient` debían incluir `ws_key` en la URI. Se creó un método auxiliar que construye las URIs con el parámetro ya incluido.
### 7.2 PrestaShop requiere XML para escrituras
**Descripción:** Al intentar crear un producto enviando JSON en el cuerpo de la petición POST, PrestaShop devolvía error 400.
**Diagnóstico:** El Webservice de PrestaShop es de una generación anterior y solo acepta XML para operaciones de escritura (POST y PUT). JSON está disponible solo para las respuestas de lectura (GET) mediante `output_format=JSON`.
**Solución:** Implementar métodos `toProductXml()`, `toStockAvailableXml()` en `PrestashopClient` que construyen el documento XML manualmente con `StringBuilder`. Se eligió construcción manual sobre una librería XML porque la estructura es fija y simple, y añadir una dependencia XML solo para este uso sería sobredimensionado.
**Aprendizaje:** Antes de diseñar la capa de integración, hay que verificar el formato de datos que acepta cada API, no asumirlo.
### 7.3 Descubrimiento de bugs no documentados en PrestaShop 8.2.5
**Descripción:** PrestaShop 8.2.5 introdujo cambios en el Webservice y comportamientos no documentados que no existían en versiones anteriores. Ninguno de estos estaba en la documentación oficial ni en la mayoría de recursos online, porque la versión 8 es relativamente reciente.
Estos bugs se descubrieron durante las pruebas en entorno real y requirieron depuración directa en la base de datos de PrestaShop con consultas SQL para entender qué estaba pasando. Están documentados en detalle en la sección 8.
**Impacto en el desarrollo:** Lo que en teoría debería haber sido una verificación de 2-3 horas se convirtió en una sesión de depuración de varios días. Cada bug desvelaba el siguiente.
### 7.4 Despliegue en Railway: versión de Maven inexistente
**Descripción:** Al subir los cambios finales del proyecto (con los archivos de la app JavaFX), el deploy en Railway falló con el error:
```
mise ERROR Failed to install aqua:apache/maven@3.9.16:
HTTP status client error (404 Not Found)
```
**Causa:** Railway usa Railpack para detectar automáticamente las versiones de herramientas. Railpack intentó instalar `maven@3.9.16`, una versión que no existe en el repositorio oficial de Apache Maven.
**Solución:** Crear el fichero `sync-service/.mise.toml` que fija las versiones de Java y Maven de forma explícita:
**Aprendizaje:** La detección automática de versiones no siempre es fiable. Es buena práctica fijar explícitamente las versiones de las herramientas de build para garantizar reproducibilidad.
**Causa:** Las Shared Variables de Railway son variables globales del proyecto visibles para todos los servicios, pero **no se inyectan automáticamente** como variables de entorno. Solo sirven para referenciarlas con `${{NombreVariable}}` dentro de las variables propias del servicio.
**Impacto:** El servicio arrancaba pero fallaba porque las propiedades `integration.dolibarr.api-key` y `integration.prestashop.api-key` llegaban vacías, y la validación `@NotBlank` hacía que Spring Boot abortara el arranque.
### 7.6 Fat JAR incompatible con JavaFX
**Descripción:** Se intentó configurar `maven-shade-plugin` para generar un JAR ejecutable con todas las dependencias incluidas (fat JAR), de forma que la app JavaFX se pudiera distribuir como un único fichero.
**Causa:** JavaFX no funciona con fat JARs. Las librerías nativas de JavaFX (ficheros `.dll` en Windows, `.so` en Linux, `.dylib` en macOS) no se pueden incluir dentro de un JAR. Al intentar ejecutar el fat JAR, la JVM no encontraba las librerías nativas y lanzaba `UnsatisfiedLinkError`.
**Solución:** Eliminar `maven-shade-plugin` del `pom.xml` y usar el plugin oficial `javafx-maven-plugin` con el objetivo `javafx:run`. Este plugin gestiona correctamente las librerías nativas de JavaFX.
**Limitación resultante:** La app JavaFX requiere que Maven esté instalado para ejecutarse. No existe un JAR distribuible de un solo fichero. Para una distribución real, habría que usar `jpackage` (herramienta incluida en el JDK desde Java 14) que genera un instalador nativo para cada plataforma.
Durante la verificación end-to-end del sistema con datos reales en Dolibarr y PrestaShop 8.2.5, se encontraron y corrigieron seis bugs. Todos estaban relacionados con comportamientos no documentados o cambios introducidos en PS 8 que no existían en versiones anteriores.
Spring 6 RestClient intenta deserializar `false` como `List<PrestashopProductDto>` y lanza `HttpMessageConversionException`. La sincronización fallaba completamente para cualquier producto que no existiera aún en PS.
**Fix:** Capturar `HttpMessageConversionException` y `RestClientException` en `getProductByReference()` y devolver `Optional.empty()`.
En algunos casos, cuando el filtro no coincide con ningún producto, PS devuelve HTTP 404 en lugar de 200 con lista vacía. Este comportamiento no está documentado.
**Síntoma:** Después de sincronizar productos y stock desde Dolibarr, la tienda mostraba todos los productos como "OUT OF STOCK" aunque el campo `quantity` en la base de datos tuviera valores correctos (ej. 50 unidades).
El **storefront** solo lee los registros con `id_shop = 1` para mostrar la disponibilidad. Cuando el Webservice crea un producto, PS crea automáticamente el `stock_available` con `id_shop = 0`. Los productos creados manualmente desde el admin tenían `id_shop = 1`.
**Síntoma:** Los productos sincronizados aparecían correctamente en la tienda online para los clientes, pero el panel de administración (Catálogo → Productos) mostraba 0 productos.
El **admin de PS 8** filtra automáticamente por `state = 1` en su listado. El **storefront** no filtra por `state`. Un producto con `state = 0` es visible para los clientes pero invisible en el panel de administración.
Al crear productos via Webservice sin especificar `state`, PS los crea con `state = 0` (valor por defecto de la columna SQL). Este campo `state` no existía en PrestaShop 7 y no está documentado en el Webservice.
**Síntoma:** Al revisar la base de datos de PrestaShop, había 18 instancias del mismo producto de prueba, con IDs del 23 al 40.
**Causa:** Consecuencia en cadena del Bug 4.
El método `getProductByReference()` filtra los productos de PS. Los productos con `state = 0` (Bug 4) no aparecían en los resultados del filtro aunque existieran en la base de datos. El sync-service interpretaba "no existe" y lo creaba de nuevo en cada ejecución. 18 ejecuciones = 18 duplicados.
**Fix:** El fix del Bug 4 (`state = 1`) resuelve la causa raíz. Una vez que los productos tienen `state = 1`, el filtro los devuelve correctamente y el sync detecta que ya existen → hace UPDATE en lugar de CREATE.
**Causa:** PrestaShop usa soft-delete. Al eliminar un producto desde el admin, no se borra el registro de `ps_product`; simplemente se pone `active = 0`. El sync-service tenía en `product_mapping` el antiguo `prestashop_id` y llamaba a `PUT /api/products/{id}`, que devolvía 200 pero el producto con `active = 0` seguía sin aparecer en la tienda.
El endpoint raíz del Webservice (`/api/`) devuelve HTTP 500 en PS 8.2.5, aunque en versiones anteriores devolvía el listado de recursos disponibles. No es un bug crítico, pero hay que usar siempre recursos específicos: `/api/products`, `/api/orders`, etc.
---
## 9. Decisiones técnicas clave
Esta sección explica las decisiones más importantes en formato pregunta-respuesta, tal como podría plantearlas un tribunal en la defensa oral.
---
**¿Por qué `fixedDelay` y no `fixedRate` en los jobs programados?**
`fixedRate` dispara el job cada N segundos desde que empezó el anterior, independientemente de si ha terminado. Si una sync tarda más que el intervalo, la siguiente ya debería haber empezado antes de que termine la anterior: se acumulan ejecuciones en cola.
`fixedDelay` espera N segundos desde que terminó el anterior. Si una sync tarda 20 minutos con `fixedDelay=15m`, la siguiente empezará a los 35 minutos del inicio de la primera. Nunca puede haber solapamiento.
Aunque hay tres métodos `@Scheduled`, el pool de un único hilo garantiza que los tres flujos sean estrictamente secuenciales: nunca pueden ejecutarse dos a la vez. Esto es importante porque los tres flujos acceden a las mismas tablas de la base de datos local y a las mismas APIs externas. Con dos hilos, dos flujos podrían modificar `product_mapping` simultáneamente y producir inconsistencias.
`@Transactional` abre una transacción al inicio del método y hace rollback si lanza una excepción. En un batch de 100 productos, si falla el número 50, los 49 anteriores ya guardados se desharían. El comportamiento correcto para sincronización es procesamiento parcial: si falla 1 de 100, los 99 que funcionaron se persisten igualmente. Cada `save()` es un commit independiente.
`finally` se ejecuta **siempre**, tanto si el código termina normalmente como si lanza cualquier tipo de excepción (incluyendo `Error` de la JVM). Capturar con `catch (Exception e)` no captura `Error`. Con `finally`, el `SyncLog` siempre se escribe, garantizando que nunca haya una ejecución sin registro.
Permite usar `{noop}texto_plano` en desarrollo (sin hash, más cómodo para depurar) y `{bcrypt}hash_seguro` en producción, en el mismo campo de configuración. El código de `SecurityConfig.java` no cambia entre entornos. Solo cambia el valor de la variable de entorno `API_SECURITY_PASSWORD`.
`create-drop` borra la base de datos en cada reinicio del servidor: inaceptable en producción. `update` intenta inferir los cambios del esquema a partir de las entidades JPA, pero tiene limitaciones y puede producir resultados inesperados (no elimina columnas, no gestiona índices correctamente). Flyway ejecuta SQL explícito y versionado: sabes exactamente qué SQL se va a ejecutar y en qué orden.
H2 en memoria arranca en milisegundos y no requiere instalación. `MODE=MySQL` hace que H2 acepte la sintaxis SQL de MySQL, por lo que las migraciones Flyway funcionan sin modificaciones en ambos entornos. Si un desarrollador nuevo clona el repositorio, puede ejecutar el proyecto directamente sin configurar una base de datos.
RestTemplate está marcado como deprecated en Spring 6 (la versión que usa este proyecto). WebClient es para programación reactiva (non-blocking), que añade complejidad innecesaria en un sistema síncrono. RestClient es la API moderna síncrona de Spring 6, con una API fluida más legible.
El mapping local puede quedar obsoleto si alguien modifica PS manualmente (borra un producto, cambia su referencia). Si el sync-service confía ciegamente en el mapping local, intentará actualizar un ID de PS que ya no existe o está borrado. Consultar PS directamente por SKU en cada sync añade una petición HTTP pero garantiza que la operación es siempre correcta, independientemente del estado del mapping local.
**¿Por qué SKU como identificador común y no los IDs internos?**
Los IDs internos de Dolibarr y PrestaShop son autoincrementales, independientes y sin ninguna relación entre sí. El mismo producto puede ser ID 15 en uno y ID 42 en el otro. El SKU (referencia) es el único campo que el negocio controla y que tiene el mismo valor en ambos sistemas. Es el "pasaporte" del producto entre sistemas.
---
**¿Por qué arquitectura en tres capas y no conectar las apps directamente con Dolibarr y PrestaShop?**
Si las apps conectaran directamente, las claves de API de Dolibarr y PrestaShop estarían en los dispositivos del usuario (inseguro). No habría base de datos para el historial. El scheduling no sería posible (las apps pueden estar cerradas). Y la lógica de sincronización habría que implementarla en JavaFX, Android y cualquier cliente futuro. Con el servicio intermediario, toda esa complejidad vive en un solo lugar.
---
## 10. Limitaciones conocidas y trabajo futuro
### Limitaciones actuales
**Distribución de la app JavaFX:**
La app JavaFX se ejecuta con `mvn javafx:run`, lo que requiere que el usuario tenga Java 21 y Maven instalados. No existe un instalador `.exe` ni un JAR ejecutable autónomo. Para generar un instalador real habría que usar `jpackage` (incluido en el JDK), que genera instaladores nativos (`.exe` para Windows, `.dmg` para macOS, `.deb/.rpm` para Linux).
**Sin sincronización en tiempo real:**
Los cambios en Dolibarr tardan hasta 15 minutos en aparecer en PrestaShop (el intervalo del job de productos). Para pedidos, el delay máximo es de 5 minutos. Para un negocio con alta rotación de stock esto podría ser insuficiente, aunque los intervalos son configurables y reducibles.
**Un solo usuario de API:**
La configuración de seguridad actual tiene un único usuario (`admin`). Para un entorno empresarial real, habría que implementar gestión de usuarios con diferentes niveles de acceso.
**Sin gestión de imágenes de productos:**
La sincronización de productos transfiere nombre, descripción, precio y stock, pero no las imágenes. Las imágenes de PrestaShop hay que subirlas manualmente o implementar un flujo adicional.
**Sin sincronización de categorías:**
Los productos se asignan a una categoría por defecto en PrestaShop (`defaultCategoryId` en la configuración). No hay sincronización de la jerarquía de categorías de Dolibarr a PrestaShop.
**Limitaciones de la API de Dolibarr:**
El cliente `DolibarrClient` obtiene todos los productos en cada sync (sin filtro incremental por fecha), porque la API de Dolibarr no ofrece un endpoint de "modificados desde fecha X". Esto significa que con un catálogo muy grande (miles de productos), cada sync de productos puede tardar bastante.
### Trabajo futuro
- **`jpackage`**: generar instaladores nativos para Windows y macOS de la app JavaFX.
- **Filtro incremental en Dolibarr**: implementar detección de cambios por timestamp para evitar procesar todos los productos en cada sync.
- **Sincronización de imágenes**: transferir las imágenes de los productos de Dolibarr a PrestaShop.
- **Webhooks**: en lugar de polling periódico, PrestaShop puede enviar notificaciones cuando ocurre un pedido. Esto eliminaría el delay y la carga de las consultas periódicas.
- **Gestión de usuarios**: múltiples usuarios con roles en la API.
- **Panel web ligero**: interfaz web adicional para acceso desde cualquier dispositivo sin instalación.
- **Tests de integración**: actualmente los tests son unitarios con mocks. Tests de integración con WireMock simularían las APIs de Dolibarr y PrestaShop para verificar el comportamiento end-to-end.
---
## 11. Conclusión
Este proyecto ha desarrollado un sistema completo de integración entre dos plataformas comerciales que no se comunican nativamente: el ERP Dolibarr y la tienda PrestaShop. El sistema cumple los dos requisitos funcionales del enunciado: sincronización de productos y stock del ERP hacia la tienda, e importación de pedidos de la tienda hacia el ERP.
El desarrollo se estructuró en seis fases progresivas, comenzando por la infraestructura base y terminando con las apps cliente. Esta progresión permitió validar cada capa antes de construir la siguiente: los clientes HTTP se probaron antes de implementar los servicios de sincronización; la API REST se verificó en Swagger antes de desarrollar las apps cliente.
La parte más compleja del proyecto no fue la arquitectura ni el código, sino la **integración con PrestaShop 8.2.5**. Los seis bugs no documentados encontrados durante las pruebas en entorno real evidencian que las pruebas en entorno controlado (con mocks o datos ficticios) no pueden sustituir las pruebas en el sistema real. Cada bug requirió análisis directo de la base de datos de PrestaShop para entender la causa raíz, lo que fue una experiencia práctica valiosa en diagnóstico y depuración de sistemas.
Desde el punto de vista arquitectónico, el diseño en tres capas con un servicio intermediario centralizado ha demostrado ser la elección correcta: permite mantener la lógica de integración en un único lugar, protege las credenciales de las APIs externas, posibilita el scheduling automático y es extensible (añadir una tercera app cliente no requiere cambios en el backend).
Las decisiones técnicas adoptadas (RestClient, Flyway, `fixedDelay`, `try/finally`, `DelegatingPasswordEncoder`, H2 en modo MySQL) no son arbitrarias: cada una responde a un requisito concreto o a una limitación del entorno. Poder argumentar estas decisiones es parte del objetivo del TFG.
El sistema está desplegado y en funcionamiento en Railway, conectado con los sistemas reales de Dolibarr y PrestaShop en el hosting del proyecto.
| **ERP** | Enterprise Resource Planning. Software de gestión empresarial que integra procesos como contabilidad, stock, ventas y RRHH. En este proyecto: Dolibarr. |
| **Webservice** | Interfaz de programación oficial de PrestaShop para integración con sistemas externos. Usa XML para escrituras y puede devolver JSON en lecturas. |
| **SKU** | Stock Keeping Unit. Código único que identifica un producto. En Dolibarr: `ref`. En PrestaShop: `reference`. El campo es el mismo en ambos sistemas. |
| **Mapping** | Registro que relaciona el mismo objeto en dos sistemas distintos. Ejemplo: el producto `CAMISETA-001` es ID 15 en Dolibarr e ID 42 en PrestaShop. |
| **Soft-delete** | Técnica de borrado lógico: en lugar de eliminar el registro, se marca con un flag (`active=0`). PrestaShop usa soft-delete para productos. |
| **Idempotente** | Una operación que produce el mismo resultado independientemente de cuántas veces se ejecute. Las syncs son idempotentes: ejecutar PRODUCT_PUSH dos veces no crea duplicados. |
| **Flyway** | Herramienta de migración de esquemas de base de datos. Ejecuta ficheros SQL versionados (`V1__`, `V2__`...) en orden y registra cuáles ya se aplicaron. |
| **RestClient** | Cliente HTTP síncrono de Spring 6. Reemplaza al deprecated `RestTemplate`. |
| **fixedDelay** | Modo de scheduling: el temporizador empieza a contar cuando termina la ejecución anterior. Sin solapamiento posible. |
| **fixedRate** | Modo de scheduling: el temporizador empieza a contar cuando comienza la ejecución anterior. Puede causar solapamientos si el job tarda más que el intervalo. |
| **`@ConditionalOnProperty`** | Anotación Spring que activa un bean solo si una propiedad tiene un valor concreto. Permite desactivar el scheduler en dev. |
| **ISO-8601** | Estándar para fechas y duraciones. `PT15M` = 15 minutos, `PT5M` = 5 minutos, `2025-05-10T14:32:11Z` = fecha y hora UTC. |
| **`DelegatingPasswordEncoder`** | Codificador de Spring que selecciona el algoritmo de hash según el prefijo `{id}` de la contraseña. Permite `{noop}` en dev y `{bcrypt}` en prod. |
| **Upsert** | Operación que crea el registro si no existe y lo actualiza si existe. Combinación de INSERT y UPDATE. |
| **Fat JAR** | JAR que incluye todas las dependencias dentro. No funciona con JavaFX por sus librerías nativas. |
| **Proxy inverso** | Servidor (nginx en este caso) que recibe peticiones HTTP y las reenvía a otro servidor (Apache/PHP). El proxy puede modificar los headers en tránsito. |
| **`@NotBlank`** | Anotación de validación de Jakarta que rechaza strings null, vacíos o con solo espacios. Provoca fallo al arrancar si la propiedad no está configurada. |
| **Tercero (Thirdparty)** | Término de Dolibarr para cualquier entidad externa: cliente, proveedor, etc. Cada pedido importado desde PS se asocia a un tercero en Dolibarr. |