ProyectoIntermodular/DOCUMENTACION.md

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

  1. Objetivo del proyecto
  2. Arquitectura general del sistema
  3. Sistemas externos: Dolibarr y PrestaShop
  4. sync-service — El núcleo del sistema
  5. Apps cliente
  6. Despliegue en Railway
  7. Pruebas en entorno real y bugs encontrados
  8. Decisiones técnicas clave
  9. Cómo arrancar el proyecto
  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.

@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 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 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:

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 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:

{
  "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.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:

[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:

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.