From 7884fe96e18b344cdf8ec23b76fcac5384555459 Mon Sep 17 00:00:00 2001 From: Lam Tran Date: Thu, 9 Jun 2022 10:28:54 +0200 Subject: [PATCH] #363 remove discounted prices from syncing (#382) --- README.md | 34 ++-- config/spotbugs-exclude.xml | 5 + .../sync/ProductSyncWithDiscountedPrice.java | 177 ++++++++++++++++++ .../sync/util/IntegrationTestUtils.java | 11 +- .../project/sync/product/ProductSyncer.java | 54 +++++- .../sync/product/ProductSyncerTest.java | 38 ++++ src/test/resources/product-key-10.json | 89 +++++++++ 7 files changed, 389 insertions(+), 19 deletions(-) create mode 100644 src/integration-test/java/com/commercetools/project/sync/ProductSyncWithDiscountedPrice.java create mode 100644 src/test/resources/product-key-10.json diff --git a/README.md b/README.md index 080a46aa..dba4533d 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ Example: ##### Download ```bash -docker pull commercetools/commercetools-project-sync:5.1.2 +docker pull commercetools/commercetools-project-sync:5.1.3 ``` ##### Run @@ -215,14 +215,14 @@ docker run \ -e TARGET_PROJECT_KEY=xxxx \ -e TARGET_CLIENT_ID=xxxx \ -e TARGET_CLIENT_SECRET=xxxx \ -commercetools/commercetools-project-sync:5.1.2 -s all +commercetools/commercetools-project-sync:5.1.3 -s all ``` ### Examples - To run the all sync modules from a source project to a target project ```bash - docker run commercetools/commercetools-project-sync:5.1.2 -s all + docker run commercetools/commercetools-project-sync:5.1.3 -s all ``` This will run the following sync modules in the given order: 1. `Type` Sync and `ProductType` Sync and `States` Sync and `TaxCategory` Sync and `CustomObject` Sync in parallel. @@ -232,68 +232,68 @@ commercetools/commercetools-project-sync:5.1.2 -s all - To run the type sync ```bash - docker run commercetools/commercetools-project-sync:5.1.2 -s types + docker run commercetools/commercetools-project-sync:5.1.3 -s types ``` - To run the productType sync ```bash - docker run commercetools/commercetools-project-sync:5.1.2 -s productTypes + docker run commercetools/commercetools-project-sync:5.1.3 -s productTypes ``` - To run the states sync ```bash - docker run commercetools/commercetools-project-sync:5.1.2 -s states + docker run commercetools/commercetools-project-sync:5.1.3 -s states ``` - To run the taxCategory sync ```bash - docker run commercetools/commercetools-project-sync:5.1.2 -s taxCategories + docker run commercetools/commercetools-project-sync:5.1.3 -s taxCategories ``` - To run the category sync ```bash - docker run commercetools/commercetools-project-sync:5.1.2 -s categories + docker run commercetools/commercetools-project-sync:5.1.3 -s categories ``` - To run the product sync ```bash - docker run commercetools/commercetools-project-sync:5.1.2 -s products + docker run commercetools/commercetools-project-sync:5.1.3 -s products ``` - To run the cartDiscount sync ```bash - docker run commercetools/commercetools-project-sync:5.1.2 -s cartDiscounts + docker run commercetools/commercetools-project-sync:5.1.3 -s cartDiscounts ``` - To run the inventoryEntry sync ```bash - docker run commercetools/commercetools-project-sync:5.1.2 -s inventoryEntries + docker run commercetools/commercetools-project-sync:5.1.3 -s inventoryEntries ``` - To run the customObject sync ```bash - docker run commercetools/commercetools-project-sync:5.1.2 -s customObjects + docker run commercetools/commercetools-project-sync:5.1.3 -s customObjects ``` - To run the customer sync ```bash - docker run commercetools/commercetools-project-sync:5.1.2 -s customers + docker run commercetools/commercetools-project-sync:5.1.3 -s customers ``` - To run the shoppingList sync ```bash - docker run commercetools/commercetools-project-sync:5.1.2 -s shoppingLists + docker run commercetools/commercetools-project-sync:5.1.3 -s shoppingLists ``` - To run both products and shoppingList sync ```bash - docker run commercetools/commercetools-project-sync:5.1.2 -s products shoppingLists + docker run commercetools/commercetools-project-sync:5.1.3 -s products shoppingLists ``` - To run type, productType and shoppingList sync ```bash - docker run commercetools/commercetools-project-sync:5.1.2 -s types productTypes shoppingLists + docker run commercetools/commercetools-project-sync:5.1.3 -s types productTypes shoppingLists ``` - To run all sync modules using a runner name ```bash - docker run commercetools/commercetools-project-sync:5.1.2 -s all -r myRunnerName + docker run commercetools/commercetools-project-sync:5.1.3 -s all -r myRunnerName ``` diff --git a/config/spotbugs-exclude.xml b/config/spotbugs-exclude.xml index e8d2c408..aaef4e40 100644 --- a/config/spotbugs-exclude.xml +++ b/config/spotbugs-exclude.xml @@ -24,4 +24,9 @@ + + + + + diff --git a/src/integration-test/java/com/commercetools/project/sync/ProductSyncWithDiscountedPrice.java b/src/integration-test/java/com/commercetools/project/sync/ProductSyncWithDiscountedPrice.java new file mode 100644 index 00000000..85516e6b --- /dev/null +++ b/src/integration-test/java/com/commercetools/project/sync/ProductSyncWithDiscountedPrice.java @@ -0,0 +1,177 @@ +package com.commercetools.project.sync; + +import static com.commercetools.project.sync.util.IntegrationTestUtils.cleanUpProjects; +import static com.commercetools.project.sync.util.IntegrationTestUtils.createITSyncerFactory; +import static com.commercetools.project.sync.util.SphereClientUtils.CTP_SOURCE_CLIENT; +import static com.commercetools.project.sync.util.SphereClientUtils.CTP_TARGET_CLIENT; +import static com.neovisionaries.i18n.CountryCode.DE; +import static io.sphere.sdk.models.DefaultCurrencyUnits.EUR; +import static io.sphere.sdk.models.LocalizedString.ofEnglish; +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; + +import com.neovisionaries.i18n.CountryCode; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.models.Reference; +import io.sphere.sdk.productdiscounts.DiscountedPrice; +import io.sphere.sdk.productdiscounts.ProductDiscount; +import io.sphere.sdk.productdiscounts.ProductDiscountDraft; +import io.sphere.sdk.productdiscounts.ProductDiscountDraftBuilder; +import io.sphere.sdk.productdiscounts.ProductDiscountValue; +import io.sphere.sdk.productdiscounts.commands.ProductDiscountCreateCommand; +import io.sphere.sdk.products.Price; +import io.sphere.sdk.products.PriceDraft; +import io.sphere.sdk.products.PriceDraftBuilder; +import io.sphere.sdk.products.Product; +import io.sphere.sdk.products.ProductDraft; +import io.sphere.sdk.products.ProductDraftBuilder; +import io.sphere.sdk.products.ProductVariant; +import io.sphere.sdk.products.ProductVariantDraft; +import io.sphere.sdk.products.ProductVariantDraftBuilder; +import io.sphere.sdk.products.commands.ProductCreateCommand; +import io.sphere.sdk.products.queries.ProductQuery; +import io.sphere.sdk.producttypes.ProductType; +import io.sphere.sdk.producttypes.ProductTypeDraft; +import io.sphere.sdk.producttypes.ProductTypeDraftBuilder; +import io.sphere.sdk.producttypes.commands.ProductTypeCreateCommand; +import io.sphere.sdk.queries.PagedQueryResult; +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.money.CurrencyUnit; +import org.javamoney.moneta.FastMoney; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import uk.org.lidalia.slf4jext.Level; +import uk.org.lidalia.slf4jtest.TestLogger; +import uk.org.lidalia.slf4jtest.TestLoggerFactory; + +// This will suppress MoreThanOneLogger warnings in this class +@SuppressWarnings("PMD.MoreThanOneLogger") +class ProductSyncWithDiscountedPrice { + private static final TestLogger cliRunnerTestLogger = + TestLoggerFactory.getTestLogger(CliRunner.class); + + private static final String MAIN_PRODUCT_TYPE_KEY = "main-product-type"; + private static final String MAIN_PRODUCT_MASTER_VARIANT_KEY = "main-product-master-variant-key"; + private static final String MAIN_PRODUCT_KEY = "product-with-references"; + private static final FastMoney TEN_EUR = FastMoney.of(10, EUR); + + @BeforeEach + void setup() { + cliRunnerTestLogger.clearAll(); + + ProductDiscountDraft productDiscountDraft = + ProductDiscountDraftBuilder.of() + .value(ProductDiscountValue.ofExternal()) + .name(ofEnglish("testProductDiscount")) + .predicate("1=1") + .sortOrder("0.9") + .isActive(true) + .build(); + ProductDiscount productDiscount = + CTP_TARGET_CLIENT + .execute(ProductDiscountCreateCommand.of(productDiscountDraft)) + .toCompletableFuture() + .join(); + setupProjectData(CTP_SOURCE_CLIENT, null); + setupProjectData(CTP_TARGET_CLIENT, productDiscount.getId()); + } + + static void setupProjectData(@Nonnull final SphereClient sphereClient, String productDiscountId) { + final ProductTypeDraft productTypeDraft = + ProductTypeDraftBuilder.of( + MAIN_PRODUCT_TYPE_KEY, + MAIN_PRODUCT_TYPE_KEY, + "a productType for t-shirts", + emptyList()) + .build(); + + final ProductType productType = + sphereClient + .execute(ProductTypeCreateCommand.of(productTypeDraft)) + .toCompletableFuture() + .join(); + + final PriceDraft priceDraft = + PriceDraftBuilder.of( + getPriceDraft(BigDecimal.valueOf(222), EUR, DE, null, null, productDiscountId)) + .build(); + + final ProductVariantDraft masterVariant = + ProductVariantDraftBuilder.of() + .key(MAIN_PRODUCT_MASTER_VARIANT_KEY) + .sku(MAIN_PRODUCT_MASTER_VARIANT_KEY) + .prices(priceDraft) + .build(); + + final ProductDraft draft = + ProductDraftBuilder.of( + productType, + ofEnglish(MAIN_PRODUCT_KEY), + ofEnglish(MAIN_PRODUCT_KEY), + masterVariant) + .key(MAIN_PRODUCT_KEY) + .build(); + + sphereClient.execute(ProductCreateCommand.of(draft)).toCompletableFuture().join(); + } + + @AfterAll + static void tearDownSuite() { + cleanUpProjects(CTP_SOURCE_CLIENT, CTP_TARGET_CLIENT); + } + + @Test + void run_WhenTargetProductHasDiscountedPrice_ShouldNotRemoveIt() { + // test + CliRunner.of() + .run(new String[] {"-s", "products", "-r", "runnerName", "-f"}, createITSyncerFactory()); + + // assertions + assertThat(cliRunnerTestLogger.getAllLoggingEvents()) + .allMatch(loggingEvent -> !Level.ERROR.equals(loggingEvent.getLevel())); + + final PagedQueryResult productQueryResult = + CTP_TARGET_CLIENT.execute(ProductQuery.of()).toCompletableFuture().join(); + + assertThat(productQueryResult.getResults()) + .hasSize(1) + .singleElement() + .satisfies( + product -> { + final ProductVariant stagedMasterVariant = + product.getMasterData().getStaged().getMasterVariant(); + assertThat(stagedMasterVariant.getPrices()) + .satisfies( + prices -> { + Price price = prices.get(0); + assertThat(price.getDiscounted()).isNotNull(); + assertThat(price.getDiscounted().getValue()).isEqualTo(TEN_EUR); + }); + }); + } + + @Nonnull + public static PriceDraft getPriceDraft( + @Nonnull final BigDecimal value, + @Nonnull final CurrencyUnit currencyUnits, + @Nullable final CountryCode countryCode, + @Nullable final ZonedDateTime validFrom, + @Nullable final ZonedDateTime validUntil, + @Nullable final String productDiscountReferenceId) { + DiscountedPrice discounted = null; + if (productDiscountReferenceId != null) { + discounted = + DiscountedPrice.of(TEN_EUR, Reference.of("product-discount", productDiscountReferenceId)); + } + return PriceDraftBuilder.of(Price.of(value, currencyUnits)) + .country(countryCode) + .validFrom(validFrom) + .validUntil(validUntil) + .discounted(discounted) + .build(); + } +} diff --git a/src/integration-test/java/com/commercetools/project/sync/util/IntegrationTestUtils.java b/src/integration-test/java/com/commercetools/project/sync/util/IntegrationTestUtils.java index 9028d2a4..014e81a9 100644 --- a/src/integration-test/java/com/commercetools/project/sync/util/IntegrationTestUtils.java +++ b/src/integration-test/java/com/commercetools/project/sync/util/IntegrationTestUtils.java @@ -35,6 +35,8 @@ import io.sphere.sdk.inventory.commands.InventoryEntryDeleteCommand; import io.sphere.sdk.inventory.queries.InventoryEntryQuery; import io.sphere.sdk.models.Versioned; +import io.sphere.sdk.productdiscounts.commands.ProductDiscountDeleteCommand; +import io.sphere.sdk.productdiscounts.queries.ProductDiscountQuery; import io.sphere.sdk.products.Product; import io.sphere.sdk.products.ProductVariant; import io.sphere.sdk.products.commands.ProductDeleteCommand; @@ -172,8 +174,15 @@ private static void deleteProjectData(@Nonnull final SphereClient client) { final CompletableFuture deleteCustomObject = queryAndExecute( client, CustomObjectQuery.ofJsonNode(), CustomObjectDeleteCommand::ofJsonNode); + final CompletableFuture deleteProductDiscount = + queryAndExecute(client, ProductDiscountQuery.of(), ProductDiscountDeleteCommand::of); - CompletableFuture.allOf(deleteProduct, deleteInventory, deleteCartDiscount, deleteCustomObject) + CompletableFuture.allOf( + deleteProduct, + deleteInventory, + deleteCartDiscount, + deleteCustomObject, + deleteProductDiscount) .join(); queryAndExecute( diff --git a/src/main/java/com/commercetools/project/sync/product/ProductSyncer.java b/src/main/java/com/commercetools/project/sync/product/ProductSyncer.java index 98e2e190..a0983eb3 100644 --- a/src/main/java/com/commercetools/project/sync/product/ProductSyncer.java +++ b/src/main/java/com/commercetools/project/sync/product/ProductSyncer.java @@ -19,9 +19,15 @@ import io.sphere.sdk.client.SphereClient; import io.sphere.sdk.commands.UpdateAction; import io.sphere.sdk.models.WithKey; +import io.sphere.sdk.products.PriceDraft; +import io.sphere.sdk.products.PriceDraftBuilder; import io.sphere.sdk.products.Product; import io.sphere.sdk.products.ProductDraft; +import io.sphere.sdk.products.ProductDraftBuilder; import io.sphere.sdk.products.ProductProjection; +import io.sphere.sdk.products.ProductVariantDraft; +import io.sphere.sdk.products.ProductVariantDraftBuilder; +import io.sphere.sdk.products.ProductVariantDraftDsl; import io.sphere.sdk.products.queries.ProductProjectionQuery; import io.sphere.sdk.queries.QueryPredicate; import java.time.Clock; @@ -30,6 +36,7 @@ import java.util.Optional; import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; +import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.slf4j.Logger; @@ -115,10 +122,55 @@ protected CompletionStage> transform(@Nonnull ListIssue: https://github.com/commercetools/commercetools-project-sync/issues/363 + */ + private List removeDiscountedFromPrices( + @Nonnull final List productDrafts) { + return productDrafts + .stream() + .map( + productDraft -> { + final List productVariants = + productDraft + .getVariants() + .stream() + .map(this::createProductVariantDraftWithoutDiscounted) + .collect(Collectors.toList()); + final ProductVariantDraft masterVariant = productDraft.getMasterVariant(); + ProductVariantDraft masterVariantDraft = null; + if (masterVariant != null) { + masterVariantDraft = createProductVariantDraftWithoutDiscounted(masterVariant); + } + return ProductDraftBuilder.of(productDraft) + .masterVariant(masterVariantDraft) + .variants(productVariants) + .build(); + }) + .collect(Collectors.toList()); + } + + private ProductVariantDraftDsl createProductVariantDraftWithoutDiscounted( + @Nonnull final ProductVariantDraft productVariantDraft) { + final List prices = productVariantDraft.getPrices(); + List priceDrafts = null; + if (prices != null) { + priceDrafts = + prices + .stream() + .map(priceDraft -> PriceDraftBuilder.of(priceDraft).discounted(null).build()) + .collect(Collectors.toList()); + } + return ProductVariantDraftBuilder.of(productVariantDraft).prices(priceDrafts).build(); + } + @Nonnull private static Throwable getCompletionExceptionCause(@Nonnull final Throwable exception) { if (exception instanceof CompletionException) { diff --git a/src/test/java/com/commercetools/project/sync/product/ProductSyncerTest.java b/src/test/java/com/commercetools/project/sync/product/ProductSyncerTest.java index 20a5de94..cc52a2bf 100644 --- a/src/test/java/com/commercetools/project/sync/product/ProductSyncerTest.java +++ b/src/test/java/com/commercetools/project/sync/product/ProductSyncerTest.java @@ -152,6 +152,44 @@ void transform_WithAttributeReferences_ShouldReplaceProductReferenceIdsWithKeys( assertThat(testLogger.getAllLoggingEvents()).isEmpty(); } + @Test + void transform_WithDiscountedPrices_ShouldRemoveDiscountedPrices() { + // preparation + final SphereClient sourceClient = mock(SphereClient.class); + final ProductSyncer productSyncer = + ProductSyncer.of(sourceClient, mock(SphereClient.class), getMockedClock(), null); + final List productPage = + Collections.singletonList( + readObjectFromResource("product-key-10.json", Product.class) + .toProjection(ProductProjectionType.STAGED)); + + String jsonStringProductTypes = + "{\"results\":[{\"id\":\"53c4a8b4-754f-4b95-b6f2-3e1e70e3d0d3\"," + + "\"key\":\"prodType1\"}]}"; + final ResourceKeyIdGraphQlResult productTypesResult = + SphereJsonUtils.readObject(jsonStringProductTypes, ResourceKeyIdGraphQlResult.class); + + when(sourceClient.execute(any(ResourceIdsGraphQlRequest.class))) + .thenReturn(CompletableFuture.completedFuture(productTypesResult)); + + // test + final List draftsFromPageStage = + productSyncer.transform(productPage).toCompletableFuture().join(); + + final Optional productDraftKey1 = + draftsFromPageStage + .stream() + .filter(productDraft -> "productKey10".equals(productDraft.getKey())) + .findFirst(); + + assertThat(productDraftKey1) + .hasValueSatisfying( + productDraft -> + assertThat(productDraft.getMasterVariant().getPrices()) + .anySatisfy(priceDraft -> assertThat(priceDraft.getDiscounted()).isNull())); + assertThat(testLogger.getAllLoggingEvents()).isEmpty(); + } + @Test void transform_WithErrorOnGraphQlRequest_ShouldContinueAndLogError() { // preparation diff --git a/src/test/resources/product-key-10.json b/src/test/resources/product-key-10.json new file mode 100644 index 00000000..7edbbe64 --- /dev/null +++ b/src/test/resources/product-key-10.json @@ -0,0 +1,89 @@ +{ + "id": "6f7c7c6d-6c7c-4c84-ab33-b08ca461f7c2", + "version": 3, + "lastMessageSequenceNumber": 1, + "createdAt": "2022-05-23T20:12:36.534Z", + "lastModifiedAt": "2022-06-03T10:43:47.348Z", + "lastModifiedBy": { + "isPlatformClient": true + }, + "createdBy": { + "isPlatformClient": true + }, + "productType": { + "typeId": "product-type", + "id": "9aad3fdf-56fc-40e7-96d1-078b8b016f86" + }, + "masterData": { + "current": { + "name": { + "en": "Wine subscription" + }, + "categories": [], + "categoryOrderHints": {}, + "slug": { + "en": "wine-subscription" + }, + "masterVariant": { + "id": 1, + "sku": "wine01", + "prices": [], + "images": [], + "attributes": [], + "assets": [] + }, + "variants": [], + "searchKeywords": {} + }, + "staged": { + "name": { + "en": "Wine subscription" + }, + "categories": [], + "categoryOrderHints": {}, + "slug": { + "en": "wine-subscription" + }, + "masterVariant": { + "id": 1, + "sku": "wine01", + "prices": [ + { + "id": "4aced858-bb95-4112-b021-64f1b754d74b", + "value": { + "type": "centPrecision", + "currencyCode": "EUR", + "centAmount": 4200, + "fractionDigits": 2 + }, + "discounted": { + "value": { + "type": "centPrecision", + "currencyCode": "EUR", + "centAmount": 4000, + "fractionDigits": 2 + }, + "discount": { + "typeId": "product-discount", + "id": "659e94b7-8d5b-404e-8d72-547e3931b42f" + } + } + } + ], + "images": [], + "attributes": [], + "assets": [] + }, + "variants": [], + "searchKeywords": {} + }, + "published": false, + "hasStagedChanges": true + }, + "key": "productKey10", + "taxCategory": { + "typeId": "tax-category", + "id": "e245e8d6-7a15-4bd5-8c8b-d033c662e17a" + }, + "lastVariantId": 1 +}