ProyectoIntermodular/DOCUMENTACION.md

1005 lines
52 KiB
Markdown

# 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
1. [Objetivo del proyecto](#1-objetivo-del-proyecto)
2. [Arquitectura general del sistema](#2-arquitectura-general-del-sistema)
3. [Sistemas externos: Dolibarr y PrestaShop](#3-sistemas-externos-dolibarr-y-prestashop)
4. [sync-service — El núcleo del sistema](#4-sync-service--el-núcleo-del-sistema)
- 4.1 [Stack tecnológico y decisiones de diseño](#41-stack-tecnológico-y-decisiones-de-diseño)
- 4.2 [Estructura de paquetes](#42-estructura-de-paquetes)
- 4.3 [Modelo de dominio — entidades JPA](#43-modelo-de-dominio--entidades-jpa)
- 4.4 [Capa de integración — clientes HTTP](#44-capa-de-integración--clientes-http)
- 4.5 [Flujos de sincronización — servicios](#45-flujos-de-sincronización--servicios)
- 4.6 [Scheduling — tareas automáticas](#46-scheduling--tareas-automáticas)
- 4.7 [API REST propia](#47-api-rest-propia)
- 4.8 [Seguridad](#48-seguridad)
- 4.9 [Persistencia y migraciones](#49-persistencia-y-migraciones)
- 4.10 [Configuración externalizada](#410-configuración-externalizada)
5. [Apps cliente](#5-apps-cliente)
- 5.1 [JavaFX — Cliente escritorio](#51-javafx--cliente-escritorio)
- 5.2 [Android — Cliente móvil](#52-android--cliente-móvil)
6. [Despliegue en Railway](#6-despliegue-en-railway)
7. [Pruebas en entorno real y bugs encontrados](#7-pruebas-en-entorno-real-y-bugs-encontrados)
8. [Decisiones técnicas clave](#8-decisiones-técnicas-clave)
9. [Cómo arrancar el proyecto](#9-cómo-arrancar-el-proyecto)
10. [Glosario](#10-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:**
1. Sincronizar productos, propiedades y stock del ERP hacia PrestaShop.
2. 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 productos
- `GET /products/{id}` — obtener producto concreto
- `POST /products` — crear producto
- `PUT /products/{id}` — actualizar producto
- `GET /products/{id}/stock` — obtener stock
- `GET /thirdparties` — buscar clientes (terceros)
- `POST /thirdparties` — crear cliente
- `POST /orders` — crear comanda
- `POST /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 productos
- `GET /api/products?filter[reference]=SKU` — buscar por referencia
- `POST /api/products` — crear producto (XML)
- `PUT /api/products/{id}` — actualizar producto (XML)
- `GET /api/stock_availables` — consultar stock
- `PUT /api/stock_availables/{id}` — actualizar stock
- `GET /api/orders` — listar pedidos
- `GET /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.
```java
@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.
```java
@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.
```java
@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.
```java
// 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 activos
- `getProductByRef(String ref)` — busca un producto por SKU
- `createProduct(DolibarrProductDto)` — crea un producto nuevo
- `updateProduct(int id, DolibarrProductDto)` — actualiza un producto existente
- `getStockForProduct(int productId)` — obtiene el stock actual
- `getOrCreateThirdparty(String email, String name)` — busca o crea un cliente
- `createOrder(DolibarrOrderDto)` — crea una comanda
- `createInvoiceFromOrder(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:
1. **Autenticación por query param**: cada URL debe incluir `?ws_key=...&output_format=JSON`.
2. **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.
3. **Múltiples bugs de PS 8** que requirieron manejo especial (ver sección 7).
**Ejemplo de XML para crear un producto:**
```xml
<?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:**
1. Obtiene todos los productos de Dolibarr via `DolibarrClient.getProducts()`.
2. 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 con `updateProduct()`.
d. Guarda/actualiza el `ProductMapping` con los IDs de ambos sistemas.
3. Al finalizar, actualiza el `SyncLog` con 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:**
1. Obtiene todos los `ProductMapping` con status `SYNCED` (solo productos que ya existen en los dos sistemas).
2. Para cada uno:
a. Consulta el stock actual en Dolibarr.
b. Busca el ID del `stock_available` en PrestaShop para ese producto.
c. Actualiza el `stock_available` en 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:**
1. Obtiene los pedidos de PrestaShop creados desde la última ejecución (usando `filter[date_add]`).
2. 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 el `OrderMapping` con 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:
```java
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.
```java
@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 con `fixedRate=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:**
```json
{
"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)`.
```java
// 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:
```sql
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:
```java
@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.example` y `application-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 `main` redespliega 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`:
```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=true` es necesario porque MySQL 8 usa `caching_sha2_password` como 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:
```sql
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:**
```sql
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+
```bash
# 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)
```bash
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. |