52 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
Índice
- Objetivo del proyecto
- Arquitectura general del sistema
- Sistemas externos: Dolibarr y PrestaShop
- sync-service — El núcleo del sistema
- 4.1 Stack tecnológico y decisiones de diseño
- 4.2 Estructura de paquetes
- 4.3 Modelo de dominio — entidades JPA
- 4.4 Capa de integración — clientes HTTP
- 4.5 Flujos de sincronización — servicios
- 4.6 Scheduling — tareas automáticas
- 4.7 API REST propia
- 4.8 Seguridad
- 4.9 Persistencia y migraciones
- 4.10 Configuración externalizada
- Apps cliente
- Despliegue en Railway
- Pruebas en entorno real y bugs encontrados
- Decisiones técnicas clave
- Cómo arrancar el proyecto
- Glosario
1. Objetivo del proyecto
El proyecto tiene como objetivo integrar dos sistemas comerciales que trabajan de forma independiente:
- Dolibarr: software ERP (Enterprise Resource Planning) de código abierto. Gestiona el catálogo de productos, el stock y los pedidos internos de la empresa.
- PrestaShop: plataforma de comercio electrónico. Es la tienda online visible para los clientes.
El problema que resuelve este proyecto es que ambos sistemas no se comunican entre sí por defecto. Esto obliga a los empleados a actualizar manualmente los productos, el stock y los pedidos en los dos sistemas, lo que es lento, propenso a errores y genera inconsistencias (por ejemplo, vender un producto sin stock porque el ERP aún no había actualizado la tienda).
Los dos requisitos funcionales del enunciado son:
- Sincronizar productos, propiedades y stock del ERP hacia PrestaShop.
- Registrar en el ERP cualquier venta realizada en PrestaShop.
La solución desarrollada es un servicio intermediario (sync-service) que actúa como puente entre los dos sistemas, consultando sus APIs y manteniendo la coherencia de datos de forma automática.
2. Arquitectura general del sistema
El sistema se divide en tres capas:
┌──────────────────────────────────────────────────────────┐
│ CAPA 1 — APPS CLIENTE │
│ │
│ ┌──────────────────┐ ┌────────────────────┐ │
│ │ JavaFX │ │ Android │ │
│ │ (escritorio) │ │ (móvil) │ │
│ └────────┬─────────┘ └────────┬───────────┘ │
└────────────┼────────────────────────── ┼────────────────-┘
│ HTTP REST + Basic Auth │
│ │
┌────────────▼───────────────────────────▼────────────────┐
│ CAPA 2 — SYNC-SERVICE │
│ │
│ Spring Boot 3 · Java 21 · Desplegado en Railway │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ API REST │ │ Sync Jobs │ │ Persistencia │ │
│ │ (endpoints │ │ (@Scheduled) │ │ JPA + H2/ │ │
│ │ para apps) │ │ │ │ MySQL │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└───────────────┬──────────────────┬───────────────────────┘
│ │
┌──────────▼──────┐ ┌────────▼───────────┐
│ DOLIBARR │ │ PRESTASHOP │
│ ERP · REST API │ │ E-commerce · WS │
│ DOLAPIKEY auth │ │ ws_key query param│
└─────────────────┘ └────────────────────┘
Capa 1 — Apps cliente: Interfaces de usuario para que el personal de la empresa pueda monitorizar el estado de la sincronización, ver los mappings de productos y pedidos, y lanzar sincronizaciones manuales. Hay dos implementaciones: escritorio (JavaFX) y móvil (Android).
Capa 2 — sync-service: El núcleo del sistema. Es un servidor Spring Boot que:
- Expone una API REST consumida por las apps cliente.
- Ejecuta tareas de sincronización periódicas de forma automática.
- Mantiene una base de datos local con el historial de sincronizaciones y el mapeo de IDs entre sistemas.
Capa 3 — Sistemas externos: Dolibarr y PrestaShop ya existían con sus propias APIs. El proyecto no modifica ninguno de estos dos sistemas; solo los consulta a través de sus APIs oficiales.
¿Por qué esta arquitectura?
La decisión de tener un servicio intermediario en lugar de conectar directamente las apps cliente con Dolibarr y PrestaShop tiene varias ventajas:
- Un único punto de acceso: las apps cliente solo necesitan hablar con el sync-service. Si cambia la URL de Dolibarr o las claves de API, solo hay que cambiar la configuración del sync-service.
- Lógica centralizada: la lógica de sincronización (cómo se mapean los campos, qué hacer cuando falla un ítem, cómo se registran los errores) está en un solo lugar.
- Historial persistente: el sync-service guarda en base de datos el resultado de cada ejecución, algo que no sería posible si las apps cliente hicieran las llamadas directamente.
- Sincronización automática sin intervención: el scheduling solo puede vivir en un proceso servidor persistente, no en una app de escritorio que puede estar cerrada.
3. Sistemas externos: Dolibarr y PrestaShop
3.1 Dolibarr
URL base de la API: https://prestashop.loading.net/dolibarr/api/index.php
Dolibarr tiene una API REST nativa bien documentada. La autenticación se hace mediante un header HTTP:
DOLAPIKEY: <clave_api>
La clave se obtiene desde el panel de administración de Dolibarr (Settings → Users → API key). La API es estándar: devuelve JSON y sigue convenciones REST (GET para leer, POST para crear, PUT para actualizar).
Recursos utilizados en el proyecto:
GET /products— obtener lista de productosGET /products/{id}— obtener producto concretoPOST /products— crear productoPUT /products/{id}— actualizar productoGET /products/{id}/stock— obtener stockGET /thirdparties— buscar clientes (terceros)POST /thirdparties— crear clientePOST /orders— crear comandaPOST /invoices— crear factura
3.2 PrestaShop
URL base de la API: https://prestashop.loading.net/tienda/api
PrestaShop tiene un Webservice nativo. Aquí surgió el primer problema: la autenticación oficial del Webservice usa HTTP Basic Auth (la clave como usuario, sin contraseña). Sin embargo, el servidor nginx del hosting elimina el header Authorization antes de que llegue a PHP, lo que hace que cada petición falle con 401.
Solución: Pasar la clave de autenticación como query param en lugar de header:
GET /api/products?ws_key=TU_CLAVE&output_format=JSON
Este comportamiento está documentado como limitación conocida en hostings compartidos con nginx como proxy inverso.
Recursos utilizados:
GET /api/products— listar productosGET /api/products?filter[reference]=SKU— buscar por referenciaPOST /api/products— crear producto (XML)PUT /api/products/{id}— actualizar producto (XML)GET /api/stock_availables— consultar stockPUT /api/stock_availables/{id}— actualizar stockGET /api/orders— listar pedidosGET /api/customers/{id}— obtener datos del cliente
Importante: PrestaShop Webservice usa XML para crear y actualizar recursos (no JSON). JSON solo está disponible para las respuestas de lectura mediante
output_format=JSON.
3.3 El identificador común: SKU
El mayor reto de la integración es que Dolibarr y PrestaShop tienen IDs de producto completamente independientes. Un producto puede ser ID 15 en Dolibarr e ID 42 en PrestaShop, sin ninguna relación entre esos números.
La solución es usar el SKU (código de referencia del producto) como identificador común:
- En Dolibarr se llama
ref - En PrestaShop se llama
reference
El sync-service mantiene una tabla product_mapping que relaciona los dos IDs a través del SKU. Si el SKU es CAMISETA-001, la tabla guarda que ese producto es el ID 15 en Dolibarr y el ID 42 en PrestaShop.
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, sealed classes disponibles |
| Spring Boot | 3.x | Framework estándar de la industria. Reduce código boilerplate enormemente |
| Maven | 3.9 | Gestión de dependencias y build estándar en proyectos Java |
| Spring Data JPA + Hibernate | incluido en Boot | Abstracción sobre JDBC. Repositorios sin SQL para operaciones CRUD |
| H2 | dev | Base de datos en memoria para desarrollo. Sin instalación |
| MySQL | prod | Base de datos relacional estándar. Plugin de Railway |
| Flyway | incluido en Boot | Migraciones de esquema versionadas. SQL puro, sin XML |
| RestClient | Spring 6 | Cliente HTTP moderno. Reemplaza RestTemplate (deprecated) y WebClient (reactivo, innecesario) |
| Jackson | incluido en Boot | Serialización/deserialización JSON estándar |
| Lombok | 1.18 | Reduce boilerplate: getters, constructores, builders |
| SLF4J + Logback | incluido en Boot | Logging estándar. Configurado en logback.xml |
| SpringDoc OpenAPI | 2.x | Genera Swagger UI automáticamente desde las anotaciones |
| Spring Security | incluido en Boot | HTTP Basic Auth para proteger todos los endpoints |
| JUnit 5 + Mockito | incluido en Boot | Tests unitarios e integración |
Qué NO se usó y por qué:
- No WebClient / Reactor: el proyecto es síncrono. La programación reactiva añade complejidad sin beneficio real cuando no hay miles de peticiones concurrentes.
- No Docker Compose / Kubernetes: hosting compartido y Railway no requieren esto. Sobre-ingeniería para el contexto del proyecto.
- No Kafka / RabbitMQ: no hay comunicación asíncrona entre microservicios porque no hay microservicios.
- No CQRS / Event Sourcing / DDD táctico: patrones válidos en sistemas grandes. Para este proyecto añaden complejidad sin valor.
- No RestTemplate: está marcado como deprecated en Spring 6.
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
│ ├── IntegrationProperties.java # @ConfigurationProperties para Dolibarr y PS
│ ├── SecurityConfig.java # Spring Security (Basic Auth)
│ ├── SchedulingConfig.java # ThreadPoolTaskScheduler de 1 hilo
│ ├── DolibarrClientConfig.java # Bean RestClient para Dolibarr
│ ├── PrestashopClientConfig.java # Bean RestClient para PrestaShop
│ └── SslConfig.java # Configuración SSL/TLS
│
├── integration/ # Clientes HTTP hacia sistemas externos
│ ├── dolibarr/
│ │ ├── DolibarrClient.java # Todas las llamadas a la API de Dolibarr
│ │ ├── dto/ # DTOs: DolibarrProductDto, DolibarrOrderDto...
│ │ └── exception/DolibarrApiException.java
│ └── prestashop/
│ ├── PrestashopClient.java # Todas las llamadas al WS de PrestaShop
│ ├── dto/ # DTOs: PrestashopProductDto, PrestashopOrderDto...
│ └── exception/PrestashopApiException.java
│
├── mapping/ # Entidades JPA y repositorios
│ ├── ProductMapping.java # Mapeo SKU ↔ IDs Dolibarr/PS
│ ├── OrderMapping.java # Mapeo ID pedido PS ↔ Dolibarr
│ ├── SyncLog.java # Registro de cada ejecución de sync
│ ├── SyncStatus.java # Enum: PENDING, SYNCED, ERROR
│ ├── OrderSyncStatus.java # Enum: IMPORTED, INVOICED, ERROR
│ ├── SyncType.java # Enum: PRODUCT_PUSH, STOCK_PUSH, ORDER_PULL
│ ├── ProductMappingRepository.java # Spring Data JPA
│ ├── OrderMappingRepository.java
│ └── SyncLogRepository.java
│
├── sync/ # Lógica de sincronización
│ ├── ProductSyncService.java # Flujo 1: productos Dolibarr → PS
│ ├── StockSyncService.java # Flujo 2: stock Dolibarr → PS
│ ├── OrderSyncService.java # Flujo 3: pedidos PS → Dolibarr
│ ├── SyncScheduler.java # @Scheduled que dispara los 3 flujos
│ └── SyncResult.java # Record con 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
└── dto/ # DTOs de respuesta para las apps cliente
├── SyncTriggerResponse.java
├── SyncLogResponse.java
├── ProductMappingResponse.java
└── OrderMappingResponse.java
Esta estructura sigue el principio de separación de responsabilidades:
integration/solo sabe hablar con APIs externas. No conoce la lógica de negocio.sync/contiene la lógica de negocio. No sabe cómo funciona HTTP.mapping/define el modelo de datos persistente.api/expone los datos al exterior.
4.3 Modelo de dominio — entidades JPA
El sistema persiste tres tipos de datos en la base de datos local.
ProductMapping
Relaciona cada producto mediante su SKU compartido con sus IDs en cada sistema.
@Entity @Table(name = "product_mapping")
public class ProductMapping {
Long id;
String sku; // identificador común, único en la tabla
Integer dolibarrId; // ID del producto en Dolibarr
Integer prestashopId; // ID del producto en PrestaShop
Instant lastSyncedAt; // cuándo fue la última sync exitosa
SyncStatus syncStatus; // PENDING / SYNCED / ERROR
String errorMessage; // null si syncStatus = SYNCED
}
OrderMapping
Registra qué pedidos de PrestaShop han sido importados a Dolibarr y sus IDs resultantes.
@Entity @Table(name = "order_mapping")
public class OrderMapping {
Long id;
Integer prestashopOrderId; // ID del pedido en PS
Integer dolibarrOrderId; // ID de la comanda creada en Dolibarr
Integer dolibarrInvoiceId; // ID de la factura (puede ser null)
Instant importedAt;
OrderSyncStatus status; // IMPORTED / INVOICED / ERROR
}
SyncLog
Registra el resultado de cada ejecución de un flujo de sincronización.
@Entity @Table(name = "sync_log")
public class SyncLog {
Long id;
SyncType syncType; // PRODUCT_PUSH / STOCK_PUSH / ORDER_PULL
Instant startedAt;
Instant finishedAt; // null mientras está ejecutando
Integer itemsProcessed;
Integer itemsFailed;
String errorDetails; // null si todo fue bien; texto con errores si hubo fallos
}
Por qué Instant para las fechas: Instant representa un punto exacto en el tiempo en UTC, sin zona horaria. Es el tipo correcto para timestamps de sistema. Hibernate lo persiste como DATETIME en MySQL y funciona correctamente con H2 en modo MySQL.
Por qué no @Data de Lombok en entidades JPA: @Data genera equals() y hashCode() basados en todos los campos. En entidades JPA esto puede causar bucles infinitos si hay relaciones bidireccionales y problemas de rendimiento al comparar entidades en colecciones. Se usan @Getter, @Setter, @Builder, etc. de forma explícita.
4.4 Capa de integración — clientes HTTP
DolibarrClient
Gestiona todas las llamadas a la API REST de Dolibarr. Se configura como un bean Spring con el header DOLAPIKEY añadido de forma global a todas las peticiones.
// En DolibarrClientConfig.java
RestClient.builder()
.baseUrl(properties.dolibarr().baseUrl())
.defaultHeader("DOLAPIKEY", properties.dolibarr().apiKey())
.build();
Esto evita repetir el header en cada llamada. Si cambia la clave, solo hay que cambiarla en la configuración.
Métodos principales:
getProducts()— lista todos los productos activosgetProductByRef(String ref)— busca un producto por SKUcreateProduct(DolibarrProductDto)— crea un producto nuevoupdateProduct(int id, DolibarrProductDto)— actualiza un producto existentegetStockForProduct(int productId)— obtiene el stock actualgetOrCreateThirdparty(String email, String name)— busca o crea un clientecreateOrder(DolibarrOrderDto)— crea una comandacreateInvoiceFromOrder(int orderId)— genera factura desde comanda
PrestashopClient
Gestiona todas las llamadas al Webservice de PrestaShop. Más complejo que el cliente de Dolibarr por varias razones:
- Autenticación por query param: cada URL debe incluir
?ws_key=...&output_format=JSON. - XML para escrituras: crear y actualizar recursos requiere construir un documento XML a mano, ya que el Webservice de PS no acepta JSON en las peticiones de escritura.
- Múltiples bugs de PS 8 que requirieron manejo especial (ver sección 7).
Ejemplo de XML para crear un producto:
<?xml version="1.0" encoding="UTF-8"?>
<prestashop xmlns:xlink="...">
<product>
<reference>PROD-001</reference>
<price>19.99</price>
<active>1</active>
<state>1</state> <!-- PS 8: necesario para que aparezca en el admin -->
<name><language id="1">Nombre del producto</language></name>
<associations>
<categories><category><id>2</id></category></categories>
</associations>
</product>
</prestashop>
4.5 Flujos de sincronización — servicios
Los tres flujos de sincronización son los servicios más importantes del sistema.
Flujo 1: PRODUCT_PUSH — Productos Dolibarr → PrestaShop
Objetivo: que todos los productos del ERP estén disponibles en la tienda online.
Proceso:
- Obtiene todos los productos de Dolibarr via
DolibarrClient.getProducts(). - Para cada producto con SKU válido:
a. Busca en PrestaShop si ya existe un producto con ese SKU.
b. Si no existe → lo crea en PrestaShop con
createProduct(). c. Si ya existe → lo actualiza conupdateProduct(). d. Guarda/actualiza elProductMappingcon los IDs de ambos sistemas. - Al finalizar, actualiza el
SyncLogcon el resultado.
Patrón upsert por SKU: En lugar de confiar en el mapping local para decidir si crear o actualizar, el servicio siempre consulta PrestaShop como fuente de verdad. Esto evita el problema de que un producto exista en PS pero haya sido eliminado y el mapping local quede desactualizado (ver Bug 6 en sección 7).
Manejo de errores: Si falla un producto concreto (error de API, timeout, etc.), el error se registra en el ProductMapping de ese SKU como ERROR con el mensaje del error, pero el bucle continúa con el siguiente producto. Esto permite sincronización parcial: si de 100 productos falla uno, los 99 restantes se sincronizan correctamente.
Flujo 2: STOCK_PUSH — Stock Dolibarr → PrestaShop
Objetivo: que el stock visible en la tienda coincida con el stock real del ERP.
Proceso:
- Obtiene todos los
ProductMappingcon statusSYNCED(solo productos que ya existen en los dos sistemas). - Para cada uno:
a. Consulta el stock actual en Dolibarr.
b. Busca el ID del
stock_availableen PrestaShop para ese producto. c. Actualiza elstock_availableen PrestaShop con la cantidad de Dolibarr.
Por qué stock separado de productos: PrestaShop gestiona el stock en una tabla separada (ps_stock_available), no en ps_product. Por eso hay un flujo específico para stock.
Flujo 3: ORDER_PULL — Pedidos PrestaShop → Dolibarr
Objetivo: que los pedidos realizados en la tienda online queden registrados en el ERP.
Proceso:
- Obtiene los pedidos de PrestaShop creados desde la última ejecución (usando
filter[date_add]). - Para cada pedido no procesado aún (verificando que no existe en
OrderMapping): a. Obtiene los datos del cliente del pedido. b. Busca en Dolibarr si ese cliente ya existe (por email); si no, lo crea. c. Crea una comanda en Dolibarr con las líneas del pedido. d. Crea una factura desde la comanda. e. Guarda elOrderMappingcon los IDs del pedido, la comanda y la factura.
Por qué se crea también la factura: Un pedido en PrestaShop implica que el cliente ha pagado o comprometido la compra. En el flujo contable de Dolibarr, eso corresponde tanto a una comanda (registro del compromiso) como a una factura (documento contable).
El patrón try/finally en todos los servicios
Todos los servicios de sincronización siguen este patrón:
public SyncResult synchronize() {
SyncLog syncLog = syncLogRepository.save(SyncLog.builder()
.syncType(SyncType.PRODUCT_PUSH)
.startedAt(Instant.now())
.build());
int processed = 0;
int failed = 0;
List<String> errors = new ArrayList<>();
try {
// lógica de sincronización
} finally {
// SIEMPRE se ejecuta, aunque explote algo inesperado
syncLog.setFinishedAt(Instant.now());
syncLog.setItemsProcessed(processed);
syncLog.setItemsFailed(failed);
syncLogRepository.save(syncLog);
}
}
El bloque finally garantiza que siempre se escribe el log de sincronización, incluso si hay una excepción inesperada. Sin esto, si el servicio falla a mitad de ejecución, no habría ningún registro de lo ocurrido.
Por qué no @Transactional en los servicios de batch: Si se usara @Transactional, un fallo en el ítem 50 haría rollback de los ítems 1-49, perdiendo todo el trabajo. El procesamiento parcial (procesar 49 de 50 bien) es mejor comportamiento que todo-o-nada en un sistema de sincronización.
4.6 Scheduling — tareas automáticas
El SyncScheduler ejecuta los tres flujos de forma automática en producción.
@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() { ... }
@Scheduled(fixedDelayString = "${sync.scheduling.stock-push-delay:PT5M}",
initialDelayString = "${sync.scheduling.initial-delay:PT30S}")
public void scheduleStockPush() { ... }
@Scheduled(fixedDelayString = "${sync.scheduling.order-pull-delay:PT5M}",
initialDelayString = "${sync.scheduling.initial-delay:PT30S}")
public void scheduleOrderPull() { ... }
}
Decisiones técnicas en el scheduling:
fixedDelay en lugar de fixedRate:
fixedRate= espera X tiempo desde el inicio de la ejecución anterior. Si una sync tarda 16 minutos confixedRate=15m, la siguiente ya debería haber arrancado hace 1 minuto → se encolan dos ejecuciones simultáneas.fixedDelay= espera X tiempo desde la finalización de la ejecución anterior. Si tarda 16 minutos, la siguiente arranca a los 16+15 = 31 minutos del inicio. Sin solapamiento.
ThreadPoolTaskScheduler(poolSize=1):
El scheduler tiene un solo hilo de ejecución. Aunque tres métodos @Scheduled estén activos, solo puede ejecutarse uno a la vez. Esto es importante porque los tres flujos acceden a la misma base de datos local y a las mismas APIs externas.
@ConditionalOnProperty:
El scheduler solo se crea si la propiedad sync.scheduling.enabled=true. En el perfil dev esta propiedad es false, lo que significa que las syncs automáticas no se ejecutan en local. Hay que lanzarlas manualmente desde la API o Swagger.
Intervalos configurables:
Los intervalos están configurados en las variables de entorno del servicio en Railway. El valor por defecto en el código (:PT15M, :PT5M) usa notación ISO-8601 para duraciones. PT15M = 15 minutos, PT5M = 5 minutos.
4.7 API REST propia
El sync-service expone una API REST que consumen las apps cliente. Está protegida con Basic Auth.
Endpoints de sincronización manual
POST /api/sync/products → lanza PRODUCT_PUSH inmediatamente
POST /api/sync/stock → lanza STOCK_PUSH inmediatamente
POST /api/sync/orders → lanza ORDER_PULL inmediatamente
GET /api/sync/diagnostics/dolibarr → lista productos de Dolibarr (test de conectividad)
Respuesta de los POST:
{
"syncType": "PRODUCT_PUSH",
"itemsProcessed": 12,
"itemsFailed": 0,
"hasErrors": false,
"errors": []
}
Endpoints de mappings
GET /api/mappings/products → todos los mappings de productos
GET /api/mappings/products?status=SYNCED → filtrado por estado
GET /api/mappings/orders → todos los mappings de pedidos
Endpoints de logs
GET /api/logs → historial de todas las ejecuciones (ordenado por fecha desc)
GET /api/logs?type=PRODUCT_PUSH → filtrado por tipo
GET /api/logs/{id} → detalle de una ejecución concreta
Swagger UI
En producción: https://proyectointermodular-production-a9c3.up.railway.app/swagger-ui.html
SpringDoc genera esta interfaz automáticamente desde las anotaciones @RestController, @GetMapping, etc. del código. No requiere mantener documentación separada.
4.8 Seguridad
Todos los endpoints (excepto /actuator/health y /h2-console/**) requieren autenticación.
Se usa HTTP Basic Auth: cada petición incluye un header Authorization: Basic base64(usuario:contraseña).
// SecurityConfig.java
http
.csrf(csrf -> csrf.disable()) // sin sesiones → sin CSRF
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
CSRF desactivado: CSRF (Cross-Site Request Forgery) es un ataque que funciona usando las cookies de sesión de un usuario autenticado. Como esta API es STATELESS (sin sesiones, sin cookies), no hay superficie de ataque CSRF. Desactivarlo es correcto y necesario.
DelegatingPasswordEncoder:
Permite tener contraseñas con diferentes algoritmos de hash en el mismo campo de configuración. El prefijo {id} indica el algoritmo:
{noop}admin123→ sin hash (solo para desarrollo, texto plano){bcrypt}$2a$10$...→ hash BCrypt (producción)
Con esto, el mismo campo api.security.password sirve tanto para dev como para prod sin cambios en el código.
4.9 Persistencia y migraciones
Flyway gestiona el esquema de la base de datos. Al arrancar la aplicación, Flyway compara las migraciones en src/main/resources/db/migration/ con las ya aplicadas en la BD y ejecuta las que faltan.
El fichero V1__init.sql crea las tres tablas:
CREATE TABLE product_mapping (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
sku VARCHAR(100) NOT NULL UNIQUE,
dolibarr_id INT,
prestashop_id INT,
last_synced_at DATETIME(6),
sync_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
error_message TEXT
);
CREATE TABLE order_mapping (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
prestashop_order_id INT NOT NULL,
dolibarr_order_id INT,
dolibarr_invoice_id INT,
imported_at DATETIME(6),
status VARCHAR(30) NOT NULL DEFAULT 'IMPORTED'
);
CREATE TABLE sync_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
sync_type VARCHAR(30) NOT NULL,
started_at DATETIME(6) NOT NULL,
finished_at DATETIME(6),
items_processed INT,
items_failed INT,
error_details TEXT
);
H2 en modo MySQL: El perfil dev usa H2 en memoria con MODE=MySQL. Esto hace que H2 acepte la misma sintaxis SQL de MySQL, por lo que el mismo V1__init.sql funciona en dev y en prod sin ninguna modificación.
Por qué Flyway sobre Liquibase: Flyway usa SQL puro. Liquibase usa XML o YAML para describir los cambios, añadiendo una capa de abstracción innecesaria para este proyecto. Flyway es más simple y el resultado es el mismo.
4.10 Configuración externalizada
Todas las credenciales y URLs están externalizadas en ficheros de configuración, nunca en el código.
IntegrationProperties.java usa @ConfigurationProperties con validación:
@ConfigurationProperties(prefix = "integration")
@Validated
public record IntegrationProperties(
@Valid Dolibarr dolibarr,
@Valid Prestashop prestashop
) {
public record Dolibarr(
@NotBlank String baseUrl,
@NotBlank String apiKey,
double defaultTaxRate
) {}
public record Prestashop(
@NotBlank String baseUrl,
@NotBlank String apiKey,
int defaultCategoryId
) {}
}
Si apiKey está vacío o no está configurado, la aplicación no arranca y muestra un error claro. Esto evita arrancar con configuración inválida y fallar silenciosamente en tiempo de ejecución.
Perfiles:
application-dev.yml— H2, logging DEBUG, scheduling desactivado, credenciales dev. En.gitignore, nunca se sube al repositorio.application-prod.yml— MySQL, logging INFO, scheduling activado. En.gitignore, las credenciales reales se configuran como variables de entorno en Railway.application-dev.yml.exampleyapplication-prod.yml.example— plantillas sin credenciales reales. Sí están en el repositorio.
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/
La aplicación escritorio permite al personal de la empresa monitorizar y controlar la sincronización sin necesidad de usar Swagger o la terminal.
Pantallas
Login: Formulario de conexión al sync-service. Las credenciales se guardan automáticamente al hacer login exitoso y se pre-rellenan en los siguientes arranques de la app. Tiene un checkbox para cambiar rápidamente entre servidor Railway y servidor local.
Panel (Dashboard): Vista principal con:
- Tres tarjetas de sincronización (Productos, Stock, Pedidos), cada una con la información de la última ejecución (fecha relativa, ítems procesados, posibles errores).
- Botón "Sincronizar todo" que lanza los tres flujos en secuencia mostrando el progreso.
- Botones individuales en cada tarjeta para lanzar solo ese flujo.
- Actualización automática cada 30 segundos.
Productos: Tabla con todos los mappings de productos. Incluye:
- Búsqueda por SKU en tiempo real (filtrado client-side).
- Filter chips para filtrar por estado (Todos / Sincronizados / Pendientes / Con error).
- Chips de estado con colores (verde = sincronizado, naranja = pendiente, rojo = error).
- Botones para sincronizar productos y stock.
Pedidos: Tabla con todos los mappings de pedidos. Muestra el ID del pedido en PS, el ID de la comanda y factura en Dolibarr, el estado y la fecha de importación.
Historial: Tabla con todos los logs de sincronización. Filter chips por tipo. Al hacer click en una fila se abre un diálogo modal con el detalle completo (duración, ítems procesados, texto de error si lo hubo).
Ajustes: Formulario para cambiar la URL del servidor, usuario y contraseña en caliente. Botón "Probar conexión" verifica la conectividad sin guardar. Botón "Guardar" actualiza la configuración y reinicializa el cliente HTTP.
Arquitectura interna
JavaFxClientApplication # Application de JavaFX, punto de entrada
├── ui/stages/
│ ├── LoginStage # Ventana de login
│ └── MainStage # Ventana principal con tabs
├── ui/controllers/
│ ├── DashboardController # Lógica del tab Panel
│ ├── ProductsController # Lógica del tab Productos
│ ├── OrdersController # Lógica del tab Pedidos
│ ├── LogsController # Lógica del tab Historial
│ └── SettingsController # Lógica del tab Ajustes
├── ui/dialogs/
│ └── LogDetailDialog # Diálogo modal de detalle de log
├── api/
│ └── SyncServiceClient # Cliente HTTP hacia el sync-service
├── model/
│ ├── SessionManager # Gestiona la sesión actual (credenciales + cliente)
│ ├── AppSettings # DTO de ajustes (URL, usuario, contraseña)
│ ├── SyncLogResponse # DTO de respuesta de la API
│ ├── ProductMappingResponse
│ ├── OrderMappingResponse
│ └── SyncTriggerResponse
└── util/
├── SettingsStore # Persistencia de ajustes (java.util.prefs)
├── DateUtils # Formateo de fechas y labels localizados
└── AlertUtil # Diálogos de alerta reutilizables
Persistencia de ajustes: Se usa java.util.prefs.Preferences, que en Windows guarda los datos en el registro del sistema. No requiere ninguna dependencia adicional y es la forma estándar de Java para guardar preferencias de usuario.
Threading: Todas las llamadas HTTP se ejecutan en hilos de background (new Thread(() -> { ... }).start()) y el resultado se aplica al UI mediante Platform.runLater(). Esto evita que la interfaz se congele mientras se espera la respuesta del servidor.
Diseño visual: Paleta de colores Material 3 idéntica a la app Android:
- Primario:
#1565C0(azul) - Éxito/Stock:
#2E7D32(verde) - Pedidos:
#E65100(naranja) - Error:
#C62828(rojo)
5.2 Android — Cliente móvil
Directorio: android-app/
Tecnología: Kotlin, Android SDK
Desarrollado por: compañera del equipo
La app Android ofrece las mismas funcionalidades que la app JavaFX pero optimizada para dispositivos móviles con la misma paleta Material 3.
6. Despliegue en Railway
¿Por qué Railway?
El enunciado especifica que Dolibarr y PrestaShop están en un hosting compartido bajo httpdocs. Este tipo de hosting no permite ejecutar procesos Java persistentes: solo ejecuta PHP bajo petición. Por tanto, el sync-service no puede ejecutarse en el mismo servidor que Dolibarr y PrestaShop.
Railway es una plataforma PaaS (Platform as a Service) que permite desplegar aplicaciones como contenedores sin necesitar configurar servidores. Se elige por:
- Plan gratuito suficiente para el volumen de este proyecto.
- Integración directa con GitHub: cada push a
mainredespliega automáticamente. - Plugin de MySQL gestionado con red privada interna.
- Variables de entorno seguras para las credenciales.
Configuración
Root directory del servicio: sync-service (Railway usa esta subcarpeta como raíz del build).
Build: Railway detecta automáticamente que es un proyecto Maven y usa mvn package para compilar. La versión de Java y Maven se fija en sync-service/.mise.toml:
[tools]
java = "21.0.2"
maven = "3.9.9"
Base de datos: Plugin MySQL de Railway. Se comunica con el sync-service a través de la red privada interna (mysql.railway.internal:3306), más rápida y segura que la pública.
Variables de entorno configuradas en Railway:
SPRING_PROFILES_ACTIVE=prod
SPRING_DATASOURCE_URL=jdbc:mysql://mysql.railway.internal:3306/railway?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8&allowPublicKeyRetrieval=true
SPRING_DATASOURCE_USERNAME=root
SPRING_DATASOURCE_PASSWORD=<contraseña 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
allowPublicKeyRetrieval=truees necesario porque MySQL 8 usacaching_sha2_passwordcomo método de autenticación por defecto, que requiere esta opción en el conector JDBC.
Lección aprendida — Variables compartidas de Railway: Railway tiene "Shared Variables" que en teoría se comparten entre servicios. En la práctica, no se inyectan automáticamente en los servicios. Todas las variables deben configurarse directamente en el servicio ProyectoIntermodular, no como Shared Variables.
7. Pruebas en entorno real y bugs encontrados
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 en el comportamiento de la API de PrestaShop 8, que difiere de la documentación oficial.
Bug 1 — {"products": false} en lugar de array vacío
Contexto: Al buscar un producto por referencia y no encontrarlo, se esperaba recibir {"products": []} (lista vacía).
Comportamiento real: PrestaShop 8 devuelve {"products": false} cuando ningún producto coincide con el filtro.
Consecuencia: El cliente HTTP de Spring 6 lanzaba HttpMessageConversionException al intentar deserializar false como List<PrestashopProductDto>. La sincronización fallaba completamente.
Fix: Capturar HttpMessageConversionException y RestClientException en getProductByReference() y tratarlos como "no encontrado" (devolver Optional.empty()).
Archivo: PrestashopClient.java — método getProductByReference
Bug 2 — HTTP 404 en lugar de lista vacía
Contexto: Similar al Bug 1, pero en lugar de JSON inválido, PrestaShop devuelve HTTP 404.
Comportamiento real: En algunos casos, cuando el filtro no coincide con ningún resultado, PS devuelve 404 en lugar de 200 con lista vacía. Esto no está documentado.
Fix: Capturar PrestashopApiException con código 404 y devolver Optional.empty().
Archivo: PrestashopClient.java — método getProductByReference
Bug 3 — Stock siempre "OUT OF STOCK" en la tienda
Contexto: Después de sincronizar productos y stock desde Dolibarr, la tienda mostraba todos los productos como "OUT OF STOCK" aunque el stock en la base de datos fuera mayor que 0.
Causa: PrestaShop gestiona el stock en la tabla ps_stock_available. Tiene dos tipos de registros:
id_shop = 0→ stock global (sin tienda específica)id_shop = 1→ stock para la tienda específica (ID 1)
El storefront de PS solo lee los registros id_shop = 1 para mostrar disponibilidad. Cuando el sync-service creaba un producto via Webservice, PS creaba automáticamente el stock_available con id_shop = 0. Los productos creados manualmente desde el admin de PS tenían id_shop = 1.
Evidencia: Query SQL en la BD de PS:
SELECT id_product, id_shop, quantity FROM ps_stock_available
WHERE id_product IN (42, 43);
-- Resultado: id_shop = 0 para todos los creados via Webservice
Fix código: Añadir <id_shop>1</id_shop> en el XML de toStockAvailableXml().
Fix BD (one-time): UPDATE ps_stock_available SET id_shop=1 WHERE id_shop=0 para corregir los registros ya existentes.
Archivo: PrestashopClient.java — método toStockAvailableXml
Bug 4 — Productos invisibles en el panel de administración de PS 8
Contexto: Después de sincronizar productos, la tienda online los mostraba correctamente, pero el panel de administración de PrestaShop (Catalog → Products) mostraba 0 productos.
Causa: PrestaShop 8 introdujo una nueva columna state en ps_product:
state = 0→ borrador (no publicado)state = 1→ publicado
El admin de PS 8 filtra por state = 1 en su listado. El storefront no filtra por state. Por tanto, un producto con state = 0 es visible en la tienda pero invisible en el panel de administración.
Cuando se crea un producto via Webservice sin especificar state, PS lo crea con state = 0 (valor por defecto de la columna).
Evidencia:
SELECT id_product, reference, active, state FROM ps_product
WHERE id_product IN (42, 43);
-- Resultado: state = 0 para todos los creados via Webservice
Fix código: Añadir <state>1</state> en el XML de toProductXml().
Fix BD (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)
Contexto: Al revisar la BD de PrestaShop, se encontraron 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 por filter[active]=[1]. Los productos con state = 0 (Bug 4), aunque estaban activos (active = 1), no eran devueltos correctamente. El sync-service interpretaba que el producto no existía en PS y lo creaba de nuevo en cada ejecución.
Fix: El fix del Bug 4 (state = 1) resolvió la causa raíz. Los 18 duplicados se eliminaron manualmente desde el panel de administración de PS.
Bug 6 — Soft-delete en PrestaShop: el mapping local queda obsoleto
Contexto: Al eliminar un producto desde el panel de administración de PS, el sync-service seguía intentando hacer PUT al mismo ID de PS, recibiendo 200 pero el producto ya no existía realmente en la tienda.
Causa: PrestaShop usa soft-delete: al eliminar un producto desde el admin, no borra el registro de la tabla ps_product, sino que pone active = 0. El mapping local del sync-service tenía el antiguo ID de PS almacenado y lo usaba para el UPDATE, pero ese ID ya apuntaba a un producto "borrado".
Fix: Cambiar la estrategia de sincronización: en lugar de confiar en el mapping local para decidir si crear o actualizar, siempre se consulta PrestaShop por SKU como fuente de verdad:
- Si
getProductByReference(sku)devuelve resultado → UPDATE. - Si devuelve vacío → CREATE.
Esto hace la sincronización idempotente y resiliente ante cambios manuales en PS.
Archivo: ProductSyncService.java — método pushProduct
Observación adicional — Endpoint raíz /api/ da HTTP 500
El endpoint raíz del Webservice de PrestaShop (/api/) devuelve HTTP 500 en PS 8.2.5, aunque en versiones anteriores devolvía la lista de recursos disponibles. No es un bug crítico pero hay que evitar llamar a ese endpoint. Siempre se usan recursos específicos: /api/products, /api/orders, etc.
8. Decisiones técnicas clave
Esta sección resume las decisiones de diseño más importantes y su justificación. Son los puntos que se deben poder argumentar en la defensa oral.
fixedDelay vs fixedRate en @Scheduled
fixedDelay mide el tiempo desde que termina la ejecución anterior. fixedRate mide desde que empieza. Si una sync tarda más que el intervalo configurado y se usa fixedRate, se acumulan ejecuciones pendientes. Con fixedDelay eso es imposible: la siguiente siempre espera a que termine la anterior.
ThreadPoolTaskScheduler(poolSize=1) — pool de un solo hilo
Aunque hay tres métodos @Scheduled, el scheduler tiene un único hilo de ejecución. Esto garantiza que los tres flujos son estrictamente secuenciales. Dos flujos no pueden ejecutarse a la vez aunque el pool tuviera más hilos, porque acceden a los mismos recursos (BD local, mismas APIs).
No @Transactional en servicios de batch
En el procesamiento de múltiples ítems, usar @Transactional haría que un fallo en el ítem N deshiciera todos los guardados anteriores (rollback). El comportamiento correcto para sincronización es procesamiento parcial: si falla 1 de 100, los 99 que funcionaron se guardan correctamente. Cada save() es un autocommit independiente.
try/finally garantiza escritura del SyncLog
El bloque finally en cada servicio asegura que el SyncLog siempre se escribe, incluso si hay una excepción no controlada. Sin esto, si el servicio crashea a mitad de ejecución, no quedaría ningún registro del intento.
DelegatingPasswordEncoder — mismo campo para dev y prod
Permite usar {noop}texto_plano en desarrollo y {bcrypt}hash en producción en el mismo campo de configuración. El código no cambia entre entornos; solo la variable de entorno.
Flyway sobre Liquibase
Flyway usa SQL puro para las migraciones. Liquibase añade una capa de abstracción (XML/YAML) que no aporta valor en este proyecto. Ambas herramientas resuelven el mismo problema; Flyway es más simple.
H2 con MODE=MySQL para desarrollo
H2 acepta la sintaxis SQL de MySQL cuando se activa MODE=MySQL. Esto permite usar exactamente los mismos ficheros de migración Flyway en dev y prod sin ninguna adaptación.
RestClient sobre RestTemplate y WebClient
RestTemplate está marcado como deprecated en Spring 6. 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.
PS como fuente de verdad para productos (no el mapping local)
Siempre se consulta PS antes de decidir si crear o actualizar. El mapping local puede quedar obsoleto si alguien borra un producto desde el admin de PS. Hacer getProductByReference() en cada sync cuesta una petición HTTP extra pero garantiza que nunca se actualiza un ID de PS que ya no existe.
SKU como identificador común en lugar de IDs internos
Los IDs internos de Dolibarr y PrestaShop son independientes y no tienen relación entre sí. El SKU (referencia del producto) es el único campo que tiene el mismo valor en los dos sistemas. Es el "pasaporte" del producto.
9. Cómo arrancar el proyecto
sync-service en local (desarrollo)
Requisitos: JDK 21, Maven 3.8+
# 1. Crear fichero de configuración dev
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
- Servidor:
http://localhost:8080 - Swagger UI:
http://localhost:8080/swagger-ui.html - Consola H2:
http://localhost:8080/h2-console(JDBC URL:jdbc:h2:mem:syncdb) - Credenciales API:
admin/admin123
sync-service en producción (Railway)
El despliegue es automático: cada push a la rama main del repositorio GitHub dispara un nuevo deploy en Railway.
- URL:
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)
cd javafx-client
mvn javafx:run
La primera ejecución descarga las dependencias (~1-2 minutos). Las siguientes arrancan directamente.
Los ajustes de conexión (URL, usuario, contraseña) se guardan automáticamente al hacer login y se pueden cambiar desde la pestaña Ajustes de la aplicación.
android-app
Abrir el directorio android-app/ con Android Studio y ejecutar en emulador o dispositivo físico.
10. Glosario
| Término | Definición |
|---|---|
| ERP | Enterprise Resource Planning. Software de gestión empresarial que integra procesos de negocio (contabilidad, stock, ventas...). En este proyecto: Dolibarr. |
| Webservice | Interfaz de programación de PrestaShop para integración con sistemas externos. Usa XML para escrituras y puede devolver JSON para lecturas. |
| SKU | Stock Keeping Unit. Código único que identifica un producto. En Dolibarr se llama ref; en PrestaShop reference. |
| Mapping | Registro que relaciona el mismo objeto en dos sistemas distintos. Por ejemplo, el producto con SKU 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 de la base de datos, se marca con un flag (ej. 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 operaciones de sync son idempotentes: ejecutar PRODUCT_PUSH dos veces no crea productos 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 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 en orden. |
| 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 donde el temporizador empieza a contar cuando termina la ejecución anterior. |
| fixedRate | Modo de scheduling donde el temporizador empieza a contar cuando empieza la ejecución anterior (puede causar solapamientos). |
@ConditionalOnProperty |
Anotación Spring que activa un bean solo si una propiedad tiene un valor determinado. Permite desactivar el scheduler en el perfil dev. |
| ISO-8601 | Estándar internacional para representar fechas y duraciones. PT15M = 15 minutos, PT5M = 5 minutos. |
DelegatingPasswordEncoder |
Codificador de contraseñas de Spring que selecciona el algoritmo basándose en el prefijo {id} de la contraseña almacenada. |