product descriptions and etc.
This commit is contained in:
parent
a719e83ce7
commit
7aa6e7f767
|
|
@ -1,10 +1,13 @@
|
||||||
package com.teterialosjuanjos.tfg.sync_service.api.controller;
|
package com.teterialosjuanjos.tfg.sync_service.api.controller;
|
||||||
|
|
||||||
|
import com.teterialosjuanjos.tfg.sync_service.api.dto.CategoryMappingResponse;
|
||||||
import com.teterialosjuanjos.tfg.sync_service.api.dto.OrderMappingResponse;
|
import com.teterialosjuanjos.tfg.sync_service.api.dto.OrderMappingResponse;
|
||||||
import com.teterialosjuanjos.tfg.sync_service.api.dto.ProductMappingResponse;
|
import com.teterialosjuanjos.tfg.sync_service.api.dto.ProductMappingResponse;
|
||||||
import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.PrestashopClient;
|
import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.PrestashopClient;
|
||||||
import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.exception.PrestashopApiException;
|
import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.exception.PrestashopApiException;
|
||||||
import com.teterialosjuanjos.tfg.sync_service.mapping.*;
|
import com.teterialosjuanjos.tfg.sync_service.mapping.*;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
@ -27,6 +30,7 @@ public class MappingController {
|
||||||
|
|
||||||
private final ProductMappingRepository productMappingRepository;
|
private final ProductMappingRepository productMappingRepository;
|
||||||
private final OrderMappingRepository orderMappingRepository;
|
private final OrderMappingRepository orderMappingRepository;
|
||||||
|
private final CategoryMappingRepository categoryMappingRepository;
|
||||||
private final PrestashopClient prestashopClient;
|
private final PrestashopClient prestashopClient;
|
||||||
|
|
||||||
@Operation(summary = "List product mappings, optionally filtered by sync status")
|
@Operation(summary = "List product mappings, optionally filtered by sync status")
|
||||||
|
|
@ -72,6 +76,41 @@ public class MappingController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Category mappings ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Operation(summary = "List all Dolibarr ↔ PrestaShop category mappings")
|
||||||
|
@GetMapping("/categories")
|
||||||
|
public List<CategoryMappingResponse> getCategoryMappings() {
|
||||||
|
return categoryMappingRepository.findAll().stream()
|
||||||
|
.map(MappingController::toCategoryResponse).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Create a category mapping (dolibarrId ↔ prestashopId)")
|
||||||
|
@PostMapping("/categories")
|
||||||
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
|
public CategoryMappingResponse createCategoryMapping(@RequestBody CategoryMappingResponse request) {
|
||||||
|
CategoryMapping mapping = CategoryMapping.builder()
|
||||||
|
.dolibarrId(request.dolibarrId())
|
||||||
|
.prestashopId(request.prestashopId())
|
||||||
|
.label(request.label())
|
||||||
|
.build();
|
||||||
|
return toCategoryResponse(categoryMappingRepository.save(mapping));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "Delete a category mapping by id")
|
||||||
|
@DeleteMapping("/categories/{id}")
|
||||||
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
|
public void deleteCategoryMapping(@PathVariable Long id) {
|
||||||
|
if (!categoryMappingRepository.existsById(id)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Category mapping not found: " + id);
|
||||||
|
}
|
||||||
|
categoryMappingRepository.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CategoryMappingResponse toCategoryResponse(CategoryMapping m) {
|
||||||
|
return new CategoryMappingResponse(m.getId(), m.getDolibarrId(), m.getPrestashopId(), m.getLabel());
|
||||||
|
}
|
||||||
|
|
||||||
private static OrderMappingResponse toOrderResponse(OrderMapping m) {
|
private static OrderMappingResponse toOrderResponse(OrderMapping m) {
|
||||||
return new OrderMappingResponse(
|
return new OrderMappingResponse(
|
||||||
m.getId(), m.getPrestashopOrderId(), m.getDolibarrOrderId(),
|
m.getId(), m.getPrestashopOrderId(), m.getDolibarrOrderId(),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.teterialosjuanjos.tfg.sync_service.api.dto;
|
||||||
|
|
||||||
|
public record CategoryMappingResponse(
|
||||||
|
Long id,
|
||||||
|
Integer dolibarrId,
|
||||||
|
Integer prestashopId,
|
||||||
|
String label
|
||||||
|
) {}
|
||||||
|
|
@ -115,6 +115,23 @@ public class DolibarrClient {
|
||||||
.body(DolibarrProductDto.class);
|
.body(DolibarrProductDto.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the categories linked to a product.
|
||||||
|
* Returns an empty list if the product has no categories or the endpoint returns 404.
|
||||||
|
*/
|
||||||
|
public List<DolibarrCategoryDto> getProductCategories(Integer productId) {
|
||||||
|
try {
|
||||||
|
List<DolibarrCategoryDto> cats = restClient.get()
|
||||||
|
.uri("/products/{id}/categories", productId)
|
||||||
|
.retrieve()
|
||||||
|
.body(new ParameterizedTypeReference<List<DolibarrCategoryDto>>() {});
|
||||||
|
return cats != null ? cats : List.of();
|
||||||
|
} catch (DolibarrApiException e) {
|
||||||
|
if (e.getStatusCode().value() == 404) return List.of();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Thirdparties (customers) ──────────────────────────────────────────
|
// ── Thirdparties (customers) ──────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record DolibarrCategoryDto(Integer id, String label) {}
|
||||||
|
|
@ -17,6 +17,8 @@ public record DolibarrProductDto(
|
||||||
String ref,
|
String ref,
|
||||||
String label,
|
String label,
|
||||||
String description,
|
String description,
|
||||||
|
/** Public-facing short note; mapped to PS {@code description_short} */
|
||||||
|
@JsonProperty("note_public") String notePublic,
|
||||||
String price,
|
String price,
|
||||||
/** Current stock level across all warehouses */
|
/** Current stock level across all warehouses */
|
||||||
@JsonProperty("stock_reel") Double stockReel,
|
@JsonProperty("stock_reel") Double stockReel,
|
||||||
|
|
|
||||||
|
|
@ -316,10 +316,15 @@ public class PrestashopClient {
|
||||||
sb.append("<name><language id=\"1\">").append(escapeXml(name)).append("</language></name>");
|
sb.append("<name><language id=\"1\">").append(escapeXml(name)).append("</language></name>");
|
||||||
sb.append("<description><language id=\"1\">").append(escapeXml(description)).append("</language></description>");
|
sb.append("<description><language id=\"1\">").append(escapeXml(description)).append("</language></description>");
|
||||||
sb.append("<description_short><language id=\"1\">").append(escapeXml(descShort)).append("</language></description_short>");
|
sb.append("<description_short><language id=\"1\">").append(escapeXml(descShort)).append("</language></description_short>");
|
||||||
if (dto.idCategoryDefault() != null) {
|
List<String> cats = (dto.categoryIds() != null && !dto.categoryIds().isEmpty())
|
||||||
|
? dto.categoryIds()
|
||||||
|
: (dto.idCategoryDefault() != null ? List.of(dto.idCategoryDefault()) : List.of());
|
||||||
|
if (!cats.isEmpty()) {
|
||||||
sb.append("<associations>");
|
sb.append("<associations>");
|
||||||
sb.append("<categories nodeType=\"category\" api=\"categories\">");
|
sb.append("<categories nodeType=\"category\" api=\"categories\">");
|
||||||
sb.append("<category><id>").append(escapeXml(dto.idCategoryDefault())).append("</id></category>");
|
for (String catId : cats) {
|
||||||
|
sb.append("<category><id>").append(escapeXml(catId)).append("</id></category>");
|
||||||
|
}
|
||||||
sb.append("</categories>");
|
sb.append("</categories>");
|
||||||
sb.append("</associations>");
|
sb.append("</associations>");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.teterialosjuanjos.tfg.sync_service.integration.prestashop.dto;
|
package com.teterialosjuanjos.tfg.sync_service.integration.prestashop.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
@ -31,7 +32,9 @@ public record PrestashopProductDto(
|
||||||
@JsonDeserialize(using = MultilangStringDeserializer.class) String name,
|
@JsonDeserialize(using = MultilangStringDeserializer.class) String name,
|
||||||
@JsonDeserialize(using = MultilangStringDeserializer.class) String description,
|
@JsonDeserialize(using = MultilangStringDeserializer.class) String description,
|
||||||
@JsonProperty("description_short")
|
@JsonProperty("description_short")
|
||||||
@JsonDeserialize(using = MultilangStringDeserializer.class) String descriptionShort
|
@JsonDeserialize(using = MultilangStringDeserializer.class) String descriptionShort,
|
||||||
|
/** PS category IDs to set on write; not present in PS JSON responses (ignored on read). */
|
||||||
|
@JsonIgnore List<String> categoryIds
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/** Envelope for {@code GET /api/products?display=full} */
|
/** Envelope for {@code GET /api/products?display=full} */
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.teterialosjuanjos.tfg.sync_service.mapping;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a Dolibarr product category ID to its PrestaShop counterpart.
|
||||||
|
* Must be populated manually before category sync will work.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "category_mapping")
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class CategoryMapping {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "dolibarr_id", nullable = false, unique = true)
|
||||||
|
private Integer dolibarrId;
|
||||||
|
|
||||||
|
@Column(name = "prestashop_id", nullable = false)
|
||||||
|
private Integer prestashopId;
|
||||||
|
|
||||||
|
@Column(length = 255)
|
||||||
|
private String label;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.teterialosjuanjos.tfg.sync_service.mapping;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface CategoryMappingRepository extends JpaRepository<CategoryMapping, Long> {
|
||||||
|
|
||||||
|
Optional<CategoryMapping> findByDolibarrId(Integer dolibarrId);
|
||||||
|
|
||||||
|
List<CategoryMapping> findByDolibarrIdIn(List<Integer> dolibarrIds);
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.PrestashopC
|
||||||
import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.dto.PrestashopProductDto;
|
import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.dto.PrestashopProductDto;
|
||||||
import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.dto.PrestashopStockAvailableDto;
|
import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.dto.PrestashopStockAvailableDto;
|
||||||
import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.exception.PrestashopApiException;
|
import com.teterialosjuanjos.tfg.sync_service.integration.prestashop.exception.PrestashopApiException;
|
||||||
|
import com.teterialosjuanjos.tfg.sync_service.integration.dolibarr.dto.DolibarrCategoryDto;
|
||||||
import com.teterialosjuanjos.tfg.sync_service.mapping.*;
|
import com.teterialosjuanjos.tfg.sync_service.mapping.*;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
@ -39,6 +40,7 @@ public class ProductSyncService {
|
||||||
private final DolibarrClient dolibarrClient;
|
private final DolibarrClient dolibarrClient;
|
||||||
private final PrestashopClient prestashopClient;
|
private final PrestashopClient prestashopClient;
|
||||||
private final ProductMappingRepository productMappingRepository;
|
private final ProductMappingRepository productMappingRepository;
|
||||||
|
private final CategoryMappingRepository categoryMappingRepository;
|
||||||
private final SyncLogRepository syncLogRepository;
|
private final SyncLogRepository syncLogRepository;
|
||||||
private final IntegrationProperties integrationProperties;
|
private final IntegrationProperties integrationProperties;
|
||||||
|
|
||||||
|
|
@ -128,17 +130,18 @@ public class ProductSyncService {
|
||||||
|
|
||||||
private void pushProduct(DolibarrProductDto dolProduct) {
|
private void pushProduct(DolibarrProductDto dolProduct) {
|
||||||
String sku = dolProduct.ref();
|
String sku = dolProduct.ref();
|
||||||
|
List<String> psCategoryIds = resolveCategories(dolProduct.id());
|
||||||
|
|
||||||
// PS is source of truth: local mapping may be stale if PS products were deleted
|
// PS is source of truth: local mapping may be stale if PS products were deleted
|
||||||
Optional<PrestashopProductDto> existingPs = prestashopClient.getProductByReference(sku);
|
Optional<PrestashopProductDto> existingPs = prestashopClient.getProductByReference(sku);
|
||||||
|
|
||||||
if (existingPs.isPresent()) {
|
if (existingPs.isPresent()) {
|
||||||
Integer psId = existingPs.get().id();
|
Integer psId = existingPs.get().id();
|
||||||
prestashopClient.updateProduct(psId, toPrestashopDto(dolProduct, psId));
|
prestashopClient.updateProduct(psId, toPrestashopDto(dolProduct, psId, psCategoryIds));
|
||||||
upsertMapping(sku, dolProduct.id(), psId);
|
upsertMapping(sku, dolProduct.id(), psId);
|
||||||
log.debug("ProductSync: updated PS product reference={} id={}", sku, psId);
|
log.debug("ProductSync: updated PS product reference={} id={}", sku, psId);
|
||||||
} else {
|
} else {
|
||||||
PrestashopProductDto created = prestashopClient.createProduct(toPrestashopDto(dolProduct, null));
|
PrestashopProductDto created = prestashopClient.createProduct(toPrestashopDto(dolProduct, null, psCategoryIds));
|
||||||
if (created == null) {
|
if (created == null) {
|
||||||
throw new PrestashopApiException(
|
throw new PrestashopApiException(
|
||||||
"Product created in PS but not found via GET (reference=" + sku + ")");
|
"Product created in PS but not found via GET (reference=" + sku + ")");
|
||||||
|
|
@ -149,6 +152,20 @@ public class ProductSyncService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> resolveCategories(Integer dolibarrProductId) {
|
||||||
|
try {
|
||||||
|
List<DolibarrCategoryDto> dolibarrCats = dolibarrClient.getProductCategories(dolibarrProductId);
|
||||||
|
if (dolibarrCats.isEmpty()) return List.of();
|
||||||
|
List<Integer> dolibarrIds = dolibarrCats.stream().map(DolibarrCategoryDto::id).toList();
|
||||||
|
return categoryMappingRepository.findByDolibarrIdIn(dolibarrIds).stream()
|
||||||
|
.map(m -> String.valueOf(m.getPrestashopId()))
|
||||||
|
.toList();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("ProductSync: could not resolve categories for Dolibarr product id={}: {}", dolibarrProductId, e.getMessage());
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pushes Dolibarr stock to the PS stock_available record immediately after product creation.
|
* 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).
|
* Skipped when stockReel is null or zero (PS default is already 0).
|
||||||
|
|
@ -200,10 +217,16 @@ public class ProductSyncService {
|
||||||
*
|
*
|
||||||
* @param prestashopId null for create, actual PS ID for update
|
* @param prestashopId null for create, actual PS ID for update
|
||||||
*/
|
*/
|
||||||
private PrestashopProductDto toPrestashopDto(DolibarrProductDto src, Integer prestashopId) {
|
private PrestashopProductDto toPrestashopDto(DolibarrProductDto src, Integer prestashopId, List<String> psCategoryIds) {
|
||||||
String categoryId = String.valueOf(integrationProperties.prestashop().defaultCategoryId());
|
String defaultCategoryId = String.valueOf(integrationProperties.prestashop().defaultCategoryId());
|
||||||
|
String effectiveCategoryId = (psCategoryIds != null && !psCategoryIds.isEmpty())
|
||||||
|
? psCategoryIds.get(0) : defaultCategoryId;
|
||||||
|
List<String> allCategoryIds = (psCategoryIds != null && !psCategoryIds.isEmpty())
|
||||||
|
? psCategoryIds : List.of(defaultCategoryId);
|
||||||
|
|
||||||
String label = src.label() != null ? src.label() : "";
|
String label = src.label() != null ? src.label() : "";
|
||||||
String description = src.description() != null ? src.description() : "";
|
String description = src.description() != null ? src.description() : "";
|
||||||
|
String descriptionShort = src.notePublic() != null ? src.notePublic() : "";
|
||||||
String active = (src.toSell() != null && src.toSell() == 0) ? "0" : "1";
|
String active = (src.toSell() != null && src.toSell() == 0) ? "0" : "1";
|
||||||
|
|
||||||
return new PrestashopProductDto(
|
return new PrestashopProductDto(
|
||||||
|
|
@ -211,10 +234,11 @@ public class ProductSyncService {
|
||||||
src.ref(),
|
src.ref(),
|
||||||
src.price(),
|
src.price(),
|
||||||
active,
|
active,
|
||||||
categoryId,
|
effectiveCategoryId,
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
""
|
descriptionShort,
|
||||||
|
allCategoryIds
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
CREATE TABLE category_mapping (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
dolibarr_id INT NOT NULL UNIQUE,
|
||||||
|
prestashop_id INT NOT NULL,
|
||||||
|
label VARCHAR(255)
|
||||||
|
);
|
||||||
Loading…
Reference in New Issue