sincronización de producto ha empezado a fallar

This commit is contained in:
Mireya Serrano 2026-05-25 12:17:54 +02:00
parent abe534f4da
commit bc858122af
2 changed files with 0 additions and 227 deletions

View File

@ -1,226 +0,0 @@
# TFG — Integración Dolibarr-PrestaShop
## Quién soy
Estudiante DAM (Desarrollo de Aplicaciones Multiplataforma, FP Grado Superior). Este es mi TFG, evaluado por tribunal. Criterios: calidad de código, arquitectura limpia, justificación de decisiones técnicas, documentación, patrones correctos, sistema funcional end-to-end. Tengo que defenderlo oralmente — todo debe ser entendido y argumentable por mí.
## Objetivo
Integrar Dolibarr (ERP) con PrestaShop (e-commerce), ambos en hosting compartido bajo `httpdocs`.
Requisitos del enunciado:
1. Gestionar productos, propiedades y stock del ERP para integrarlos en PrestaShop.
2. Registrar en el ERP cualquier venta realizada a través de PrestaShop.
## Arquitectura (tres capas)
**Capa 1 — Apps cliente** (se hará después, no tocar ahora):
- JavaFX (escritorio)
- Kotlin Android (móvil)
- Panel web ligero (opcional)
**Capa 2 — `sync-service`** (esto es lo que construimos):
- Spring Boot 3 + Java 21 + Maven
- API REST propia que consumen las apps cliente
- Tareas `@Scheduled` de sincronización
- Persistencia local: mapeo de IDs y logs de sincronización
**Capa 3 — Sistemas externos:**
- **Dolibarr**: API REST nativa. Base: `https://prestashop.loading.net/dolibarr/api/index.php`. Auth: header `DOLAPIKEY: <clave>`. Formato: JSON.
- **PrestaShop**: Webservice nativo. Base: `https://prestashop.loading.net/tienda/api`. Auth: `ws_key` como **query param** (NO Basic Auth — nginx del hosting elimina el header `Authorization`). Formato: JSON con `&output_format=JSON`.
- **Bug PS 8.2.5**: endpoint raíz `/api/` devuelve 500. Usar siempre recursos específicos: `/api/products`, `/api/orders`, etc.
**Identificador común:** SKU/referencia del producto — `ref` en Dolibarr, `reference` en PrestaShop. Nunca los IDs internos.
## Stack tecnológico
| Capa | Elección |
|------|----------|
| Lenguaje/Framework | Java 21 + Spring Boot 3 + Maven |
| Persistencia | Spring Data JPA. H2 en `dev`, MySQL/MariaDB en `prod` |
| Cliente HTTP | `RestClient` de Spring (no `RestTemplate`, no `WebClient`) |
| Serialización | Jackson (incluido en Spring Web) |
| Validación | Jakarta Validation |
| Logging | SLF4J + Logback (default Spring Boot) |
| Testing | JUnit 5 + Mockito + Spring Boot Test + WireMock |
| Docs API | SpringDoc OpenAPI (Swagger UI) |
| Scheduling | `@Scheduled` nativo. ShedLock si hace falta lock distribuido |
## Estado actual
**Fases 0–5 completadas. `sync-service` funcional end-to-end.**
- Compila y arranca con `./mvnw spring-boot:run -Dspring-boot.run.profiles=dev`.
- Código en Gitea: `https://projectes.ieslamar.org/mireya.2526/ProyectoIntermodular.git`
- Espejo en GitHub: `https://github.com/luklpz/ProyectoIntermodular.git` (para Railway)
- Despliegue: Railway.app (Loading.es hosting compartido no puede ejecutar procesos Java persistentes)
**Pendiente:**
- Fase 6 — Apps cliente (proyecto separado)
## Modelo de dominio (entidades JPA locales)
- **`ProductMapping`**: `id`, `sku` (único), `dolibarrId`, `prestashopId`, `lastSyncedAt`, `syncStatus` (enum: `PENDING/SYNCED/ERROR`), `errorMessage`.
- **`OrderMapping`**: `id`, `prestashopOrderId`, `dolibarrOrderId`, `dolibarrInvoiceId`, `importedAt`, `status` (enum).
- **`SyncLog`**: `id`, `syncType` (enum: `PRODUCT_PUSH/STOCK_PUSH/ORDER_PULL`), `startedAt`, `finishedAt`, `itemsProcessed`, `itemsFailed`, `errorDetails` (TEXT).
## Flujos de sincronización
1. **`pushProductsToPrestashop`** — lee productos Dolibarr modificados desde `lastSyncedAt`, crea/actualiza en PrestaShop por SKU, actualiza `ProductMapping`.
2. **`pushStockToPrestashop`** — lee stock actual en Dolibarr de productos mapeados, empuja a `stock_availables` de PrestaShop.
3. **`pullOrdersFromPrestashop`** — consulta pedidos PrestaShop desde `lastSyncedAt` con `filter[date_add]=...`, crea/localiza cliente en Dolibarr, crea `commande`, opcionalmente factura, guarda `OrderMapping`.
Cada flujo escribe una entrada en `SyncLog` (éxito o fallo).
## Roadmap por fases
- ✅ **Fase 0 — Setup base**
- Estructura de paquetes: `integration.dolibarr`, `integration.prestashop`, `sync`, `mapping`, `api`, `config`
- Perfiles `application-dev.yml` (H2 MODE=MySQL) y `application-prod.yml` (MySQL) — ambos gitignoreados, `.example` en repo
- `@ConfigurationProperties` con `@Validated` para todas las properties sensibles
- SpringDoc OpenAPI en `pom.xml`
- ✅ **Fase 1 — Clientes HTTP tipados**
- `DolibarrClient`: `RestClient` + header `DOLAPIKEY` global. Métodos: getProducts, getProductByRef, createProduct, updateProduct, updateStock, getOrCreateThirdparty, createOrder, createInvoiceFromOrder
- `PrestashopClient`: `RestClient` + `ws_key` como query param en cada petición. Métodos: getProducts, getProductByReference, createProduct, updateProduct, getStockAvailableForProduct, updateStockAvailable, getOrdersSince, getCustomer
- DTOs separados por API con `@JsonIgnoreProperties(ignoreUnknown=true)`
- `DolibarrApiException`, `PrestashopApiException` con body logueado
- ✅ **Fase 2 — Modelo y persistencia**
- Entidades JPA: `ProductMapping`, `OrderMapping`, `SyncLog`
- Repositorios Spring Data JPA
- Flyway: `V1__init.sql` con tablas product_mapping, order_mapping, sync_log
- ✅ **Fase 3 — Servicios de sincronización**
- `ProductSyncService`, `StockSyncService`, `OrderSyncService`
- Cada uno devuelve `SyncResult(processed, failed)`
- `try/finally` garantiza que `SyncLog` se escribe siempre
- NO `@Transactional` en batch: cada `save()` auto-commit independiente
- ✅ **Fase 4 — Scheduling**
- `SyncScheduler` con `@Scheduled(fixedDelayString=...)` (delay post-finalización, no entre inicios)
- `@ConditionalOnProperty(sync.scheduling.enabled)` — desactivado en dev
- `ThreadPoolTaskScheduler(poolSize=1)` — ejecución secuencial, sin solapamiento
- ✅ **Fase 5 — API REST propia**
- `SyncController`: POST `/api/sync/{products,stock,orders}` — sync manual
- `MappingController`: GET `/api/mappings/products?status=`, `/api/mappings/orders`
- `SyncLogController`: GET `/api/logs?type=`, `/api/logs/{id}`
- Spring Security: Basic Auth, `DelegatingPasswordEncoder` (`{noop}` dev / `{bcrypt}` prod), STATELESS, CSRF off
- Swagger UI: `/swagger-ui.html`
- ✅ **Despliegue en Railway** ← completado
- GitHub mirror: `https://github.com/luklpz/ProyectoIntermodular.git`
- URL pública: `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`
- Root directory del servicio Railway: `sync-service`
- MySQL plugin de Railway — red privada (`mysql.railway.internal:3306`)
- Todas las credenciales en variables del servicio ProyectoIntermodular (ver sección Despliegue)
- 🔲 **Verificación end-to-end con datos reales** ← HACER ANTES DE FASE 6
- Ver sección "Verificación end-to-end" más abajo
- **Fase 6 — Apps cliente** — proyecto separado, avisaré cuando toque
## Verificación end-to-end con datos reales
Antes de Fase 6 hay que probar que los tres flujos funcionan con datos reales en Dolibarr y PrestaShop.
**Swagger UI:** `https://proyectointermodular-production-a9c3.up.railway.app/swagger-ui.html`
**Credenciales:** usuario `admin`, contraseña `admin123`
### Flujo 1 — Productos Dolibarr → PrestaShop
1. Crear un producto en Dolibarr (`https://prestashop.loading.net/dolibarr`) con referencia/SKU única (ej: `PROD-001`), nombre, precio y stock.
2. En Swagger: click "Authorize" → `admin` / `admin123`.
3. `POST /api/sync/products` → "Try it out" → "Execute".
4. Verificar respuesta: `{"processed": 1, "failed": 0}`.
5. Comprobar que el producto aparece en PrestaShop (`https://prestashop.loading.net/tienda`).
6. `GET /api/mappings/products` → debe listar el producto con `syncStatus: SYNCED`.
### Flujo 2 — Stock Dolibarr → PrestaShop
1. Modificar el stock del producto en Dolibarr.
2. `POST /api/sync/stock` → "Try it out" → "Execute".
3. Verificar que el stock actualizado aparece en PrestaShop.
### Flujo 3 — Pedidos PrestaShop → Dolibarr
1. Crear un pedido en PrestaShop comprando el producto.
2. `POST /api/sync/orders` → "Try it out" → "Execute".
3. Verificar respuesta: `{"processed": 1, "failed": 0}`.
4. Comprobar que el pedido aparece en Dolibarr como `commande`.
5. `GET /api/mappings/orders` → debe listar el pedido mapeado.
### Ver logs de cualquier sync
`GET /api/logs` → lista todas las ejecuciones con duración, items procesados y errores.
`GET /api/logs/{id}` → detalle de una ejecución concreta.
### Si algo falla
- `GET /api/logs` → campo `errorDetails` del log tendrá el mensaje de error.
- Los logs del servidor están en Railway → ProyectoIntermodular → Deploy Logs.
## Despliegue (Railway)
**Variables en el servicio ProyectoIntermodular (literales, NO Shared Variables):**
```
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=<ver MySQL service → MYSQLPASSWORD>
SPRING_JPA_DATABASE_PLATFORM=org.hibernate.dialect.MySQLDialect
SPRING_JPA_HIBERNATE_DDL_AUTO=validate
SYNC_SCHEDULING_ENABLED=true
DOLIBARR_API_KEY=<ver application-dev.yml local>
PRESTASHOP_API_KEY=<ver application-dev.yml local>
API_SECURITY_PASSWORD={noop}admin123
```
**Lecciones aprendidas Railway:**
- Las Shared Variables NO inyectan automáticamente en servicios — poner todo en variables del servicio directamente
- `${{MySQL.MYSQLPASSWORD}}` solo resuelve en variables propias del servicio, no en Shared Variables
- MySQL 8 requiere `allowPublicKeyRetrieval=true` en la URL JDBC (usa `caching_sha2_password`)
- `mysql.railway.internal:3306` es la red privada interna — funciona sin configuración adicional
Flyway ejecuta `V1__init.sql` en primer arranque y crea las tablas automáticamente.
## Convenciones de código
- Constructor injection con `@RequiredArgsConstructor` (Lombok). Sin `@Autowired` en campos.
- Lombok: `@Getter`, `@Setter`, `@RequiredArgsConstructor`, `@Builder` cuando aporten. **Nunca `@Data` en entidades JPA** (rompe `equals/hashCode` con relaciones bidireccionales).
- Nombres: `PascalCase` clases, minúsculas paquetes.
- JavaDoc en clases públicas y métodos no triviales (para la memoria del TFG).
- Commits pequeños y atómicos, mensajes en imperativo en inglés.
- Tests: unitarios con Mockito para servicios; integración mínima con `@SpringBootTest` + WireMock.
## Lo que NO quiero
- Microservicios, Kafka, Redis, Docker Compose con cinco contenedores.
- Dependencias sin valor real.
- CQRS, event sourcing, hexagonal estricto, DDD táctico complejo. Arquitectura en capas limpia es suficiente.
- Mezclar lógica Dolibarr y PrestaShop en la misma clase.
- Strings mágicos: todo a constantes o properties.
- `catch (Exception e)` genérico.
- Generar 15 archivos de golpe sin explicar qué hace cada uno.
## Cómo trabajamos
- Discutir diseño antes de generar código grande.
- Justificar decisiones técnicas (las tengo que defender ante tribunal).
- Avisar de trade-offs explícitamente.
- Una fase del roadmap a la vez. No saltar fases.
- README.md actualizado: cómo arrancar, perfiles, variables de entorno, endpoints.
## Decisiones técnicas clave (para defensa oral)
- **`fixedDelay` no `fixedRate`**: espera a que termine antes de contar el siguiente intervalo. Evita solapamiento si una sync tarda más de lo esperado.
- **`ThreadPoolTaskScheduler(poolSize=1)`**: jobs ejecutan secuencial, imposible ejecutar dos a la vez aunque lleguen a la vez.
- **No `@Transactional` en batch**: fallo en item 3 no hace rollback de items 1-2 ya guardados. Procesamiento parcial es mejor que todo-o-nada para sync.
- **`try/finally` en services**: `SyncLog` siempre se escribe aunque explote algo inesperado.
- **`DelegatingPasswordEncoder`**: soporta `{noop}` en dev y `{bcrypt}` en prod con el mismo campo de configuración.
- **Flyway sobre Liquibase**: menos verboso, SQL puro, suficiente para este proyecto.
- **H2 con `MODE=MySQL`**: mismas migraciones Flyway funcionan en dev y prod sin cambios.

View File

@ -76,7 +76,6 @@ public class PrestashopClient {
.queryParam("output_format", "JSON") .queryParam("output_format", "JSON")
.queryParam("display", "full") .queryParam("display", "full")
.queryParam("filter[reference]", exactRef) .queryParam("filter[reference]", exactRef)
.queryParam("filter[active]", "[1]")
.build()) .build())
.retrieve() .retrieve()
.body(PrestashopProductDto.ListResponse.class); .body(PrestashopProductDto.ListResponse.class);