From 4bf6b77c6c8797f0806d9a1f5c7de8ac518e3d65 Mon Sep 17 00:00:00 2001 From: Mireya Serrano Date: Tue, 26 May 2026 17:06:07 +0200 Subject: [PATCH] products --- .../prestashop/PrestashopClient.java | 83 +++++++++++++++++-- .../prestashop/dto/PrestashopCategoryDto.java | 15 ++++ .../prestashop/dto/PrestashopLanguageDto.java | 12 +++ .../sync_service/sync/ProductSyncService.java | 29 ++++++- 4 files changed, 129 insertions(+), 10 deletions(-) create mode 100644 sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/prestashop/dto/PrestashopCategoryDto.java create mode 100644 sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/prestashop/dto/PrestashopLanguageDto.java diff --git a/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/prestashop/PrestashopClient.java b/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/prestashop/PrestashopClient.java index dbd8315..c526a00 100644 --- a/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/prestashop/PrestashopClient.java +++ b/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/prestashop/PrestashopClient.java @@ -15,6 +15,7 @@ import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; /** * Typed HTTP client for the PrestaShop Webservice. @@ -32,7 +33,8 @@ public class PrestashopClient { private final RestClient restClient; private final String wsKey; - private final String languageId; + private final String fallbackLanguageId; + private volatile List cachedLanguageIds; public PrestashopClient( @Qualifier("prestashopRestClient") RestClient restClient, @@ -40,7 +42,7 @@ public class PrestashopClient { ) { this.restClient = restClient; this.wsKey = props.prestashop().apiKey(); - this.languageId = String.valueOf(props.prestashop().languageId()); + this.fallbackLanguageId = String.valueOf(props.prestashop().languageId()); } // ── Products ────────────────────────────────────────────────────────── @@ -282,6 +284,66 @@ public class PrestashopClient { } } + // ── Languages / Categories ──────────────────────────────────────────── + + /** + * Fetches all active PrestaShop language IDs and caches them for the lifetime of the bean. + * Falls back to the configured {@code language-id} if the request fails. + * Sending content for ALL active languages ensures the value is set regardless of which + * language is the shop default. + */ + public synchronized List getActiveLanguageIds() { + if (cachedLanguageIds != null) return cachedLanguageIds; + try { + PrestashopLanguageDto.ListResponse resp = restClient.get() + .uri(u -> u.path("/languages") + .queryParam("ws_key", wsKey) + .queryParam("output_format", "JSON") + .queryParam("display", "full") + .queryParam("filter[active]", "[1]") + .build()) + .retrieve() + .body(PrestashopLanguageDto.ListResponse.class); + if (resp != null && resp.languages() != null && !resp.languages().isEmpty()) { + cachedLanguageIds = resp.languages().stream() + .map(l -> String.valueOf(l.id())) + .collect(Collectors.toList()); + log.info("PS active language IDs: {}", cachedLanguageIds); + return cachedLanguageIds; + } + } catch (Exception e) { + log.warn("Could not fetch PS language IDs, falling back to id={}: {}", fallbackLanguageId, e.getMessage()); + } + cachedLanguageIds = List.of(fallbackLanguageId); + return cachedLanguageIds; + } + + /** + * Finds a PrestaShop category whose name matches (case-insensitive) the given label. + * Returns the PS category ID, or empty if not found. + */ + public Optional findCategoryIdByName(String label) { + if (label == null || label.isBlank()) return Optional.empty(); + try { + PrestashopCategoryDto.ListResponse resp = restClient.get() + .uri(u -> u.path("/categories") + .queryParam("ws_key", wsKey) + .queryParam("output_format", "JSON") + .queryParam("display", "full") + .build()) + .retrieve() + .body(PrestashopCategoryDto.ListResponse.class); + if (resp == null || resp.categories() == null) return Optional.empty(); + return resp.categories().stream() + .filter(c -> label.equalsIgnoreCase(c.name())) + .map(PrestashopCategoryDto::id) + .findFirst(); + } catch (Exception e) { + log.warn("Could not search PS categories for label='{}': {}", label, e.getMessage()); + return Optional.empty(); + } + } + // ── XML builders ────────────────────────────────────────────────────── // PS WebService requires XML body for writes; output_format=JSON only affects the response. @@ -314,10 +376,19 @@ public class PrestashopClient { sb.append("1"); sb.append("1"); sb.append("both"); - sb.append("").append(escapeXml(slug)).append(""); - sb.append("").append(escapeXml(name)).append(""); - sb.append("").append(escapeXml(description)).append(""); - sb.append("").append(escapeXml(descShort)).append(""); + List langIds = getActiveLanguageIds(); + sb.append(""); + for (String lid : langIds) sb.append("").append(escapeXml(slug)).append(""); + sb.append(""); + sb.append(""); + for (String lid : langIds) sb.append("").append(escapeXml(name)).append(""); + sb.append(""); + sb.append(""); + for (String lid : langIds) sb.append("").append(escapeXml(description)).append(""); + sb.append(""); + sb.append(""); + for (String lid : langIds) sb.append("").append(escapeXml(descShort)).append(""); + sb.append(""); List cats = (dto.categoryIds() != null && !dto.categoryIds().isEmpty()) ? dto.categoryIds() : (dto.idCategoryDefault() != null ? List.of(dto.idCategoryDefault()) : List.of()); diff --git a/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/prestashop/dto/PrestashopCategoryDto.java b/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/prestashop/dto/PrestashopCategoryDto.java new file mode 100644 index 0000000..a67fbc6 --- /dev/null +++ b/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/prestashop/dto/PrestashopCategoryDto.java @@ -0,0 +1,15 @@ +package com.teterialosjuanjos.tfg.sync_service.integration.prestashop.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record PrestashopCategoryDto( + Integer id, + @JsonDeserialize(using = MultilangStringDeserializer.class) String name +) { + @JsonIgnoreProperties(ignoreUnknown = true) + public record ListResponse(List categories) {} +} diff --git a/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/prestashop/dto/PrestashopLanguageDto.java b/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/prestashop/dto/PrestashopLanguageDto.java new file mode 100644 index 0000000..b970a02 --- /dev/null +++ b/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/integration/prestashop/dto/PrestashopLanguageDto.java @@ -0,0 +1,12 @@ +package com.teterialosjuanjos.tfg.sync_service.integration.prestashop.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record PrestashopLanguageDto(Integer id, String active) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record ListResponse(List languages) {} +} diff --git a/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/sync/ProductSyncService.java b/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/sync/ProductSyncService.java index e6ef6c9..5c411ba 100644 --- a/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/sync/ProductSyncService.java +++ b/sync-service/src/main/java/com/teterialosjuanjos/tfg/sync_service/sync/ProductSyncService.java @@ -156,16 +156,37 @@ public class ProductSyncService { try { List dolibarrCats = dolibarrClient.getProductCategories(dolibarrProductId); if (dolibarrCats.isEmpty()) return List.of(); - List dolibarrIds = dolibarrCats.stream().map(DolibarrCategoryDto::id).toList(); - return categoryMappingRepository.findByDolibarrIdIn(dolibarrIds).stream() - .map(m -> String.valueOf(m.getPrestashopId())) - .toList(); + + List psIds = new ArrayList<>(); + for (DolibarrCategoryDto cat : dolibarrCats) { + if (cat.id() == null) continue; + CategoryMapping mapping = categoryMappingRepository.findByDolibarrId(cat.id()) + .orElseGet(() -> discoverAndSaveCategory(cat)); + if (mapping != null) psIds.add(String.valueOf(mapping.getPrestashopId())); + } + return psIds; } catch (Exception e) { log.warn("ProductSync: could not resolve categories for Dolibarr product id={}: {}", dolibarrProductId, e.getMessage()); return List.of(); } } + private CategoryMapping discoverAndSaveCategory(DolibarrCategoryDto cat) { + Optional psId = prestashopClient.findCategoryIdByName(cat.label()); + if (psId.isEmpty()) { + log.warn("ProductSync: no PS category found for Dolibarr category '{}' (id={}), skipping", cat.label(), cat.id()); + return null; + } + CategoryMapping mapping = CategoryMapping.builder() + .dolibarrId(cat.id()) + .prestashopId(psId.get()) + .label(cat.label()) + .build(); + categoryMappingRepository.save(mapping); + log.info("ProductSync: auto-linked Dolibarr category '{}' (id={}) → PS id={}", cat.label(), cat.id(), psId.get()); + return mapping; + } + /** * Pushes Dolibarr stock to the PS stock_available record immediately after product creation. * Skipped when stockReel is null or zero (PS default is already 0).