79 KiB
Documentación técnica — TFG Integración Dolibarr · PrestaShop
Ciclo: Desarrollo de Aplicaciones Multiplataforma (DAM) — Grado Superior
Proyecto: Sistema de sincronización bidireccional entre ERP Dolibarr y tienda PrestaShop
Repositorio: https://github.com/luklpz/ProyectoIntermodular
Servicio desplegado: https://proyectointermodular-production-a9c3.up.railway.app
Swagger UI: https://proyectointermodular-production-a9c3.up.railway.app/swagger-ui.html
Cómo leer este documento
Este documento está pensado para tres perfiles distintos. Según tu situación, empieza por una sección diferente:
| Si quieres... | Ve a la sección |
|---|---|
| Entender qué hace el sistema y por qué existe | 1, 2, 3 |
| Entender la arquitectura y el código | 4, 5 |
| Arrancar el proyecto en local | 9 |
| Preparar la defensa oral ante el tribunal | 8, 10 |
| Entender los problemas que surgieron | 7, 8 |
| Saber qué queda pendiente | 11 |
Índice
- Objetivo del proyecto
- Arquitectura general del sistema
- Sistemas externos: Dolibarr y PrestaShop
- sync-service — El núcleo del sistema
- Apps cliente
- Despliegue en Railway
- Dificultades encontradas
- Pruebas en entorno real y bugs de PrestaShop 8
- Decisiones técnicas clave
- Limitaciones conocidas y trabajo futuro
- Conclusión
- Cómo arrancar el proyecto
- Glosario
1. Objetivo del proyecto
El problema
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.
La solución
Este proyecto desarrolla un sistema de sincronización automática que actúa como puente entre los dos sistemas. Sus funciones son:
- Publicar productos: mantiene la tienda actualizada con los productos y precios del ERP.
- Sincronizar stock: actualiza la disponibilidad de productos en la tienda con el stock real del almacén.
- Importar pedidos: cuando un cliente compra en la tienda, el pedido aparece automáticamente en el ERP como comanda y factura.
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).
2. Arquitectura general del sistema
El sistema se divide en tres capas independientes:
╔══════════════════════════════════════════════════════════════╗
║ CAPA 1 — APPS CLIENTE ║
║ ║
║ ┌─────────────────────┐ ┌─────────────────────┐ ║
║ │ JavaFX │ │ Android │ ║
║ │ Cliente escritorio│ │ Cliente móvil │ ║
║ └──────────┬──────────┘ └──────────┬──────────┘ ║
╚══════════════╪═══════════════════════════╪════════════════════╝
│ HTTP REST + Basic Auth │
│ (peticiones JSON) │
╔══════════════▼═══════════════════════════▼════════════════════╗
║ CAPA 2 — SYNC-SERVICE ║
║ ║
║ ┌────────────────┐ ┌─────────────────┐ ┌──────────────┐ ║
║ │ API REST │ │ Sync Jobs │ │ Base datos │ ║
║ │ (para apps │ │ (@Scheduled) │ │ H2 / MySQL │ ║
║ │ cliente) │ │ automáticos │ │ │ ║
║ └────────────────┘ └─────────────────┘ └──────────────┘ ║
║ Spring Boot 3 · Java 21 · Railway ║
╚═══════════════════╪══════════════════════╪════════════════════╝
│ │
┌─────────────▼──────┐ ┌───────────▼────────────┐
│ DOLIBARR │ │ PRESTASHOP │
│ ERP · REST API │ │ E-commerce · Webservice│
│ Auth: DOLAPIKEY │ │ Auth: ws_key param │
└────────────────────┘ └─────────────────────────┘
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:
- Expone una API REST consumida por las apps cliente.
- Ejecuta automáticamente las sincronizaciones en intervalos configurables.
- Mantiene una base de datos local con el historial y los mappings de IDs.
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.
Flujo completo de una petición
Para entender cómo se conectan todas las piezas, este es el recorrido de una sincronización de productos lanzada desde la app JavaFX:
Usuario pulsa "Sincronizar productos" en JavaFX
│
▼
JavaFX llama POST https://railway.app/api/sync/products
con header: Authorization: Basic base64(admin:admin123)
│
▼
Spring Security verifica las credenciales
│
▼
SyncController.triggerProductSync()
│
▼
ProductSyncService.synchronize()
├── Escribe SyncLog(startedAt=ahora) en BD local
├── DolibarrClient.getProducts() → GET /dolibarr/api/index.php/products
│ con header DOLAPIKEY: xxx
│ ← recibe lista de productos JSON
│
├── Para cada producto:
│ ├── PrestashopClient.getProductByReference(sku)
│ │ → GET /tienda/api/products?filter[reference]=SKU&ws_key=xxx
│ │ ← recibe producto existente O {"products": false}
│ │
│ ├── Si NO existe: PrestashopClient.createProduct()
│ │ → POST /tienda/api/products?ws_key=xxx (cuerpo: XML)
│ │ ← recibe producto creado con nuevo ID
│ │
│ ├── Si SÍ existe: PrestashopClient.updateProduct()
│ │ → PUT /tienda/api/products/{id}?ws_key=xxx (cuerpo: XML)
│ │
│ └── Guarda/actualiza ProductMapping en BD local
│
└── Actualiza SyncLog(finishedAt, itemsProcessed, itemsFailed) en BD local
│
▼
Respuesta JSON a JavaFX:
{"syncType":"PRODUCT_PUSH","itemsProcessed":12,"itemsFailed":0}
│
▼
JavaFX muestra: "Completado: 12 procesados"
¿Por qué esta arquitectura y no conectar directamente?
La alternativa sería que cada app cliente hablara directamente con las APIs de Dolibarr y PrestaShop. Esta opción fue descartada por varios motivos:
- Las claves de API estarían en el dispositivo del usuario: cualquiera con acceso al móvil o al PC podría ver las credenciales de Dolibarr.
- Sin historial: no habría base de datos local para registrar qué sincronizaciones han ocurrido y sus resultados.
- Sin automatización: las apps cliente pueden estar cerradas. El scheduling solo puede vivir en un proceso servidor que esté siempre activo.
- Duplicación de lógica: si hubiera tres apps cliente (JavaFX, Android, web), cada una tendría que implementar la misma lógica de sincronización.
3. Sistemas externos: Dolibarr y PrestaShop
3.1 Dolibarr
URL base: https://prestashop.loading.net/dolibarr
URL API: https://prestashop.loading.net/dolibarr/api/index.php
Dolibarr tiene una API REST nativa bien documentada y estándar. La autenticación se hace con un header HTTP en cada petición:
DOLAPIKEY: 3f7a9e2b1c8d4f6a...
La clave se genera en el panel de administración de Dolibarr (Configuración → Usuarios → clave API). Es sencilla de usar y funciona sin problemas.
Recursos de la API utilizados:
| Método | Endpoint | Para qué se usa |
|---|---|---|
| GET | /products |
Obtener todos los productos activos |
| GET | /products/{id} |
Obtener un producto concreto |
| PUT | /products/{id} |
Actualizar un producto |
| GET | /products/{id}/stock |
Obtener stock actual |
| GET | /thirdparties?email=x |
Buscar cliente por email |
| POST | /thirdparties |
Crear cliente nuevo |
| POST | /orders |
Crear comanda |
| POST | /invoices |
Crear factura |
| POST | /invoices/{id}/setpaid |
Marcar factura como pagada |
3.2 PrestaShop
URL base: https://prestashop.loading.net/tienda
URL API: https://prestashop.loading.net/tienda/api
PrestaShop tiene un Webservice nativo, pero presenta varias complicaciones respecto a Dolibarr.
Problema de autenticación con el hosting:
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:
# Incorrecto (nginx elimina este header):
Authorization: Basic BASE64(mi_clave_ws:)
# Correcto (sobrevive al proxy nginx):
GET /tienda/api/products?ws_key=mi_clave_ws&output_format=JSON
Problema del formato XML:
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.
Ejemplo del XML que hay que enviar para crear un producto:
<?xml version="1.0" encoding="UTF-8"?>
<prestashop xmlns:xlink="http://www.w3.org/1999/xlink">
<product>
<reference>CAMISETA-001</reference>
<price>19.99</price>
<active>1</active>
<state>1</state>
<id_category_default>2</id_category_default>
<name>
<language id="1">Camiseta básica blanca</language>
</name>
<description>
<language id="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 |
3.3 El identificador común: SKU
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í.
Ejemplo concreto:
| Campo | Dolibarr | PrestaShop |
|---|---|---|
| ID interno | 15 | 42 |
| 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:
product_mapping:
┌────┬───────────────┬─────────────┬──────────────┬────────────────┬─────────────┐
│ id │ sku │ dolibarr_id │ prestashop_id│ last_synced_at │ sync_status │
├────┼───────────────┼─────────────┼──────────────┼────────────────┼─────────────┤
│ 1 │ CAMISETA-001 │ 15 │ 42 │ 2025-05-10 ... │ SYNCED │
│ 2 │ PANTALON-002 │ 16 │ 43 │ 2025-05-10 ... │ SYNCED │
│ 3 │ ZAPATOS-003 │ 17 │ (null) │ (null) │ PENDING │
└────┴───────────────┴─────────────┴──────────────┴────────────────┴─────────────┘
4. sync-service — El núcleo del sistema
4.1 Stack tecnológico y decisiones de diseño
| Tecnología | Versión | Justificación |
|---|---|---|
| Java | 21 | LTS activo. Records, pattern matching disponibles. Versión estándar en empresas |
| Spring Boot | 3.x | Framework industrial estándar. Reduce enormemente el código de infraestructura |
| Maven | 3.9 | Gestión de dependencias estándar en proyectos Java |
| Spring Data JPA + Hibernate | incluido en Boot | Abstracción sobre JDBC. Repositorios sin SQL para operaciones CRUD estándar |
| H2 | dev | Base de datos en memoria. Cero instalación, arranca con la app |
| MySQL | prod | Base de datos relacional estándar en producción. Plugin de Railway |
| Flyway | incluido en Boot | Migraciones de esquema versionadas en SQL puro |
| RestClient | Spring 6 | Cliente HTTP moderno, síncrono, tipo-seguro |
| Jackson | incluido en Boot | Serialización/deserialización JSON estándar de facto en Java |
| Lombok | 1.18 | Elimina boilerplate: getters, setters, constructores, builders |
| SLF4J + Logback | incluido en Boot | Logging estándar. Sin dependencias adicionales |
| SpringDoc OpenAPI | 2.x | Genera Swagger UI automáticamente desde anotaciones del código |
| Spring Security | incluido en Boot | HTTP Basic Auth para proteger todos los endpoints |
| JUnit 5 + Mockito | incluido en Boot | Tests unitarios |
Tecnologías descartadas y por qué:
- 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.
4.2 Estructura de paquetes
sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/
│
├── SyncServiceApplication.java ← Punto de entrada (@SpringBootApplication)
│
├── config/ ← Configuración de Spring (beans, security...)
│ ├── IntegrationProperties.java ← @ConfigurationProperties validadas al inicio
│ ├── SecurityConfig.java ← Spring Security: Basic Auth, STATELESS, CSRF off
│ ├── SchedulingConfig.java ← ThreadPoolTaskScheduler de 1 hilo
│ ├── DolibarrClientConfig.java ← Bean RestClient para Dolibarr
│ ├── PrestashopClientConfig.java ← Bean RestClient para PrestaShop
│ └── SslConfig.java ← Configuración SSL/TLS para HTTPS
│
├── integration/ ← Capa de acceso a APIs externas
│ ├── dolibarr/
│ │ ├── DolibarrClient.java ← Todas las llamadas a la API de Dolibarr
│ │ ├── dto/ ← Objetos de datos del API de Dolibarr
│ │ │ ├── DolibarrProductDto.java
│ │ │ ├── DolibarrOrderDto.java
│ │ │ ├── DolibarrThirdpartyDto.java
│ │ │ ├── DolibarrInvoiceDto.java
│ │ │ └── DolibarrStockUpdateDto.java
│ │ └── exception/DolibarrApiException.java
│ └── prestashop/
│ ├── PrestashopClient.java ← Todas las llamadas al WS de PrestaShop
│ ├── dto/ ← Objetos de datos del WS de PrestaShop
│ │ ├── PrestashopProductDto.java
│ │ ├── PrestashopOrderDto.java
│ │ ├── PrestashopCustomerDto.java
│ │ └── PrestashopStockAvailableDto.java
│ └── exception/PrestashopApiException.java
│
├── mapping/ ← Entidades JPA y repositorios (base de datos local)
│ ├── ProductMapping.java
│ ├── OrderMapping.java
│ ├── SyncLog.java
│ ├── SyncStatus.java ← Enum: PENDING, SYNCED, ERROR
│ ├── OrderSyncStatus.java ← Enum: IMPORTED, INVOICED, ERROR
│ ├── SyncType.java ← Enum: PRODUCT_PUSH, STOCK_PUSH, ORDER_PULL
│ ├── ProductMappingRepository.java
│ ├── OrderMappingRepository.java
│ └── SyncLogRepository.java
│
├── sync/ ← Lógica de negocio: los tres flujos
│ ├── ProductSyncService.java ← Flujo 1: Dolibarr → PrestaShop (productos)
│ ├── StockSyncService.java ← Flujo 2: Dolibarr → PrestaShop (stock)
│ ├── OrderSyncService.java ← Flujo 3: PrestaShop → Dolibarr (pedidos)
│ ├── SyncScheduler.java ← @Scheduled: dispara los 3 flujos automáticamente
│ └── SyncResult.java ← Record(processed, failed, errors)
│
└── api/ ← API REST expuesta a las apps cliente
├── controller/
│ ├── SyncController.java ← POST /api/sync/{products,stock,orders}
│ ├── MappingController.java ← GET /api/mappings/{products,orders}
│ └── SyncLogController.java ← GET /api/logs, GET /api/logs/{id}
└── dto/ ← Objetos de respuesta para las apps cliente
├── SyncTriggerResponse.java
├── SyncLogResponse.java
├── ProductMappingResponse.java
└── OrderMappingResponse.java
Principio de separación de responsabilidades:
integration/solo sabe hablar con APIs externas. No contiene lógica de negocio.sync/contiene la lógica de negocio. No sabe cómo se hace una petición HTTP.mapping/define el modelo de datos. No sabe nada del resto.api/expone datos al exterior. No ejecuta lógica de sincronización.
Si mañana PrestaShop cambia el formato de su API, solo hay que modificar PrestashopClient.java. El resto del sistema no se toca.
4.3 Modelo de dominio — entidades JPA
Diagrama de la base de datos
┌──────────────────────────────┐
│ product_mapping │
├──────────────────────────────┤
│ id BIGINT (PK) │
│ sku VARCHAR(100) │ ← UNIQUE. Identificador común
│ dolibarr_id INT │ ← ID del producto en Dolibarr
│ prestashop_id INT │ ← ID del producto en PrestaShop
│ last_synced_at DATETIME │ ← Cuándo fue la última sync exitosa
│ sync_status VARCHAR(20) │ ← PENDING / SYNCED / ERROR
│ error_message TEXT │ ← null si SYNCED; mensaje si ERROR
└──────────────────────────────┘
┌──────────────────────────────┐
│ order_mapping │
├──────────────────────────────┤
│ id BIGINT (PK) │
│ prestashop_order_id INT │ ← ID del pedido en PrestaShop
│ dolibarr_order_id INT │ ← ID de la comanda en Dolibarr
│ dolibarr_invoice_id INT │ ← ID de la factura en Dolibarr
│ imported_at DATETIME │
│ status VARCHAR │ ← IMPORTED / INVOICED / ERROR
└──────────────────────────────┘
┌──────────────────────────────┐
│ sync_log │
├──────────────────────────────┤
│ id BIGINT (PK) │
│ sync_type VARCHAR(30) │ ← PRODUCT_PUSH / STOCK_PUSH / ORDER_PULL
│ started_at DATETIME │ ← Cuándo empezó
│ finished_at DATETIME │ ← null si aún está ejecutando
│ items_processed INT │ ← Cuántos ítems se procesaron bien
│ items_failed INT │ ← Cuántos fallaron
│ error_details TEXT │ ← null si todo fue bien; errores si hubo fallos
└──────────────────────────────┘
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.
¿Por qué Instant para fechas?
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.
¿Por qué no @Data de Lombok?
@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.
4.4 Capa de integración — clientes HTTP
DolibarrClient
Se configura como un bean Spring con el header DOLAPIKEY añadido globalmente:
RestClient.builder()
.baseUrl("https://.../dolibarr/api/index.php")
.defaultHeader("DOLAPIKEY", "3f7a9e2b1c8d...")
.build();
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).
PrestashopClient
Más complejo. Cada URL debe incluir la clave como query param y el formato de salida:
restClient.get()
.uri("/products?ws_key={key}&output_format=JSON&filter[reference]={ref}",
apiKey, reference)
.retrieve()
.body(PrestashopProductDto.ListResponse.class);
El cliente construye el XML de creación/actualización mediante métodos privados toProductXml(), toStockAvailableXml(). Ejemplo simplificado:
private String toProductXml(PrestashopProductDto dto) {
return """
<?xml version="1.0" encoding="UTF-8"?>
<prestashop>
<product>
<reference>%s</reference>
<price>%s</price>
<active>1</active>
<state>1</state>
...
</product>
</prestashop>
""".formatted(dto.reference(), dto.price());
}
4.5 Flujos de sincronización — servicios
Flujo 1: PRODUCT_PUSH — Productos Dolibarr → PrestaShop
Objetivo: mantener el catálogo de PrestaShop sincronizado con Dolibarr.
Diagrama de secuencia:
ProductSyncService DolibarrClient PrestashopClient BD local
│ │ │ │
│── getProducts() ────────►│ │ │
│◄── [lista productos] ───│ │ │
│ │ │ │
│ Para cada producto: │ │ │
│── getProductByReference(sku) ─────────────────►│ │
│◄── Optional<producto> ────────────────────────│ │
│ │ │ │
│ Si no existe: │ │ │
│── createProduct(xml) ──────────────────────────►│ │
│◄── producto creado (con nuevo ID) ────────────│ │
│ │ │ │
│ Si ya existe: │ │ │
│── updateProduct(id, xml) ──────────────────────►│ │
│ │ │ │
│── upsertMapping(sku, dolId, psId) ────────────────────────────────►│
│── updateSyncLog(processed, failed) ────────────────────────────────►│
Lógica de upsert por SKU:
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.
Flujo 2: STOCK_PUSH — Stock Dolibarr → PrestaShop
Objetivo: que el stock visible en la tienda coincida con el stock real del almacén.
Diagrama de secuencia:
StockSyncService DolibarrClient PrestashopClient BD local
│ │ │ │
│── findAll(status=SYNCED) ─────────────────────────────────────────►│
│◄── [lista de mappings] ─────────────────────────────────────────── │
│ │ │ │
│ Para cada mapping: │ │ │
│── getStockForProduct(dolId) ────────────────────►│ │
│◄── cantidad ───────────│ │ │
│ │ │ │
│── getStockAvailableForProduct(psId) ──────────►│ │
│◄── stockAvailableId ──────────────────────────│ │
│ │ │ │
│── updateStockAvailable(stockId, cantidad, xml) ►│ │
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.
Flujo 3: ORDER_PULL — Pedidos PrestaShop → Dolibarr
Objetivo: registrar en el ERP los pedidos realizados en la tienda online.
Diagrama de secuencia:
OrderSyncService PrestashopClient DolibarrClient BD local
│ │ │ │
│── getOrdersSince(lastSync) ──────────►│ │
│◄── [lista pedidos nuevos] ────────── │ │
│ │ │ │
│ Para cada pedido no procesado: │ │
│── getCustomer(psCustomerId) ──────────►│ │
│◄── datos del cliente ─────────────── │ │
│ │ │ │
│── getOrCreateThirdparty(email, nombre) ──────────────────► │
│ │ busca cliente por email │
│ │ si no existe → crea tercero │
│◄── dolibarrThirdpartyId ──────────────────────────────────│
│ │ │ │
│── createOrder(lineas del pedido) ────────────────────────► │
│◄── dolibarrOrderId ───────────────────────────────────────│
│ │ │ │
│── createInvoiceFromOrder(orderId) ───────────────────────► │
│◄── dolibarrInvoiceId ─────────────────────────────────────│
│ │ │ │
│── saveOrderMapping(psOrderId, dolOrderId, dolInvoiceId) ──►│
El método getOrCreateThirdparty:
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:
- Buscar en Dolibarr si existe un tercero con el email del cliente de PS.
- Si existe → usar su ID.
- Si no existe → crear un nuevo tercero con los datos del cliente de PS (nombre, email, dirección).
Esto evita duplicar clientes en Dolibarr si el mismo cliente hace varios pedidos.
¿Por qué se crea también la factura?
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.
Ambos son necesarios para que Dolibarr refleje correctamente la venta.
El patrón try/finally en los tres servicios
Todos los servicios de sincronización tienen esta estructura:
public SyncResult synchronize() {
// 1. Crear el log ANTES de empezar
SyncLog syncLog = syncLogRepository.save(SyncLog.builder()
.syncType(SyncType.PRODUCT_PUSH)
.startedAt(Instant.now())
.build());
int processed = 0, failed = 0;
List<String> errors = new ArrayList<>();
try {
// 2. Lógica principal
List<DolibarrProductDto> products = dolibarrClient.getProducts();
for (DolibarrProductDto product : products) {
try {
pushProduct(product);
processed++;
} catch (Exception e) {
failed++;
errors.add("SKU %s: %s".formatted(product.ref(), e.getMessage()));
markError(product.ref(), e.getMessage());
// No se relanza la excepción: el bucle continúa con el siguiente
}
}
} finally {
// 3. SIEMPRE se ejecuta, aunque haya una excepción inesperada
syncLog.setFinishedAt(Instant.now());
syncLog.setItemsProcessed(processed);
syncLog.setItemsFailed(failed);
syncLogRepository.save(syncLog);
}
return new SyncResult(processed, failed, errors);
}
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.
4.6 Scheduling — tareas automáticas
@Component
@ConditionalOnProperty(name = "sync.scheduling.enabled", havingValue = "true")
public class SyncScheduler {
@Scheduled(
fixedDelayString = "${sync.scheduling.product-push-delay:PT15M}",
initialDelayString = "${sync.scheduling.initial-delay:PT30S}"
)
public void scheduleProductPush() { ... } // cada 15 minutos
@Scheduled(
fixedDelayString = "${sync.scheduling.stock-push-delay:PT5M}",
initialDelayString = "${sync.scheduling.initial-delay:PT30S}"
)
public void scheduleStockPush() { ... } // cada 5 minutos
@Scheduled(
fixedDelayString = "${sync.scheduling.order-pull-delay:PT5M}",
initialDelayString = "${sync.scheduling.initial-delay:PT30S}"
)
public void scheduleOrderPull() { ... } // cada 5 minutos
}
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.
4.7 API REST propia
La API REST es la interfaz que usan las apps cliente (JavaFX, Android) para interactuar con el sistema.
Endpoints disponibles
Sincronización manual:
POST /api/sync/products → lanza PRODUCT_PUSH ahora
POST /api/sync/stock → lanza STOCK_PUSH ahora
POST /api/sync/orders → lanza ORDER_PULL ahora
GET /api/sync/diagnostics/dolibarr → verifica conectividad con Dolibarr
Ejemplo de respuesta de un POST /api/sync/products:
{
"syncType": "PRODUCT_PUSH",
"itemsProcessed": 12,
"itemsFailed": 1,
"hasErrors": true,
"errors": ["SKU CAMISETA-007: Connection timeout to PrestaShop"]
}
Mappings:
GET /api/mappings/products → todos los mappings
GET /api/mappings/products?status=ERROR → solo los que tienen error
GET /api/mappings/orders → todos los pedidos importados
Logs:
GET /api/logs → historial completo (más reciente primero)
GET /api/logs?type=STOCK_PUSH → solo logs de stock
GET /api/logs/{id} → detalle de una ejecución concreta
Ejemplo de respuesta de GET /api/logs/{id}:
{
"id": 42,
"syncType": "PRODUCT_PUSH",
"startedAt": "2025-05-10T14:32:11Z",
"finishedAt": "2025-05-10T14:32:15Z",
"itemsProcessed": 11,
"itemsFailed": 1,
"errorDetails": "SKU CAMISETA-007: Connection timeout to PrestaShop"
}
Health:
GET /actuator/health → {"status": "UP"} (no requiere autenticación)
Swagger UI
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.).
4.8 Seguridad
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).
HTTP Basic Auth:
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 desactivado:
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.
DelegatingPasswordEncoder:
Permite almacenar contraseñas con distintos algoritmos de hash en el mismo campo. El prefijo entre llaves indica el algoritmo:
{noop}admin123 → texto plano (solo para desarrollo)
{bcrypt}$2a$10$... → hash BCrypt (producción, seguro)
La ventaja es que el código no cambia entre entornos. Solo cambia el valor de la variable de entorno API_SECURITY_PASSWORD.
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.
4.9 Persistencia y migraciones
Flyway gestiona el esquema de la base de datos. Al arrancar la aplicación, Flyway:
- Busca el fichero
V1__init.sqlensrc/main/resources/db/migration/. - Comprueba si ya fue aplicado (lo registra en su tabla interna
flyway_schema_history). - Si no fue aplicado, ejecuta el SQL y crea las tablas.
- Si ya fue aplicado, no hace nada.
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.
H2 en modo MySQL:
url: jdbc:h2:mem:syncdb;MODE=MySQL
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.
4.10 Configuración externalizada
Las credenciales y URLs nunca están en el código fuente. Se leen desde ficheros de configuración o variables de entorno.
IntegrationProperties.java usa @ConfigurationProperties con validación Jakarta:
@ConfigurationProperties(prefix = "integration")
@Validated
public record IntegrationProperties(
@Valid Dolibarr dolibarr,
@Valid Prestashop prestashop
) {
public record Dolibarr(
@NotBlank String baseUrl,
@NotBlank String apiKey, // si está vacío → la app NO arranca
double defaultTaxRate
) {}
public record Prestashop(
@NotBlank String baseUrl,
@NotBlank String apiKey, // si está vacío → la app NO arranca
int defaultCategoryId
) {}
}
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.
5. Apps cliente
5.1 JavaFX — Cliente escritorio
Directorio: javafx-client/
Tecnología: JavaFX 21.0.2, Java 21, Maven
Arranque: mvn javafx:run desde javafx-client/
Pantallas y funcionalidades
Login:
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).
Panel (Dashboard):
- 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
Timelinede JavaFX.
Productos:
- Tabla con todos los mappings de productos (SKU, ID Dolibarr, ID PrestaShop, último sync, estado, error).
- Búsqueda por SKU en tiempo real (filtrado en cliente, sin peticiones al servidor).
- Filter chips para filtrar por estado: Todos / Sincronizados / Pendientes / Con error.
- Chips de estado con colores: verde (SYNCED), naranja (PENDING), rojo (ERROR).
- Botones para lanzar sincronización de productos y de stock.
Pedidos:
- Tabla con todos los mappings de pedidos importados (ID pedido PS, ID comanda Dolibarr, ID factura, estado, fecha).
- Chips de estado: gris (IMPORTED), azul (INVOICED), rojo (ERROR).
- Botón para lanzar sincronización de pedidos.
Historial:
- 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.
Ajustes:
- Formulario para cambiar la URL del servidor, usuario y contraseña sin necesidad de hacer logout.
- Botón "Probar conexión" que verifica la conectividad con los nuevos datos sin guardarlos ni modificar la sesión actual.
- Botón "Guardar" que actualiza los datos, reinicializa el cliente HTTP y persiste en el registro.
Arquitectura interna de la app JavaFX
JavaFxClientApplication ← Application de JavaFX, punto de entrada
│
├── ui/stages/
│ ├── LoginStage ← Ventana de login (420×520 px, no redimensionable)
│ └── MainStage ← Ventana principal (1200×720 px, 5 tabs)
│
├── ui/controllers/ ← Un controller por tab
│ ├── DashboardController ← Tarjetas, sync-all, auto-refresh
│ ├── ProductsController ← Tabla, búsqueda SKU, filter chips
│ ├── OrdersController ← Tabla de pedidos
│ ├── LogsController ← Tabla de logs, click → dialog
│ └── SettingsController ← Formulario de ajustes
│
├── ui/dialogs/
│ └── LogDetailDialog ← Stage modal (Modality.APPLICATION_MODAL)
│
├── api/
│ └── SyncServiceClient ← Jackson ObjectMapper + HttpURLConnection
│
├── model/
│ ├── SessionManager ← Credenciales + instancia del cliente HTTP
│ ├── AppSettings ← DTO: baseUrl, username, password
│ ├── SyncLogResponse ← DTO de respuesta de la API
│ ├── ProductMappingResponse
│ ├── OrderMappingResponse
│ └── SyncTriggerResponse
│
└── util/
├── SettingsStore ← java.util.prefs.Preferences (registro Windows)
├── DateUtils ← "hace 5 min", "2h 30m", labels localizados
└── 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:
Thread t = new Thread(() -> {
// Esto corre en background (hilo separado del UI)
List<SyncLogResponse> logs = client.getLogs(null);
// Platform.runLater(): vuelve al hilo de UI para actualizar la tabla
Platform.runLater(() -> logsTable.setItems(FXCollections.observableArrayList(logs)));
});
t.setDaemon(true); // el hilo muere cuando se cierra la app
t.start();
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.
Diseño visual: Paleta Material 3 idéntica a la app Android para consistencia visual entre plataformas.
| Elemento | Color |
|---|---|
| Primario / Productos | #1565C0 (azul) |
| Stock | #2E7D32 (verde) |
| Pedidos | #E65100 (naranja) |
| Error | #C62828 (rojo) |
| Fondo | #FAFAFA (gris muy claro) |
5.2 Android — Cliente móvil
Directorio: android-app/
Tecnología: Kotlin, Android SDK, Material 3
Desarrollado por: compañera del equipo
Pantallas y funcionalidades
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.
6. Despliegue en Railway
¿Por qué Railway?
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
maindispara 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.
Pipeline de despliegue continuo (CD)
Cada cambio en el código sigue este camino automáticamente:
Developer hace git push a GitHub (rama main)
│
▼
GitHub notifica a Railway (webhook)
│
▼
Railway clona el repositorio
│
▼
Railpack detecta: proyecto Maven en directorio sync-service/
│
▼
Instalación de herramientas via .mise.toml:
- java = "21.0.2"
- maven = "3.9.9"
│
▼
Build: mvn clean package -DskipTests
│
▼
Nuevo contenedor arranca con el JAR compilado
│
▼
Spring Boot arranca con SPRING_PROFILES_ACTIVE=prod
│
▼
Flyway conecta a MySQL y ejecuta migraciones pendientes
│
▼
Servicio disponible en la URL pública de Railway
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.
Configuración de variables de entorno
Las credenciales y configuración sensible se configuran como variables de entorno en el panel de Railway, nunca en el código:
SPRING_PROFILES_ACTIVE = prod
SPRING_DATASOURCE_URL = jdbc:mysql://mysql.railway.internal:3306/railway?...
SPRING_DATASOURCE_USERNAME = root
SPRING_DATASOURCE_PASSWORD = <contraseña del plugin MySQL>
SPRING_JPA_DATABASE_PLATFORM = org.hibernate.dialect.MySQLDialect
SPRING_JPA_HIBERNATE_DDL_AUTO = validate
SYNC_SCHEDULING_ENABLED = true
DOLIBARR_API_KEY = <clave API Dolibarr>
PRESTASHOP_API_KEY = <clave WS PrestaShop>
API_SECURITY_PASSWORD = {noop}admin123
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:
[tools]
java = "21.0.2"
maven = "3.9.9"
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.
7.5 Gestión de las credenciales en Railway
Descripción: La primera configuración del despliegue usó "Shared Variables" de Railway, pensando que se propagarían automáticamente al servicio.
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.
Solución: Configurar todas las variables directamente en el servicio ProyectoIntermodular, sin usar Shared Variables.
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.
8. Pruebas en entorno real y bugs de PrestaShop 8
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.
Bug 1 — {"products": false} en lugar de lista vacía
Versiones afectadas: PrestaShop 8.x
Comportamiento esperado: Al buscar un producto por referencia sin resultados: {"products": []}
Comportamiento real: {"products": false}
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().
Archivo: PrestashopClient.java — método getProductByReference
Bug 2 — HTTP 404 en lugar de lista vacía
Versiones afectadas: PrestaShop 8.x
Relacionado con Bug 1.
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.
Fix: Capturar PrestashopApiException con código de estado 404 y devolver Optional.empty().
Archivo: PrestashopClient.java — método getProductByReference
Bug 3 — Stock siempre "OUT OF STOCK" en la tienda
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).
Diagnóstico: PrestaShop gestiona el stock en la tabla ps_stock_available. Esta tabla puede tener dos tipos de registros para el mismo producto:
id_shop = 0→ stock "global", sin tienda específica.id_shop = 1→ stock para la tienda con ID 1 (la tienda principal).
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.
Evidencia SQL:
SELECT id_product, id_shop, quantity
FROM ps_stock_available
WHERE id_product IN (42, 43);
-- Resultado:
-- id_product | id_shop | quantity
-- 42 | 0 | 50 ← creado via WS, invisible para el storefront
-- 43 | 1 | 50 ← creado via admin, funciona
Fix código: Añadir <id_shop>1</id_shop> al XML de toStockAvailableXml().
Fix base de datos (one-time):
UPDATE ps_stock_available SET id_shop=1 WHERE id_shop=0;
Archivo: PrestashopClient.java — método toStockAvailableXml
Bug 4 — Productos invisibles en el panel de administración de PS 8
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.
Diagnóstico: PrestaShop 8 introdujo una columna state en ps_product:
state = 0→ borrador (no publicado).state = 1→ publicado.
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.
Evidencia SQL:
SELECT id_product, reference, active, state
FROM ps_product
WHERE id_product IN (42, 43);
-- Resultado:
-- id_product | reference | active | state
-- 42 | CAMISETA-001 | 1 | 0 ← invisible en admin
-- 43 | PANTALON-002 | 1 | 1 ← visible en admin
Fix código: Añadir <state>1</state> al XML de toProductXml().
Fix base de datos (one-time):
UPDATE ps_product SET state=1 WHERE state=0;
Archivo: PrestashopClient.java — método toProductXml
Bug 5 — Productos duplicados (18 copias del mismo producto)
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.
Los 18 duplicados se eliminaron manualmente desde el admin de PS.
Bug 6 — Soft-delete: el mapping local queda obsoleto
Síntoma: Al eliminar un producto desde el admin de PS y volver a ejecutar el sync, el producto no se recreaba en PS.
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.
Fix: Cambiar la estrategia: en lugar de confiar en el mapping local para decidir crear o actualizar, siempre se consulta PS por SKU:
getProductByReference(sku)devuelve producto → UPDATE.getProductByReference(sku)devuelve vacío → CREATE.
Esta estrategia es resiliente ante cualquier modificación manual en PS (borrados, cambios de referencia, etc.).
Archivo: ProductSyncService.java — método pushProduct
Observación — endpoint raíz /api/ devuelve HTTP 500
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.
¿Por qué un ThreadPoolTaskScheduler de un solo hilo?
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.
¿Por qué no @Transactional en los servicios de sincronización?
@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.
¿Por qué try/finally en lugar de capturar la excepción?
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.
¿Por qué DelegatingPasswordEncoder?
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.
¿Por qué Flyway en lugar de ddl-auto=create-drop o update?
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.
¿Por qué H2 con MODE=MySQL en lugar de una BD de verdad para desarrollo?
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.
¿Por qué RestClient y no RestTemplate ni WebClient?
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.
¿Por qué PrestaShop como fuente de verdad y no el mapping local?
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.
12. Cómo arrancar el proyecto
sync-service en local (desarrollo)
Requisitos: JDK 21, Maven 3.8+
# 1. Crear el fichero de configuración
cp sync-service/src/main/resources/application-dev.yml.example \
sync-service/src/main/resources/application-dev.yml
# 2. Editar application-dev.yml y rellenar las claves reales:
# integration.dolibarr.api-key: TU_CLAVE
# integration.prestashop.api-key: TU_CLAVE
# 3. Arrancar
cd sync-service
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
| Recurso | URL |
|---|---|
| API | http://localhost:8080 |
| Swagger UI | http://localhost:8080/swagger-ui.html |
| Consola H2 | http://localhost:8080/h2-console |
| JDBC URL H2 | jdbc:h2:mem:syncdb |
| Credenciales API | admin / admin123 |
application-dev.ymlestá en.gitignore— nunca se sube al repositorio.
sync-service en producción (Railway)
El despliegue es automático: un push a main en GitHub dispara el deploy.
| Recurso | URL |
|---|---|
| API | https://proyectointermodular-production-a9c3.up.railway.app |
| Swagger UI | https://proyectointermodular-production-a9c3.up.railway.app/swagger-ui.html |
| Health | https://proyectointermodular-production-a9c3.up.railway.app/actuator/health |
javafx-client
Requisitos: JDK 21, Maven 3.8+ (JavaFX se descarga automáticamente de Maven Central)
cd javafx-client
mvn javafx:run
La primera ejecución tarda ~1-2 minutos descargando dependencias. Las siguientes arrancan directamente.
Al iniciar, la app se conecta al servidor Railway por defecto. El checkbox "Usar servidor local" del formulario de login cambia a localhost:8080.
android-app
Abrir el directorio android-app/ con Android Studio. Seleccionar emulador o dispositivo físico y pulsar Run.
13. Glosario
| Término | Definición |
|---|---|
| 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. |
| PaaS | Platform as a Service. Servicio cloud que gestiona la infraestructura del servidor. En este proyecto: Railway. |
| Basic Auth | Mecanismo de autenticación HTTP. El cliente envía usuario:contraseña codificado en Base64 en el header Authorization. |
| CSRF | Cross-Site Request Forgery. Ataque que usa las cookies de sesión de un usuario. No aplica en APIs STATELESS sin cookies. |
| 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. |
| DTO | Data Transfer Object. Clase simple que solo transporta datos entre capas, sin lógica de negocio. |
| 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. |