diff --git a/build.gradle b/build.gradle index 3e921fbb0..1aac05a2d 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ plugins { } ext{ - commercetoolsJavaSdkV2Version = '17.18.0' + commercetoolsJavaSdkV2Version = '17.28.0' mockitoJunitJupiterVersion = '5.16.1' jupiterApiVersion = '5.11.3' assertjVersion = '3.26.3' diff --git a/src/integration-test/java/com/commercetools/sync/integration/externalsource/products/ProductSyncIT.java b/src/integration-test/java/com/commercetools/sync/integration/externalsource/products/ProductSyncIT.java index 904f35359..4e1774038 100644 --- a/src/integration-test/java/com/commercetools/sync/integration/externalsource/products/ProductSyncIT.java +++ b/src/integration-test/java/com/commercetools/sync/integration/externalsource/products/ProductSyncIT.java @@ -1015,6 +1015,121 @@ void sync_shouldSyncProductsWithoutAnyVariants() { assertThat(syncStatistics).hasValues(2, 1, 1, 0, 0); } + @Test + void sync_withNoChangesInIntegerAttribute_shouldNotUpdateTheProduct() { + // preparation + final List updateActions = new ArrayList<>(); + final TriConsumer, Optional> + warningCallBack = + (exception, newResource, oldResource) -> + warningCallBackMessages.add(exception.getMessage()); + + final ProductSyncOptions customOptions = + ProductSyncOptionsBuilder.of(TestClientUtils.CTP_TARGET_CLIENT) + .errorCallback( + (exception, oldResource, newResource, actions) -> + collectErrors(exception.getMessage(), exception.getCause())) + .warningCallback(warningCallBack) + .beforeUpdateCallback( + (actions, draft, old) -> { + updateActions.addAll(actions); + return actions; + }) + .build(); + + final ProductDraft productDraft = + createProductDraftBuilder( + PRODUCT_KEY_1_RESOURCE_PATH, + ProductTypeResourceIdentifierBuilder.of().key(productType.getKey()).build()) + .categories(emptyList()) + .taxCategory((TaxCategoryResourceIdentifier) null) + .state((StateResourceIdentifier) null) + .build(); + + // Creating the attribute draft with the changes + final Attribute sortAttrDraft = AttributeBuilder.of().name("sort").value(10).build(); + + // Creating the product variant draft with the product reference attribute + final List attributes = singletonList(sortAttrDraft); + + final ProductVariantDraft masterVariant = + ProductVariantDraftBuilder.of(productDraft.getMasterVariant()) + .attributes(attributes) + .build(); + + final ProductDraft productDraftWithChangedAttributes = + ProductDraftBuilder.of(productDraft).masterVariant(masterVariant).build(); + + // test + final ProductSync productSync = new ProductSync(customOptions); + productSync.sync(singletonList(productDraftWithChangedAttributes)).toCompletableFuture().join(); + final ProductSyncStatistics syncStatistics = + productSync + .sync(singletonList(productDraftWithChangedAttributes)) + .toCompletableFuture() + .join(); + + assertThat(syncStatistics).hasValues(2, 0, 1, 0, 0); + } + + @Test + void sync_withNoChangesInLenumAttribute_shouldNotUpdateTheProduct() { + // preparation + final List updateActions = new ArrayList<>(); + final TriConsumer, Optional> + warningCallBack = + (exception, newResource, oldResource) -> + warningCallBackMessages.add(exception.getMessage()); + + final ProductSyncOptions customOptions = + ProductSyncOptionsBuilder.of(TestClientUtils.CTP_TARGET_CLIENT) + .errorCallback( + (exception, oldResource, newResource, actions) -> + collectErrors(exception.getMessage(), exception.getCause())) + .warningCallback(warningCallBack) + .beforeUpdateCallback( + (actions, draft, old) -> { + updateActions.addAll(actions); + return actions; + }) + .build(); + + final ProductDraft productDraft = + createProductDraftBuilder( + PRODUCT_KEY_1_RESOURCE_PATH, + ProductTypeResourceIdentifierBuilder.of().key(productType.getKey()).build()) + .categories(emptyList()) + .taxCategory((TaxCategoryResourceIdentifier) null) + .state((StateResourceIdentifier) null) + .build(); + + // Creating the attribute draft with the changes + final Attribute technologyAttrDraft = + AttributeBuilder.of().name("technology").value("laser").build(); + + // Creating the product variant draft with the product reference attribute + final List attributes = singletonList(technologyAttrDraft); + + final ProductVariantDraft masterVariant = + ProductVariantDraftBuilder.of(productDraft.getMasterVariant()) + .attributes(attributes) + .build(); + + final ProductDraft productDraftWithChangedAttributes = + ProductDraftBuilder.of(productDraft).masterVariant(masterVariant).build(); + + // test + final ProductSync productSync = new ProductSync(customOptions); + productSync.sync(singletonList(productDraftWithChangedAttributes)).toCompletableFuture().join(); + final ProductSyncStatistics syncStatistics = + productSync + .sync(singletonList(productDraftWithChangedAttributes)) + .toCompletableFuture() + .join(); + + assertThat(syncStatistics).hasValues(2, 0, 1, 0, 0); + } + @Test void sync_withProductContainingAttributeChanges_shouldSyncProductCorrectly() { // preparation diff --git a/src/main/java/com/commercetools/sync/products/utils/AttributeUtils.java b/src/main/java/com/commercetools/sync/products/utils/AttributeUtils.java index 0275cd586..08b37be9c 100644 --- a/src/main/java/com/commercetools/sync/products/utils/AttributeUtils.java +++ b/src/main/java/com/commercetools/sync/products/utils/AttributeUtils.java @@ -20,8 +20,7 @@ public final class AttributeUtils { * converted value in the attribute. * * @param attribute - Attribute to replace it's value with a JSON representation - * @return - a {@link JsonNode} representing the attribute's value. extracted from the given - * attribute or empty list if the attribute * doesn't contain reference types. + * @return - a {@link JsonNode} representing the attribute's value. */ @Nonnull public static JsonNode replaceAttributeValueWithJsonAndReturnValue( diff --git a/src/main/java/com/commercetools/sync/products/utils/ProductUpdateActionUtils.java b/src/main/java/com/commercetools/sync/products/utils/ProductUpdateActionUtils.java index 9ff076ff7..32b9c34fb 100644 --- a/src/main/java/com/commercetools/sync/products/utils/ProductUpdateActionUtils.java +++ b/src/main/java/com/commercetools/sync/products/utils/ProductUpdateActionUtils.java @@ -52,6 +52,7 @@ import com.commercetools.api.models.product.ProductUpdateAction; import com.commercetools.api.models.product.ProductVariant; import com.commercetools.api.models.product.ProductVariantDraft; +import com.commercetools.api.models.product.SearchKeyword; import com.commercetools.api.models.product.SearchKeywords; import com.commercetools.api.models.state.State; import com.commercetools.api.models.state.StateResourceIdentifier; @@ -337,14 +338,23 @@ public static Optional buildSetSearchKeywordsUpdateAction( if (newSearchKeywords == null) { return Optional.empty(); } else { - return buildUpdateAction( - oldSearchKeywords, - newSearchKeywords, - () -> - ProductSetSearchKeywordsActionBuilder.of() - .searchKeywords(newSearchKeywords) - .staged(true) - .build()); + // For some reasons, values for searchKeywords could be null or {} since Java SDK v17.28.0 + // Even though they mean the same thing, they are not equals and would trigger update action + // Thus I had to do a manual comparison here. + final Map> newSearchValues = newSearchKeywords.values(); + final Map> oldSearchValue = oldSearchKeywords.values(); + if (newSearchValues == null && (oldSearchValue == null || oldSearchValue.size() == 0)) { + return Optional.empty(); + } else { + return buildUpdateAction( + oldSearchKeywords, + newSearchKeywords, + () -> + ProductSetSearchKeywordsActionBuilder.of() + .searchKeywords(newSearchKeywords) + .staged(true) + .build()); + } } } diff --git a/src/main/java/com/commercetools/sync/products/utils/ProductVariantAttributeUpdateActionUtils.java b/src/main/java/com/commercetools/sync/products/utils/ProductVariantAttributeUpdateActionUtils.java index bc32fa1bc..6915ad6a0 100644 --- a/src/main/java/com/commercetools/sync/products/utils/ProductVariantAttributeUpdateActionUtils.java +++ b/src/main/java/com/commercetools/sync/products/utils/ProductVariantAttributeUpdateActionUtils.java @@ -1,6 +1,5 @@ package com.commercetools.sync.products.utils; -import static com.commercetools.sync.commons.utils.CommonTypeUpdateActionUtils.buildUpdateAction; import static java.lang.String.format; import com.commercetools.api.models.product.Attribute; @@ -10,12 +9,17 @@ import com.commercetools.api.models.product.ProductSetAttributeInAllVariantsActionBuilder; import com.commercetools.api.models.product.ProductUpdateAction; import com.commercetools.sync.commons.exceptions.BuildUpdateActionException; +import com.commercetools.sync.commons.utils.CommonTypeUpdateActionUtils; import com.commercetools.sync.products.AttributeMetaData; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import io.vrap.rmf.base.client.utils.json.JsonUtils; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.function.Supplier; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -93,5 +97,22 @@ public static Optional buildProductVariantAttributeUpdateAc .build()); } + @Nonnull + private static Optional buildUpdateAction( + final JsonNode oldAttributeValueAsJson, + final JsonNode newAttributeValueAsJson, + final Supplier actionSupplier) { + if (oldAttributeValueAsJson instanceof ObjectNode + && newAttributeValueAsJson instanceof TextNode) { + String oldKey = oldAttributeValueAsJson.get("key").asText(); + String newKey = newAttributeValueAsJson.asText(); + return !Objects.equals(oldKey, newKey) + ? Optional.ofNullable(actionSupplier.get()) + : Optional.empty(); + } + return CommonTypeUpdateActionUtils.buildUpdateAction( + oldAttributeValueAsJson, newAttributeValueAsJson, actionSupplier); + } + private ProductVariantAttributeUpdateActionUtils() {} } diff --git a/src/test/resources/product-type.json b/src/test/resources/product-type.json index 2cca02d3d..037464067 100644 --- a/src/test/resources/product-type.json +++ b/src/test/resources/product-type.json @@ -6,6 +6,40 @@ "description": "all fleisch", "classifier": "Complex", "attributes": [ + { + "name": "technology", + "label": { + "en": "technology" + }, + "isRequired": false, + "type": { + "name": "lenum", + "values": [ + { + "key": "laser", + "label": { + "en": "Laser" + } + }, + { + "key": "plasma", + "label": { + "en": "Plasma" + } + }, + { + "key": "waterjet", + "label": { + "en": "Waterjet" + } + } + ] + }, + "attributeConstraint": "None", + "isSearchable": false, + "inputHint": "SingleLine", + "displayGroup": "Other" + }, { "name": "priceInfo", "label": {