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;
|
||||
|
||||
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.ProductMappingResponse;
|
||||
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.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.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
|
@ -27,6 +30,7 @@ public class MappingController {
|
|||
|
||||
private final ProductMappingRepository productMappingRepository;
|
||||
private final OrderMappingRepository orderMappingRepository;
|
||||
private final CategoryMappingRepository categoryMappingRepository;
|
||||
private final PrestashopClient prestashopClient;
|
||||
|
||||
@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) {
|
||||
return new OrderMappingResponse(
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 label,
|
||||
String description,
|
||||
/** Public-facing short note; mapped to PS {@code description_short} */
|
||||
@JsonProperty("note_public") String notePublic,
|
||||
String price,
|
||||
/** Current stock level across all warehouses */
|
||||
@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("<description><language id=\"1\">").append(escapeXml(description)).append("</language></description>");
|
||||
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("<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("</associations>");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
|
@ -31,7 +32,9 @@ public record PrestashopProductDto(
|
|||
@JsonDeserialize(using = MultilangStringDeserializer.class) String name,
|
||||
@JsonDeserialize(using = MultilangStringDeserializer.class) String description,
|
||||
@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} */
|
||||
|
|
|
|||
|
|
@ -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.PrestashopStockAvailableDto;
|
||||
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 lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
|
@ -39,6 +40,7 @@ public class ProductSyncService {
|
|||
private final DolibarrClient dolibarrClient;
|
||||
private final PrestashopClient prestashopClient;
|
||||
private final ProductMappingRepository productMappingRepository;
|
||||
private final CategoryMappingRepository categoryMappingRepository;
|
||||
private final SyncLogRepository syncLogRepository;
|
||||
private final IntegrationProperties integrationProperties;
|
||||
|
||||
|
|
@ -128,17 +130,18 @@ public class ProductSyncService {
|
|||
|
||||
private void pushProduct(DolibarrProductDto dolProduct) {
|
||||
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
|
||||
Optional<PrestashopProductDto> existingPs = prestashopClient.getProductByReference(sku);
|
||||
|
||||
if (existingPs.isPresent()) {
|
||||
Integer psId = existingPs.get().id();
|
||||
prestashopClient.updateProduct(psId, toPrestashopDto(dolProduct, psId));
|
||||
prestashopClient.updateProduct(psId, toPrestashopDto(dolProduct, psId, psCategoryIds));
|
||||
upsertMapping(sku, dolProduct.id(), psId);
|
||||
log.debug("ProductSync: updated PS product reference={} id={}", sku, psId);
|
||||
} else {
|
||||
PrestashopProductDto created = prestashopClient.createProduct(toPrestashopDto(dolProduct, null));
|
||||
PrestashopProductDto created = prestashopClient.createProduct(toPrestashopDto(dolProduct, null, psCategoryIds));
|
||||
if (created == null) {
|
||||
throw new PrestashopApiException(
|
||||
"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.
|
||||
* 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
|
||||
*/
|
||||
private PrestashopProductDto toPrestashopDto(DolibarrProductDto src, Integer prestashopId) {
|
||||
String categoryId = String.valueOf(integrationProperties.prestashop().defaultCategoryId());
|
||||
private PrestashopProductDto toPrestashopDto(DolibarrProductDto src, Integer prestashopId, List<String> psCategoryIds) {
|
||||
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 description = src.description() != null ? src.description() : "";
|
||||
String descriptionShort = src.notePublic() != null ? src.notePublic() : "";
|
||||
String active = (src.toSell() != null && src.toSell() == 0) ? "0" : "1";
|
||||
|
||||
return new PrestashopProductDto(
|
||||
|
|
@ -211,10 +234,11 @@ public class ProductSyncService {
|
|||
src.ref(),
|
||||
src.price(),
|
||||
active,
|
||||
categoryId,
|
||||
effectiveCategoryId,
|
||||
label,
|
||||
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