fix: use plain String for PS multilingual fields; add Dolibarr diagnostic endpoint

PS WebService GET responses return name/description as plain strings, not
language-array objects. Using List<LangValue> caused Jackson to throw
HttpMessageConversionException on every getProductByReference call.

Also adds GET /api/sync/diagnostics/dolibarr to verify Dolibarr connectivity
and product count from the Railway environment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
luklpz 2026-05-14 23:26:28 +02:00
parent 806497b24d
commit 96d75b324a
4 changed files with 28 additions and 21 deletions

View File

@ -1,6 +1,8 @@
package com.teterialosjuanjos.tfg.sync_service.api.controller;
import com.teterialosjuanjos.tfg.sync_service.api.dto.SyncTriggerResponse;
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.DolibarrClient;
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto.DolibarrProductDto;
import com.teterialosjuanjos.tfg.sync_service.sync.OrderSyncService;
import com.teterialosjuanjos.tfg.sync_service.sync.ProductSyncService;
import com.teterialosjuanjos.tfg.sync_service.sync.StockSyncService;
@ -8,10 +10,14 @@ import com.teterialosjuanjos.tfg.sync_service.sync.SyncResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* REST endpoints for manually triggering synchronization flows.
* Useful in development (where scheduling is disabled) and for on-demand resync in production.
@ -25,6 +31,7 @@ import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor
public class SyncController {
private final DolibarrClient dolibarrClient;
private final ProductSyncService productSyncService;
private final StockSyncService stockSyncService;
private final OrderSyncService orderSyncService;
@ -47,6 +54,14 @@ public class SyncController {
return toResponse("ORDER_PULL", orderSyncService.synchronize());
}
@Operation(summary = "Diagnostic: list products returned by Dolibarr API (verifies connectivity)")
@GetMapping("/diagnostics/dolibarr")
public Map<String, Object> dolibarrDiagnostics() {
List<DolibarrProductDto> products = dolibarrClient.getProducts();
List<String> refs = products.stream().map(DolibarrProductDto::ref).toList();
return Map.of("count", products.size(), "refs", refs);
}
private static SyncTriggerResponse toResponse(String syncType, SyncResult result) {
return new SyncTriggerResponse(
syncType,

View File

@ -209,9 +209,9 @@ public class PrestashopClient {
// PS WebService requires XML body for writes; output_format=JSON only affects the response.
private String toProductXml(PrestashopProductDto dto) {
String name = firstLangValue(dto.name());
String description = firstLangValue(dto.description());
String descShort = firstLangValue(dto.descriptionShort());
String name = dto.name() != null ? dto.name() : "";
String description = dto.description() != null ? dto.description() : "";
String descShort = dto.descriptionShort() != null ? dto.descriptionShort() : "";
String slug = toSlug(dto.reference());
StringBuilder sb = new StringBuilder(512);
@ -260,12 +260,6 @@ public class PrestashopClient {
return sb.toString();
}
private static String firstLangValue(List<PrestashopProductDto.LangValue> langs) {
if (langs == null || langs.isEmpty()) return "";
String v = langs.get(0).value();
return v != null ? v : "";
}
/** Generates a URL-safe slug from any string (used for PS link_rewrite). */
private static String toSlug(String text) {
if (text == null || text.isBlank()) return "product";

View File

@ -9,8 +9,10 @@ import java.util.List;
/**
* Represents a PrestaShop product.
*
* <p>Multilingual fields (name, description) are returned as arrays of {@link LangValue}
* objects, one per configured shop language.</p>
* <p>The PS WebService GET response (both list and single) returns multilingual fields
* ({@code name}, {@code description}, {@code description_short}) as plain {@link String}
* values — NOT as language-array objects. The XML request bodies we send for create/update
* wrap these strings inside {@code <language id="1">} elements.</p>
*
* <p>Response wrappers ({@link ListResponse}, {@link SingleResponse}) match the JSON envelope
* PrestaShop adds around resources: {@code {"products":[...]}} and {@code {"product":{...}}}.</p>
@ -25,15 +27,11 @@ public record PrestashopProductDto(
/** "1" = active, "0" = inactive */
String active,
@JsonProperty("id_category_default") String idCategoryDefault,
List<LangValue> name,
List<LangValue> description,
@JsonProperty("description_short") List<LangValue> descriptionShort
String name,
String description,
@JsonProperty("description_short") String descriptionShort
) {
/** One language entry for a multilingual field, e.g. {@code {"id":"1","value":"Name"}}. */
@JsonIgnoreProperties(ignoreUnknown = true)
public record LangValue(String id, String value) {}
/** Envelope for {@code GET /api/products?display=full} */
@JsonIgnoreProperties(ignoreUnknown = true)
public record ListResponse(List<PrestashopProductDto> products) {}

View File

@ -144,9 +144,9 @@ public class ProductSyncService {
src.price(),
active,
categoryId,
List.of(new PrestashopProductDto.LangValue("1", label)),
List.of(new PrestashopProductDto.LangValue("1", description)),
List.of(new PrestashopProductDto.LangValue("1", ""))
label,
description,
""
);
}
}