From 9f9c837c2e1dae346dac83d3e797dd432aac5ef9 Mon Sep 17 00:00:00 2001 From: King-Hin Leung <35692276+leungkinghin@users.noreply.github.com> Date: Mon, 19 Oct 2020 13:59:54 +0200 Subject: [PATCH 01/12] Add ShoppingList Reference Resolver helpers/utilities. (#602) Co-authored-by: aoz --- .../helpers/CustomReferenceResolver.java | 3 +- .../CustomTypeReferenceResolutionUtils.java | 22 ++ .../ShoppingListSyncOptions.java | 42 ++ .../ShoppingListSyncOptionsBuilder.java | 58 +++ .../helpers/LineItemReferenceResolver.java | 65 +++ .../ShoppingListReferenceResolver.java | 147 +++++++ .../TextLineItemReferenceResolver.java | 63 +++ .../ShoppingListReferenceResolutionUtils.java | 190 +++++++++ .../commercetools/sync/commons/MockUtils.java | 35 ++ .../sync/products/ProductSyncMockUtils.java | 1 - .../LineItemReferenceResolverTest.java | 148 +++++++ .../ShoppingListReferenceResolverTest.java | 372 ++++++++++++++++++ .../TextLineItemReferenceResolverTest.java | 156 ++++++++ ...pingListsReferenceResolutionUtilsTest.java | 193 +++++++++ 14 files changed, 1492 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptions.java create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptionsBuilder.java create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/helpers/LineItemReferenceResolver.java create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListReferenceResolver.java create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/helpers/TextLineItemReferenceResolver.java create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListReferenceResolutionUtils.java create mode 100644 src/test/java/com/commercetools/sync/shoppinglists/helpers/LineItemReferenceResolverTest.java create mode 100644 src/test/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListReferenceResolverTest.java create mode 100644 src/test/java/com/commercetools/sync/shoppinglists/helpers/TextLineItemReferenceResolverTest.java create mode 100644 src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListsReferenceResolutionUtilsTest.java diff --git a/src/main/java/com/commercetools/sync/commons/helpers/CustomReferenceResolver.java b/src/main/java/com/commercetools/sync/commons/helpers/CustomReferenceResolver.java index 85eaa80b07..05bac2d6ef 100644 --- a/src/main/java/com/commercetools/sync/commons/helpers/CustomReferenceResolver.java +++ b/src/main/java/com/commercetools/sync/commons/helpers/CustomReferenceResolver.java @@ -7,7 +7,6 @@ import io.sphere.sdk.categories.CategoryDraft; import io.sphere.sdk.models.Builder; import io.sphere.sdk.models.ResourceIdentifier; -import io.sphere.sdk.types.CustomDraft; import io.sphere.sdk.types.CustomFieldsDraft; import io.sphere.sdk.types.Type; @@ -35,7 +34,7 @@ * specified by the user, on reference resolution. */ public abstract class CustomReferenceResolver - , S extends BaseSyncOptions> + , S extends BaseSyncOptions> extends BaseReferenceResolver { public static final String TYPE_DOES_NOT_EXIST = "Type with key '%s' doesn't exist."; diff --git a/src/main/java/com/commercetools/sync/commons/utils/CustomTypeReferenceResolutionUtils.java b/src/main/java/com/commercetools/sync/commons/utils/CustomTypeReferenceResolutionUtils.java index eab092e20b..457894a3da 100644 --- a/src/main/java/com/commercetools/sync/commons/utils/CustomTypeReferenceResolutionUtils.java +++ b/src/main/java/com/commercetools/sync/commons/utils/CustomTypeReferenceResolutionUtils.java @@ -1,5 +1,8 @@ package com.commercetools.sync.commons.utils; +import io.sphere.sdk.shoppinglists.LineItem; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.TextLineItem; import io.sphere.sdk.types.Custom; import io.sphere.sdk.types.CustomFields; import io.sphere.sdk.types.CustomFieldsDraft; @@ -30,6 +33,25 @@ public final class CustomTypeReferenceResolutionUtils { @Nullable public static CustomFieldsDraft mapToCustomFieldsDraft(@Nonnull final T resource) { final CustomFields custom = resource.getCustom(); + return mapToCustomFieldsDraft(custom); + } + + /** + * Given a custom {@link CustomFields}, this method provides checking to certain resources which do not extends + * {@link Custom}, such as {@link ShoppingList}, {@link LineItem} and {@link TextLineItem}. If the custom fields + * are existing (not null) and they are reference expanded. If they are then it returns a {@link CustomFieldsDraft} + * instance with the custom type key in place of the key of the reference. Otherwise, if it's not reference expanded + * it returns a {@link CustomFieldsDraft} without the key. If the resource has null {@link Custom}, then it returns + * {@code null}. + * + * @param custom the resource to replace its custom type key, if possible. + * @return an instance of {@link CustomFieldsDraft} instance with the custom type key, if the + * custom type reference was existing and reference expanded on the resource. Otherwise, if its not + * reference expanded it returns a {@link CustomFieldsDraft} without a key. If the + * resource has no or null {@link Custom}, then it returns {@code null}. + */ + @Nullable + public static CustomFieldsDraft mapToCustomFieldsDraft(@Nullable final CustomFields custom) { if (custom != null) { if (custom.getType().getObj() != null) { return CustomFieldsDraft.ofTypeKeyAndJson(custom.getType().getObj().getKey(), diff --git a/src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptions.java b/src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptions.java new file mode 100644 index 0000000000..8ca42d8a00 --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptions.java @@ -0,0 +1,42 @@ +package com.commercetools.sync.shoppinglists; + +import com.commercetools.sync.commons.BaseSyncOptions; +import com.commercetools.sync.commons.exceptions.SyncException; +import com.commercetools.sync.commons.utils.QuadConsumer; +import com.commercetools.sync.commons.utils.TriConsumer; +import com.commercetools.sync.commons.utils.TriFunction; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +public final class ShoppingListSyncOptions extends BaseSyncOptions { + + ShoppingListSyncOptions( + @Nonnull final SphereClient ctpClient, + @Nullable final QuadConsumer, Optional, + List>> errorCallback, + @Nullable final TriConsumer, Optional> + warningCallback, + final int batchSize, + @Nullable final TriFunction>, ShoppingListDraft, + ShoppingList, List>> beforeUpdateCallback, + @Nullable final Function beforeCreateCallback + ) { + super( + ctpClient, + errorCallback, + warningCallback, + batchSize, + beforeUpdateCallback, + beforeCreateCallback + ); + } + +} diff --git a/src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptionsBuilder.java b/src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptionsBuilder.java new file mode 100644 index 0000000000..87198b375b --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptionsBuilder.java @@ -0,0 +1,58 @@ +package com.commercetools.sync.shoppinglists; + +import com.commercetools.sync.commons.BaseSyncOptionsBuilder; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; + +import javax.annotation.Nonnull; + +public final class ShoppingListSyncOptionsBuilder + extends BaseSyncOptionsBuilder { + + public static final int BATCH_SIZE_DEFAULT = 50; + + private ShoppingListSyncOptionsBuilder(@Nonnull final SphereClient ctpClient) { + this.ctpClient = ctpClient; + } + + /** + * Creates a new instance of {@link ShoppingListSyncOptionsBuilder} given a {@link SphereClient} responsible for + * interaction with the target CTP project, with the default batch size ({@code BATCH_SIZE_DEFAULT} = 50). + * + * @param ctpClient instance of the {@link SphereClient} responsible for interaction with the target CTP project. + * @return new instance of {@link ShoppingListSyncOptionsBuilder} + */ + public static ShoppingListSyncOptionsBuilder of(@Nonnull final SphereClient ctpClient) { + return new ShoppingListSyncOptionsBuilder(ctpClient).batchSize(BATCH_SIZE_DEFAULT); + } + + /** + * Creates new instance of {@link ShoppingListSyncOptions} enriched with all fields provided to {@code this} builder + * + * @return new instance of {@link ShoppingListSyncOptions} + */ + @Override + public ShoppingListSyncOptions build() { + return new ShoppingListSyncOptions( + ctpClient, + errorCallback, + warningCallback, + batchSize, + beforeUpdateCallback, + beforeCreateCallback + ); + } + + /** + * Returns an instance of this class to be used in the superclass's generic methods. Please see the JavaDoc in the + * overridden method for further details. + * + * @return an instance of this class. + */ + @Override + protected ShoppingListSyncOptionsBuilder getThis() { + return this; + } +} diff --git a/src/main/java/com/commercetools/sync/shoppinglists/helpers/LineItemReferenceResolver.java b/src/main/java/com/commercetools/sync/shoppinglists/helpers/LineItemReferenceResolver.java new file mode 100644 index 0000000000..74d5ec85d4 --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/helpers/LineItemReferenceResolver.java @@ -0,0 +1,65 @@ +package com.commercetools.sync.shoppinglists.helpers; + +import com.commercetools.sync.commons.exceptions.ReferenceResolutionException; +import com.commercetools.sync.commons.helpers.CustomReferenceResolver; +import com.commercetools.sync.services.TypeService; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import io.sphere.sdk.shoppinglists.LineItemDraft; +import io.sphere.sdk.shoppinglists.LineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; + +import javax.annotation.Nonnull; +import java.util.concurrent.CompletionStage; + +import static java.lang.String.format; + +public final class LineItemReferenceResolver + extends CustomReferenceResolver { + + static final String FAILED_TO_RESOLVE_CUSTOM_TYPE = "Failed to resolve custom type reference on " + + "LineItemDraft with SKU: '%s'."; + + /** + * Takes a {@link ShoppingListSyncOptions} instance, a {@link TypeService} to instantiate a + * {@link LineItemReferenceResolver} instance that could be used to resolve the custom type references of the + * line item drafts in the CTP project specified in the injected {@link ShoppingListSyncOptions} instance. + * + * @param shoppingListSyncOptions the container of all the options of the sync process including the CTP project + * client and/or configuration and other sync-specific options. + * @param typeService the service to fetch the types for reference resolution. + */ + public LineItemReferenceResolver(@Nonnull final ShoppingListSyncOptions shoppingListSyncOptions, + @Nonnull final TypeService typeService) { + + super(shoppingListSyncOptions, typeService); + + } + + /** + * Given a {@link ShoppingListDraft} this method attempts to resolve the custom type reference to return + * a {@link CompletionStage} which contains a new instance of the draft with the resolved references. + * + * @param lineItemDraft the lineItemDraft to resolve its references. + * @return a {@link CompletionStage} that contains as a result a new lineItemDraft instance with resolved + * references or, in case an error occurs during reference resolution, + * a {@link ReferenceResolutionException}. + */ + @Override + @Nonnull + public CompletionStage resolveReferences(@Nonnull final LineItemDraft lineItemDraft) { + return resolveCustomTypeReference(LineItemDraftBuilder.of(lineItemDraft)) + .thenApply(LineItemDraftBuilder::build); + } + + @Nonnull + protected CompletionStage resolveCustomTypeReference( + @Nonnull final LineItemDraftBuilder draftBuilder) { + + return resolveCustomTypeReference( + draftBuilder, + LineItemDraftBuilder::getCustom, + LineItemDraftBuilder::custom, + format(FAILED_TO_RESOLVE_CUSTOM_TYPE, draftBuilder.getSku())); + + } +} diff --git a/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListReferenceResolver.java b/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListReferenceResolver.java new file mode 100644 index 0000000000..5531ece3c4 --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListReferenceResolver.java @@ -0,0 +1,147 @@ +package com.commercetools.sync.shoppinglists.helpers; + +import com.commercetools.sync.commons.exceptions.ReferenceResolutionException; +import com.commercetools.sync.commons.helpers.CustomReferenceResolver; +import com.commercetools.sync.services.CustomerService; +import com.commercetools.sync.services.TypeService; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import io.sphere.sdk.customers.Customer; +import io.sphere.sdk.models.ResourceIdentifier; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.ShoppingListDraftBuilder; + +import javax.annotation.Nonnull; +import java.util.concurrent.CompletionStage; + +import static com.commercetools.sync.commons.utils.CompletableFutureUtils.mapValuesToFutureOfCompletedValues; +import static io.sphere.sdk.utils.CompletableFutureUtils.exceptionallyCompletedFuture; +import static java.lang.String.format; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.stream.Collectors.toList; + +public final class ShoppingListReferenceResolver + extends CustomReferenceResolver { + + static final String FAILED_TO_RESOLVE_CUSTOMER_REFERENCE = "Failed to resolve customer resource identifier on " + + "ShoppingListDraft with key:'%s'. Reason: %s"; + static final String CUSTOMER_DOES_NOT_EXIST = "Customer with key '%s' doesn't exist."; + static final String FAILED_TO_RESOLVE_CUSTOM_TYPE = "Failed to resolve custom type reference on " + + "ShoppingListDraft with key:'%s'. "; + + private final CustomerService customerService; + private final LineItemReferenceResolver lineItemReferenceResolver; + private final TextLineItemReferenceResolver textLineItemReferenceResolver; + + /** + * Takes a {@link ShoppingListSyncOptions} instance, a {@link CustomerService} and {@link TypeService} to + * instantiate a {@link ShoppingListReferenceResolver} instance that could be used to resolve the shopping list + * drafts in the CTP project specified in the injected {@link ShoppingListSyncOptions} instance. + * + * @param shoppingListSyncOptions the container of all the options of the sync process including the CTP project + * client and/or configuration and other sync-specific options. + * @param customerService the service to fetch the customers for reference resolution. + * @param typeService the service to fetch the types for reference resolution. + */ + public ShoppingListReferenceResolver(@Nonnull final ShoppingListSyncOptions shoppingListSyncOptions, + @Nonnull final CustomerService customerService, + @Nonnull final TypeService typeService) { + + super(shoppingListSyncOptions, typeService); + this.lineItemReferenceResolver = new LineItemReferenceResolver(shoppingListSyncOptions, typeService); + this.textLineItemReferenceResolver = new TextLineItemReferenceResolver(shoppingListSyncOptions, typeService); + this.customerService = customerService; + } + + /** + * Given a {@link ShoppingListDraft} this method attempts to resolve the customer and custom type references to + * return a {@link CompletionStage} which contains a new instance of the draft with the resolved references. + * + * @param shoppingListDraft the shoppingListDraft to resolve its references. + * @return a {@link CompletionStage} that contains as a result a new shoppingListDraft instance with resolved + * references or, in case an error occurs during reference resolution, + * a {@link ReferenceResolutionException}. + */ + @Nonnull + public CompletionStage resolveReferences(@Nonnull final ShoppingListDraft shoppingListDraft) { + return resolveCustomerReference(ShoppingListDraftBuilder.of(shoppingListDraft)) + .thenCompose(this::resolveCustomTypeReference) + .thenCompose(this::resolveLineItemReferences) + .thenCompose(this::resolveTextLineItemReferences) + .thenApply(ShoppingListDraftBuilder::build); + } + + @Nonnull + protected CompletionStage resolveCustomerReference( + @Nonnull final ShoppingListDraftBuilder draftBuilder) { + + final ResourceIdentifier customerResourceIdentifier = draftBuilder.getCustomer(); + if (customerResourceIdentifier != null && customerResourceIdentifier.getId() == null) { + String customerKey; + try { + customerKey = getKeyFromResourceIdentifier(customerResourceIdentifier); + } catch (ReferenceResolutionException referenceResolutionException) { + return exceptionallyCompletedFuture(new ReferenceResolutionException( + format(FAILED_TO_RESOLVE_CUSTOMER_REFERENCE, draftBuilder.getKey(), + referenceResolutionException.getMessage()))); + } + + return fetchAndResolveCustomerReference(draftBuilder, customerKey); + } + return completedFuture(draftBuilder); + } + + @Nonnull + private CompletionStage fetchAndResolveCustomerReference( + @Nonnull final ShoppingListDraftBuilder draftBuilder, + @Nonnull final String customerKey) { + + return customerService + .fetchCachedCustomerId(customerKey) + .thenCompose(resolvedCustomerIdOptional -> resolvedCustomerIdOptional + .map(resolvedCustomerId -> + completedFuture(draftBuilder.customer( + Customer.referenceOfId(resolvedCustomerId).toResourceIdentifier()))) + .orElseGet(() -> { + final String errorMessage = format(CUSTOMER_DOES_NOT_EXIST, customerKey); + return exceptionallyCompletedFuture(new ReferenceResolutionException( + format(FAILED_TO_RESOLVE_CUSTOMER_REFERENCE, draftBuilder.getKey(), errorMessage))); + })); + } + + @Nonnull + protected CompletionStage resolveCustomTypeReference( + @Nonnull final ShoppingListDraftBuilder draftBuilder) { + + return resolveCustomTypeReference( + draftBuilder, + ShoppingListDraftBuilder::getCustom, + ShoppingListDraftBuilder::custom, + format(FAILED_TO_RESOLVE_CUSTOM_TYPE, draftBuilder.getKey())); + } + + @Nonnull + private CompletionStage resolveLineItemReferences( + @Nonnull final ShoppingListDraftBuilder draftBuilder) { + + if (draftBuilder.getLineItems() != null) { + return mapValuesToFutureOfCompletedValues( + draftBuilder.getLineItems(), lineItemReferenceResolver::resolveReferences, toList()) + .thenApply(draftBuilder::lineItems); + } + + return completedFuture(draftBuilder); + } + + @Nonnull + private CompletionStage resolveTextLineItemReferences( + @Nonnull final ShoppingListDraftBuilder draftBuilder) { + + if (draftBuilder.getTextLineItems() != null) { + return mapValuesToFutureOfCompletedValues( + draftBuilder.getTextLineItems(), textLineItemReferenceResolver::resolveReferences, toList()) + .thenApply(draftBuilder::textLineItems); + } + + return completedFuture(draftBuilder); + } +} diff --git a/src/main/java/com/commercetools/sync/shoppinglists/helpers/TextLineItemReferenceResolver.java b/src/main/java/com/commercetools/sync/shoppinglists/helpers/TextLineItemReferenceResolver.java new file mode 100644 index 0000000000..08de765723 --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/helpers/TextLineItemReferenceResolver.java @@ -0,0 +1,63 @@ +package com.commercetools.sync.shoppinglists.helpers; + +import com.commercetools.sync.commons.exceptions.ReferenceResolutionException; +import com.commercetools.sync.commons.helpers.CustomReferenceResolver; +import com.commercetools.sync.services.TypeService; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import io.sphere.sdk.shoppinglists.TextLineItemDraft; +import io.sphere.sdk.shoppinglists.TextLineItemDraftBuilder; + +import javax.annotation.Nonnull; +import java.util.concurrent.CompletionStage; + +import static java.lang.String.format; + +public final class TextLineItemReferenceResolver + extends CustomReferenceResolver { + + static final String FAILED_TO_RESOLVE_CUSTOM_TYPE = "Failed to resolve custom type reference on " + + "TextLineItemDraft with name: '%s'."; + + /** + * Takes a {@link ShoppingListSyncOptions} instance, a {@link TypeService} to instantiate a + * {@link TextLineItemReferenceResolver} instance that could be used to resolve the text line-item drafts in the + * CTP project specified in the injected {@link ShoppingListSyncOptions} instance. + * + * @param shoppingListSyncOptions the container of all the options of the sync process including the CTP project + * client and/or configuration and other sync-specific options. + * @param typeService the service to fetch the types for reference resolution. + */ + public TextLineItemReferenceResolver(@Nonnull final ShoppingListSyncOptions shoppingListSyncOptions, + @Nonnull final TypeService typeService) { + + super(shoppingListSyncOptions, typeService); + + } + + /** + * Given a {@link TextLineItemDraft} this method attempts to resolve the attribute definition references to return + * a {@link CompletionStage} which contains a new instance of the draft with the resolved references. + * + * @param textLineItemDraft the textLineItemDraft to resolve its references. + * @return a {@link CompletionStage} that contains as a result a new textLineItemDraft instance with resolved + * references or, in case an error occurs during reference resolution, + * a {@link ReferenceResolutionException}. + */ + @Override + @Nonnull + public CompletionStage resolveReferences(@Nonnull final TextLineItemDraft textLineItemDraft) { + return resolveCustomTypeReference(TextLineItemDraftBuilder.of(textLineItemDraft)) + .thenApply(TextLineItemDraftBuilder::build); + } + + @Nonnull + protected CompletionStage resolveCustomTypeReference( + @Nonnull final TextLineItemDraftBuilder draftBuilder) { + + return resolveCustomTypeReference( + draftBuilder, + TextLineItemDraftBuilder::getCustom, + TextLineItemDraftBuilder::custom, + format(FAILED_TO_RESOLVE_CUSTOM_TYPE, draftBuilder.getName())); + } +} diff --git a/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListReferenceResolutionUtils.java b/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListReferenceResolutionUtils.java new file mode 100644 index 0000000000..d2ae666c24 --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListReferenceResolutionUtils.java @@ -0,0 +1,190 @@ +package com.commercetools.sync.shoppinglists.utils; + +import io.sphere.sdk.customers.Customer; +import io.sphere.sdk.expansion.ExpansionPath; +import io.sphere.sdk.models.Reference; +import io.sphere.sdk.models.ResourceIdentifier; +import io.sphere.sdk.shoppinglists.LineItem; +import io.sphere.sdk.shoppinglists.LineItemDraft; +import io.sphere.sdk.shoppinglists.LineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.ShoppingListDraftBuilder; +import io.sphere.sdk.shoppinglists.TextLineItem; +import io.sphere.sdk.shoppinglists.TextLineItemDraft; +import io.sphere.sdk.shoppinglists.TextLineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.expansion.ShoppingListExpansionModel; +import io.sphere.sdk.shoppinglists.queries.ShoppingListQuery; +import io.sphere.sdk.types.Type; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static com.commercetools.sync.commons.utils.CustomTypeReferenceResolutionUtils.mapToCustomFieldsDraft; +import static com.commercetools.sync.commons.utils.SyncUtils.getResourceIdentifierWithKey; +import static java.util.stream.Collectors.toList; + +/** + * Util class which provides utilities that can be used when syncing shopping lists from a source commercetools project + * to a target one. + */ +public final class ShoppingListReferenceResolutionUtils { + + /** + * Returns an {@link List}<{@link ShoppingListDraft}> consisting of the results of applying the + * mapping from {@link ShoppingList} to {@link ShoppingListDraft} with considering reference resolution. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Reference fieldfromto
customer{@link Reference}<{@link Customer}>{@link ResourceIdentifier}<{@link Customer}>
custom.type{@link Reference}<{@link Type}>{@link ResourceIdentifier}<{@link Type}>
lineItems.custom.type{@link Set}<{@link Reference}<{@link Type}>>{@link Set}<{@link ResourceIdentifier}<{@link Type}>>
textLineItems.custom.type{@link Set}<{@link Reference}<{@link Type}>>{@link Set}<{@link ResourceIdentifier}<{@link Type}>>
+ * + *

Note: The aforementioned references should be expanded with a key. + * Any reference that is not expanded will have its id in place and not replaced by the key will be + * considered as existing resources on the target commercetools project and + * the library will issues an update/create API request without reference resolution. + * + * @param shoppingLists the shopping lists with expanded references. + * @return a {@link List} of {@link ShoppingListDraft} built from the supplied {@link List} of {@link ShoppingList}. + */ + @Nonnull + public static List mapToShoppingListDrafts( + @Nonnull final List shoppingLists) { + + return shoppingLists + .stream() + .filter(Objects::nonNull) + .map(ShoppingListReferenceResolutionUtils::mapToShoppingListDraft) + .collect(toList()); + } + + @Nonnull + private static ShoppingListDraft mapToShoppingListDraft(@Nonnull final ShoppingList shoppingList) { + + return ShoppingListDraftBuilder + .of(shoppingList.getName()) + .description(shoppingList.getDescription()) + .key(shoppingList.getKey()) + .customer(getResourceIdentifierWithKey(shoppingList.getCustomer())) + .slug(shoppingList.getSlug()) + .lineItems(mapToLineItemDrafts(shoppingList.getLineItems())) + .textLineItems(mapToTextLineItemDrafts(shoppingList.getTextLineItems())) + .custom(mapToCustomFieldsDraft(shoppingList)) + .deleteDaysAfterLastModification(shoppingList.getDeleteDaysAfterLastModification()) + .anonymousId(shoppingList.getAnonymousId()) + .build(); + } + + @Nullable + private static List mapToLineItemDrafts( + @Nullable final List lineItems) { + + if (lineItems == null) { + return null; + } + + return lineItems.stream() + .filter(Objects::nonNull) + .map(ShoppingListReferenceResolutionUtils::mapToLineItemDraft) + .filter(Objects::nonNull) + .collect(toList()); + } + + @Nullable + private static LineItemDraft mapToLineItemDraft(@Nonnull final LineItem lineItem) { + + if (lineItem.getVariant() != null) { + return LineItemDraftBuilder + .ofSku(lineItem.getVariant().getSku(), lineItem.getQuantity()) + .addedAt(lineItem.getAddedAt()) + .custom(mapToCustomFieldsDraft(lineItem.getCustom())) + .build(); + } + + return null; + } + + @Nullable + private static List mapToTextLineItemDrafts( + @Nullable final List textLineItems) { + + if (textLineItems == null) { + return null; + } + + return textLineItems.stream() + .filter(Objects::nonNull) + .map(ShoppingListReferenceResolutionUtils::mapToTextLineItemDraft) + .collect(toList()); + } + + @Nonnull + private static TextLineItemDraft mapToTextLineItemDraft(@Nonnull final TextLineItem textLineItem) { + + return TextLineItemDraftBuilder.of(textLineItem.getName(), textLineItem.getQuantity()) + .description(textLineItem.getDescription()) + .addedAt(textLineItem.getAddedAt()) + .custom(mapToCustomFieldsDraft(textLineItem.getCustom())) + .build(); + } + + /** + * Builds a {@link ShoppingListQuery} for fetching shopping lists from a source CTP project with all the + * needed references expanded for the sync: + *

    + *
  • Customer
  • + *
  • Custom Type of the Shopping List
  • + *
  • Variants of the LineItems
  • + *
  • Custom Types of the LineItems
  • + *
  • Custom Types of the TextLineItems
  • + *
+ * + *

Note: Please only use this util if you desire to sync all the aforementioned references from + * a source commercetools project. Otherwise, it is more efficient to build the query without expansions, if they + * are not needed, to avoid unnecessarily bigger payloads fetched from the source project. + * + * @return the query for fetching shopping lists from the source CTP project with all the aforementioned references + * expanded. + */ + public static ShoppingListQuery buildShoppingListQuery() { + return ShoppingListQuery.of() + .withExpansionPaths(ShoppingListExpansionModel::customer) + .plusExpansionPaths(ExpansionPath.of("custom.type")) + .plusExpansionPaths(ExpansionPath.of("lineItems[*].variant")) + .plusExpansionPaths(ExpansionPath.of("lineItems[*].custom.type")) + .plusExpansionPaths(ExpansionPath.of("textLineItems[*].custom.type")); + } + + private ShoppingListReferenceResolutionUtils() { + } +} diff --git a/src/test/java/com/commercetools/sync/commons/MockUtils.java b/src/test/java/com/commercetools/sync/commons/MockUtils.java index 8f0a1c08dc..27840e33c1 100644 --- a/src/test/java/com/commercetools/sync/commons/MockUtils.java +++ b/src/test/java/com/commercetools/sync/commons/MockUtils.java @@ -1,10 +1,13 @@ package com.commercetools.sync.commons; import com.commercetools.sync.services.CategoryService; +import com.commercetools.sync.services.CustomerService; import com.commercetools.sync.services.TypeService; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import io.sphere.sdk.categories.Category; +import io.sphere.sdk.customergroups.CustomerGroup; +import io.sphere.sdk.customers.Customer; import io.sphere.sdk.models.Asset; import io.sphere.sdk.models.Reference; import io.sphere.sdk.types.CustomFields; @@ -144,4 +147,36 @@ public static Asset getAssetMockWithCustomFields(@Nullable final Reference when(asset.getCustom()).thenReturn(mockCustomFields); return asset; } + + /** + * Creates a mock {@link CustomerService} that returns a dummy customer id of value "customerId" instance + * whenever the following method is called on the service: + *

    + *
  • {@link CustomerService#fetchCachedCustomerId(String)}
  • + *
+ * + * @return the created mock of the {@link CustomerService}. + */ + public static CustomerService getMockCustomerService() { + final CustomerService customerService = mock(CustomerService.class); + when(customerService.fetchCachedCustomerId(anyString())) + .thenReturn(completedFuture(Optional.of("customerId"))); + when(customerService.cacheKeysToIds(anySet())) + .thenReturn(completedFuture(Collections.singletonMap("customerKey", "customerId"))); + return customerService; + } + + /** + * Creates a mock {@link Customer} with the supplied {@code id} and {@code key}. + * + * @param id the id of the created mock {@link Customer}. + * @param key the key of the created mock {@link CustomerGroup}. + * @return a mock customerGroup with the supplied id and key. + */ + public static Customer getMockCustomer(final String id, final String key) { + final Customer customer = mock(Customer.class); + when(customer.getId()).thenReturn(id); + when(customer.getKey()).thenReturn(key); + return customer; + } } diff --git a/src/test/java/com/commercetools/sync/products/ProductSyncMockUtils.java b/src/test/java/com/commercetools/sync/products/ProductSyncMockUtils.java index d55f8a0208..31804f774d 100644 --- a/src/test/java/com/commercetools/sync/products/ProductSyncMockUtils.java +++ b/src/test/java/com/commercetools/sync/products/ProductSyncMockUtils.java @@ -34,7 +34,6 @@ import io.sphere.sdk.types.CustomFields; import io.sphere.sdk.types.Type; - import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.text.DecimalFormat; diff --git a/src/test/java/com/commercetools/sync/shoppinglists/helpers/LineItemReferenceResolverTest.java b/src/test/java/com/commercetools/sync/shoppinglists/helpers/LineItemReferenceResolverTest.java new file mode 100644 index 0000000000..2dc335cd50 --- /dev/null +++ b/src/test/java/com/commercetools/sync/shoppinglists/helpers/LineItemReferenceResolverTest.java @@ -0,0 +1,148 @@ +package com.commercetools.sync.shoppinglists.helpers; + +import com.commercetools.sync.commons.exceptions.ReferenceResolutionException; +import com.commercetools.sync.services.TypeService; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.models.SphereException; +import io.sphere.sdk.shoppinglists.LineItemDraft; +import io.sphere.sdk.shoppinglists.LineItemDraftBuilder; +import io.sphere.sdk.types.CustomFieldsDraft; +import io.sphere.sdk.utils.CompletableFutureUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import static com.commercetools.sync.commons.MockUtils.getMockTypeService; +import static com.commercetools.sync.commons.helpers.BaseReferenceResolver.BLANK_KEY_VALUE_ON_RESOURCE_IDENTIFIER; +import static com.commercetools.sync.commons.helpers.CustomReferenceResolver.TYPE_DOES_NOT_EXIST; +import static com.commercetools.sync.shoppinglists.helpers.LineItemReferenceResolver.FAILED_TO_RESOLVE_CUSTOM_TYPE; +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class LineItemReferenceResolverTest { + + private TypeService typeService; + + private LineItemReferenceResolver referenceResolver; + + /** + * Sets up the services and the options needed for reference resolution. + */ + @BeforeEach + void setup() { + typeService = getMockTypeService(); + + final ShoppingListSyncOptions syncOptions = ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)).build(); + referenceResolver = new LineItemReferenceResolver(syncOptions, typeService); + } + + @Test + void resolveReferences_WithCustomTypeId_ShouldNotResolveCustomTypeReferenceWithKey() { + final String customTypeId = "customTypeId"; + final CustomFieldsDraft customFieldsDraft = CustomFieldsDraft + .ofTypeIdAndJson(customTypeId, new HashMap<>()); + + final LineItemDraft lineItemDraft = + LineItemDraftBuilder.ofSku("dummy-sku", 10L) + .custom(customFieldsDraft) + .build(); + + final LineItemDraft resolvedDraft = referenceResolver + .resolveReferences(lineItemDraft) + .toCompletableFuture() + .join(); + + assertThat(resolvedDraft.getCustom()).isNotNull(); + assertThat(resolvedDraft.getCustom()).isEqualTo(customFieldsDraft); + } + + @Test + void resolveReferences_WithNonNullKeyOnCustomTypeResId_ShouldResolveCustomTypeReference() { + final CustomFieldsDraft customFieldsDraft = CustomFieldsDraft + .ofTypeKeyAndJson("customTypeKey", new HashMap<>()); + + final LineItemDraft lineItemDraft = + LineItemDraftBuilder.ofSku("dummy-sku", 10L) + .custom(customFieldsDraft) + .build(); + + final LineItemDraft resolvedDraft = referenceResolver + .resolveReferences(lineItemDraft) + .toCompletableFuture() + .join(); + + assertThat(resolvedDraft.getCustom()).isNotNull(); + assertThat(resolvedDraft.getCustom().getType().getId()).isEqualTo("typeId"); + } + + @Test + void resolveReferences_WithExceptionOnCustomTypeFetch_ShouldNotResolveReferences() { + when(typeService.fetchCachedTypeId(anyString())) + .thenReturn(CompletableFutureUtils.failed(new SphereException("CTP error on fetch"))); + + final String customTypeKey = "customTypeKey"; + final CustomFieldsDraft customFieldsDraft = + CustomFieldsDraft.ofTypeKeyAndJson(customTypeKey, new HashMap<>()); + + final LineItemDraft lineItemDraft = + LineItemDraftBuilder.ofSku("dummy-sku", 10L) + .custom(customFieldsDraft) + .build(); + + assertThat(referenceResolver.resolveReferences(lineItemDraft)) + .hasFailedWithThrowableThat() + .isExactlyInstanceOf(SphereException.class) + .hasMessageContaining("CTP error on fetch"); + } + + @Test + void resolveReferences_WithNonExistentCustomType_ShouldCompleteExceptionally() { + final String customTypeKey = "customTypeKey"; + final CustomFieldsDraft customFieldsDraft = + CustomFieldsDraft.ofTypeKeyAndJson(customTypeKey, new HashMap<>()); + + final LineItemDraft lineItemDraft = + LineItemDraftBuilder.ofSku("dummy-sku", 10L) + .custom(customFieldsDraft) + .build(); + + when(typeService.fetchCachedTypeId(anyString())) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + final String expectedExceptionMessage = format(FAILED_TO_RESOLVE_CUSTOM_TYPE, lineItemDraft.getSku()); + + final String expectedMessageWithCause = + format("%s Reason: %s", expectedExceptionMessage, format(TYPE_DOES_NOT_EXIST, customTypeKey)); + + assertThat(referenceResolver.resolveReferences(lineItemDraft)) + .hasFailedWithThrowableThat() + .isExactlyInstanceOf(ReferenceResolutionException.class) + .hasMessage(expectedMessageWithCause); + } + + @Test + void resolveReferences_WithEmptyKeyOnCustomTypeResId_ShouldCompleteExceptionally() { + final CustomFieldsDraft customFieldsDraft = CustomFieldsDraft + .ofTypeKeyAndJson("", new HashMap<>()); + + final LineItemDraft lineItemDraft = + LineItemDraftBuilder.ofSku("dummy-sku", 10L) + .custom(customFieldsDraft) + .build(); + + assertThat(referenceResolver.resolveReferences(lineItemDraft)) + .hasFailedWithThrowableThat() + .isExactlyInstanceOf(ReferenceResolutionException.class) + .hasMessage(format("Failed to resolve custom type reference on LineItemDraft" + + " with SKU: 'dummy-sku'. Reason: %s", BLANK_KEY_VALUE_ON_RESOURCE_IDENTIFIER)); + + } +} diff --git a/src/test/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListReferenceResolverTest.java b/src/test/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListReferenceResolverTest.java new file mode 100644 index 0000000000..06a816f05e --- /dev/null +++ b/src/test/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListReferenceResolverTest.java @@ -0,0 +1,372 @@ +package com.commercetools.sync.shoppinglists.helpers; + +import com.commercetools.sync.commons.exceptions.ReferenceResolutionException; +import com.commercetools.sync.services.CustomerService; +import com.commercetools.sync.services.TypeService; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.customers.Customer; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.models.ResourceIdentifier; +import io.sphere.sdk.models.SphereException; +import io.sphere.sdk.shoppinglists.LineItemDraft; +import io.sphere.sdk.shoppinglists.LineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.ShoppingListDraftBuilder; +import io.sphere.sdk.shoppinglists.TextLineItemDraft; +import io.sphere.sdk.shoppinglists.TextLineItemDraftBuilder; +import io.sphere.sdk.types.CustomFieldsDraft; +import io.sphere.sdk.utils.CompletableFutureUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import static com.commercetools.sync.commons.MockUtils.getMockCustomerService; +import static com.commercetools.sync.commons.MockUtils.getMockTypeService; +import static com.commercetools.sync.commons.helpers.BaseReferenceResolver.BLANK_KEY_VALUE_ON_RESOURCE_IDENTIFIER; +import static com.commercetools.sync.commons.helpers.CustomReferenceResolver.TYPE_DOES_NOT_EXIST; +import static com.commercetools.sync.shoppinglists.helpers.ShoppingListReferenceResolver.CUSTOMER_DOES_NOT_EXIST; +import static com.commercetools.sync.shoppinglists.helpers.ShoppingListReferenceResolver.FAILED_TO_RESOLVE_CUSTOMER_REFERENCE; +import static com.commercetools.sync.shoppinglists.helpers.ShoppingListReferenceResolver.FAILED_TO_RESOLVE_CUSTOM_TYPE; +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ShoppingListReferenceResolverTest { + + private TypeService typeService; + private CustomerService customerService; + + private ShoppingListReferenceResolver referenceResolver; + + /** + * Sets up the services and the options needed for reference resolution. + */ + @BeforeEach + void setup() { + typeService = getMockTypeService(); + customerService = getMockCustomerService(); + + final ShoppingListSyncOptions syncOptions = ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)).build(); + referenceResolver = new ShoppingListReferenceResolver(syncOptions, customerService, typeService); + } + + @Test + void resolveCustomTypeReference_WithNonNullIdOnCustomTypeResId_ShouldResolveCustomTypeReference() { + final CustomFieldsDraft customFieldsDraft = CustomFieldsDraft + .ofTypeKeyAndJson("customTypeKey", new HashMap<>()); + + final ShoppingListDraftBuilder draftBuilder = + ShoppingListDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "NAME")) + .custom(customFieldsDraft) + .description(LocalizedString.of(Locale.ENGLISH, "DESCRIPTION")); + + final ShoppingListDraftBuilder resolvedDraft = referenceResolver + .resolveCustomTypeReference(draftBuilder) + .toCompletableFuture().join(); + + assertThat(resolvedDraft.getCustom()).isNotNull(); + assertThat(resolvedDraft.getCustom().getType().getId()).isEqualTo("typeId"); + + } + + @Test + void resolveCustomTypeReference_WithCustomTypeId_ShouldNotResolveCustomTypeReferenceWithKey() { + final String customTypeId = "customTypeId"; + final CustomFieldsDraft customFieldsDraft = CustomFieldsDraft + .ofTypeIdAndJson(customTypeId, new HashMap<>()); + + final ShoppingListDraftBuilder draftBuilder = + ShoppingListDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "NAME")) + .custom(customFieldsDraft) + .description(LocalizedString.of(Locale.ENGLISH, "DESCRIPTION")); + + final ShoppingListDraftBuilder resolvedDraft = referenceResolver + .resolveCustomTypeReference(draftBuilder) + .toCompletableFuture().join(); + + assertThat(resolvedDraft.getCustom()).isNotNull(); + assertThat(resolvedDraft.getCustom()).isEqualTo(customFieldsDraft); + } + + @Test + void resolveCustomTypeReference_WithExceptionOnCustomTypeFetch_ShouldNotResolveReferences() { + when(typeService.fetchCachedTypeId(anyString())) + .thenReturn(CompletableFutureUtils.failed(new SphereException("CTP error on fetch"))); + + final String customTypeKey = "customTypeKey"; + final CustomFieldsDraft customFieldsDraft = + CustomFieldsDraft.ofTypeKeyAndJson(customTypeKey, new HashMap<>()); + final ShoppingListDraftBuilder draftBuilder = + ShoppingListDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "NAME")) + .custom(customFieldsDraft) + .description(LocalizedString.of(Locale.ENGLISH, "DESCRIPTION")); + + final CompletionStage resolvedDraftCompletionStage = referenceResolver + .resolveCustomTypeReference(draftBuilder); + + assertThat(resolvedDraftCompletionStage) + .hasFailedWithThrowableThat() + .isExactlyInstanceOf(SphereException.class) + .hasMessageContaining("CTP error on fetch"); + } + + @Test + void resolveCustomTypeReference_WithNonExistentCustomType_ShouldCompleteExceptionally() { + final String customTypeKey = "customTypeKey"; + final CustomFieldsDraft customFieldsDraft = + CustomFieldsDraft.ofTypeKeyAndJson(customTypeKey, new HashMap<>()); + + final ShoppingListDraftBuilder draftBuilder = + ShoppingListDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "NAME")) + .custom(customFieldsDraft) + .description(LocalizedString.of(Locale.ENGLISH, "DESCRIPTION")); + + when(typeService.fetchCachedTypeId(anyString())) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + // test + final CompletionStage resolvedDraftCompletionStage = referenceResolver + .resolveCustomTypeReference(draftBuilder); + + // assertion + final String expectedExceptionMessage = format(FAILED_TO_RESOLVE_CUSTOM_TYPE, + draftBuilder.getKey()); + + final String expectedMessageWithCause = + format("%s Reason: %s", expectedExceptionMessage, format(TYPE_DOES_NOT_EXIST, customTypeKey)); + ; + assertThat(resolvedDraftCompletionStage) + .hasFailedWithThrowableThat() + .isExactlyInstanceOf(ReferenceResolutionException.class) + .hasMessage(expectedMessageWithCause); + } + + @Test + void resolveCustomTypeReference_WithEmptyKeyOnCustomTypeResId_ShouldCompleteExceptionally() { + final CustomFieldsDraft customFieldsDraft = CustomFieldsDraft + .ofTypeKeyAndJson("", new HashMap<>()); + + final ShoppingListDraftBuilder draftBuilder = + ShoppingListDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "NAME")) + .custom(customFieldsDraft) + .description(LocalizedString.of(Locale.ENGLISH, "DESCRIPTION")); + + assertThat(referenceResolver.resolveCustomTypeReference(draftBuilder)) + .hasFailedWithThrowableThat() + .isExactlyInstanceOf(ReferenceResolutionException.class) + .hasMessage(format("Failed to resolve custom type reference on ShoppingListDraft" + + " with key:'null'. Reason: %s", BLANK_KEY_VALUE_ON_RESOURCE_IDENTIFIER)); + } + + @Test + void resolveCustomerReference_WithCustomerKey_ShouldResolveCustomerReference() { + when(customerService.fetchCachedCustomerId("customerKey")) + .thenReturn(CompletableFuture.completedFuture(Optional.of("customerId"))); + + final ShoppingListDraftBuilder draftBuilder = + ShoppingListDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "NAME")) + .customer(ResourceIdentifier.ofKey("customerKey")) + .description(LocalizedString.of(Locale.ENGLISH, "DESCRIPTION")); + + final ShoppingListDraftBuilder resolvedDraft = referenceResolver + .resolveCustomerReference(draftBuilder) + .toCompletableFuture() + .join(); + + assertThat(resolvedDraft.getCustomer()).isNotNull(); + assertThat(resolvedDraft.getCustomer().getId()).isEqualTo("customerId"); + } + + @Test + void resolveCustomerReference_WithNullCustomerReference_ShouldNotResolveCustomerReference() { + final ShoppingListDraftBuilder draftBuilder = + ShoppingListDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "NAME")) + .customer((ResourceIdentifier) null) + .description(LocalizedString.of(Locale.ENGLISH, "DESCRIPTION")); + + final ShoppingListDraftBuilder referencesResolvedDraft = + referenceResolver.resolveCustomerReference(draftBuilder).toCompletableFuture().join(); + + assertThat(referencesResolvedDraft.getCustom()).isNull(); + } + + @Test + void resolveCustomerReference_WithExceptionOnCustomerGroupFetch_ShouldNotResolveReference() { + final ShoppingListDraftBuilder draftBuilder = + ShoppingListDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "NAME")) + .customer(ResourceIdentifier.ofKey("anyKey")) + .description(LocalizedString.of(Locale.ENGLISH, "DESCRIPTION")); + + final CompletableFuture> futureThrowingSphereException = new CompletableFuture<>(); + futureThrowingSphereException.completeExceptionally(new SphereException("CTP error on fetch")); + when(customerService.fetchCachedCustomerId(anyString())).thenReturn(futureThrowingSphereException); + + assertThat(referenceResolver.resolveCustomerReference(draftBuilder).toCompletableFuture()) + .hasFailedWithThrowableThat() + .isExactlyInstanceOf(SphereException.class) + .hasMessageContaining("CTP error on fetch"); + } + + @Test + void resolveCustomerReference_WithNullCustomerKey_ShouldNotResolveCustomerReference() { + final ShoppingListDraftBuilder draftBuilder = + ShoppingListDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "NAME")) + .customer(ResourceIdentifier.ofKey(null)) + .description(LocalizedString.of(Locale.ENGLISH, "DESCRIPTION")); + + assertThat(referenceResolver.resolveCustomerReference(draftBuilder).toCompletableFuture()) + .hasFailedWithThrowableThat() + .isExactlyInstanceOf(ReferenceResolutionException.class) + .hasMessage(format(FAILED_TO_RESOLVE_CUSTOMER_REFERENCE, draftBuilder.getKey(), + BLANK_KEY_VALUE_ON_RESOURCE_IDENTIFIER)); + } + + @Test + void resolveCustomerReference_WithEmptyCustomerKey_ShouldNotResolveCustomerReference() { + final ShoppingListDraftBuilder draftBuilder = + ShoppingListDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "NAME")) + .customer(ResourceIdentifier.ofKey(" ")) + .description(LocalizedString.of(Locale.ENGLISH, "DESCRIPTION")); + + assertThat(referenceResolver.resolveCustomerReference(draftBuilder).toCompletableFuture()) + .hasFailedWithThrowableThat() + .isExactlyInstanceOf(ReferenceResolutionException.class) + .hasMessage(format(FAILED_TO_RESOLVE_CUSTOMER_REFERENCE, draftBuilder.getKey(), + BLANK_KEY_VALUE_ON_RESOURCE_IDENTIFIER)); + } + + @Test + void resolveCustomerReference_WithNonExistingCustomerKey_ShouldNotResolveCustomerReference() { + when(customerService.fetchCachedCustomerId(anyString())) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + final String customerKey = "non-existing-customer-key"; + final ShoppingListDraftBuilder draftBuilder = + ShoppingListDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "NAME")) + .customer(ResourceIdentifier.ofKey(customerKey)) + .description(LocalizedString.of(Locale.ENGLISH, "DESCRIPTION")); + + assertThat(referenceResolver.resolveCustomerReference(draftBuilder).toCompletableFuture()) + .hasFailedWithThrowableThat() + .isExactlyInstanceOf(ReferenceResolutionException.class) + .hasMessage(format(ShoppingListReferenceResolver.FAILED_TO_RESOLVE_CUSTOMER_REFERENCE, + draftBuilder.getKey(), format(CUSTOMER_DOES_NOT_EXIST, customerKey))); + } + + @Test + void resolveCustomerReference_WithIdOnCustomerReference_ShouldNotResolveReference() { + final ShoppingListDraftBuilder draftBuilder = + ShoppingListDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "NAME")) + .customer(ResourceIdentifier.ofId("existingId")) + .description(LocalizedString.of(Locale.ENGLISH, "DESCRIPTION")); + + assertThat(referenceResolver.resolveCustomerReference(draftBuilder).toCompletableFuture()) + .hasNotFailed() + .isCompletedWithValueMatching(resolvedDraft -> + Objects.equals(resolvedDraft.getCustomer(), draftBuilder.getCustomer())); + } + + @Test + void resolveReferences_WithoutReferences_ShouldNotResolveReferences() { + final ShoppingListDraft shoppingListDraft = ShoppingListDraftBuilder + .of(LocalizedString.ofEnglish("name")) + .key("shoppingList-key") + .build(); + + final ShoppingListDraft resolvedDraft = referenceResolver + .resolveReferences(shoppingListDraft) + .toCompletableFuture() + .join(); + + assertThat(resolvedDraft).isEqualTo(shoppingListDraft); + } + + @Test + void resolveReferences_WithAllValidFieldsAndReferences_ShouldResolveReferences() { + final CustomFieldsDraft customFieldsDraft = CustomFieldsDraft + .ofTypeKeyAndJson("typeKey", new HashMap<>()); + + final ZonedDateTime addedAt = ZonedDateTime.now(); + final LineItemDraft lineItemDraft = + LineItemDraftBuilder.ofSku("variant-sku", 20L) + .custom(customFieldsDraft) + .addedAt(addedAt) + .build(); + + final TextLineItemDraft textLineItemDraft = + TextLineItemDraftBuilder + .of(LocalizedString.ofEnglish("textLineItemName"), 10L) + .description(LocalizedString.ofEnglish("desc")) + .custom(customFieldsDraft) + .addedAt(addedAt) + .build(); + + final ShoppingListDraft shoppingListDraft = ShoppingListDraftBuilder + .of(LocalizedString.ofEnglish("name")) + .key("shoppingList-key") + .description(LocalizedString.ofEnglish("desc")) + .slug(LocalizedString.ofEnglish("slug")) + .deleteDaysAfterLastModification(0) + .anonymousId("anonymousId") + .lineItems(asList(null, lineItemDraft)) + .textLineItems(asList(null, textLineItemDraft)) + .custom(CustomFieldsDraft.ofTypeKeyAndJson("typeKey", emptyMap())) + .customer(ResourceIdentifier.ofKey("customerKey")) + .build(); + + final ShoppingListDraft resolvedDraft = referenceResolver + .resolveReferences(shoppingListDraft) + .toCompletableFuture() + .join(); + + final ShoppingListDraft expectedDraft = ShoppingListDraftBuilder + .of(LocalizedString.ofEnglish("name")) + .key("shoppingList-key") + .description(LocalizedString.ofEnglish("desc")) + .slug(LocalizedString.ofEnglish("slug")) + .deleteDaysAfterLastModification(0) + .anonymousId("anonymousId") + .custom(CustomFieldsDraft.ofTypeIdAndJson("typeId", new HashMap<>())) + .customer(Customer.referenceOfId("customerId").toResourceIdentifier()) + .lineItems(singletonList(LineItemDraftBuilder + .ofSku("variant-sku", 20L) + .custom(CustomFieldsDraft.ofTypeIdAndJson("typeId", new HashMap<>())) + .addedAt(addedAt) + .build())) + .textLineItems(singletonList(TextLineItemDraftBuilder + .of(LocalizedString.ofEnglish("textLineItemName"), 10L) + .description(LocalizedString.ofEnglish("desc")) + .custom(CustomFieldsDraft.ofTypeIdAndJson("typeId", new HashMap<>())) + .addedAt(addedAt) + .build())) + .build(); + + assertThat(resolvedDraft).isEqualTo(expectedDraft); + } +} diff --git a/src/test/java/com/commercetools/sync/shoppinglists/helpers/TextLineItemReferenceResolverTest.java b/src/test/java/com/commercetools/sync/shoppinglists/helpers/TextLineItemReferenceResolverTest.java new file mode 100644 index 0000000000..04852f636b --- /dev/null +++ b/src/test/java/com/commercetools/sync/shoppinglists/helpers/TextLineItemReferenceResolverTest.java @@ -0,0 +1,156 @@ +package com.commercetools.sync.shoppinglists.helpers; + +import com.commercetools.sync.commons.exceptions.ReferenceResolutionException; +import com.commercetools.sync.services.TypeService; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.models.SphereException; +import io.sphere.sdk.shoppinglists.TextLineItemDraft; +import io.sphere.sdk.shoppinglists.TextLineItemDraftBuilder; +import io.sphere.sdk.types.CustomFieldsDraft; +import io.sphere.sdk.utils.CompletableFutureUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import static com.commercetools.sync.commons.MockUtils.getMockTypeService; +import static com.commercetools.sync.commons.helpers.BaseReferenceResolver.BLANK_KEY_VALUE_ON_RESOURCE_IDENTIFIER; +import static com.commercetools.sync.commons.helpers.CustomReferenceResolver.TYPE_DOES_NOT_EXIST; +import static com.commercetools.sync.shoppinglists.helpers.TextLineItemReferenceResolver.FAILED_TO_RESOLVE_CUSTOM_TYPE; +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TextLineItemReferenceResolverTest { + + private TypeService typeService; + + private TextLineItemReferenceResolver referenceResolver; + + /** + * Sets up the services and the options needed for reference resolution. + */ + @BeforeEach + void setup() { + typeService = getMockTypeService(); + + final ShoppingListSyncOptions syncOptions = ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)).build(); + referenceResolver = new TextLineItemReferenceResolver(syncOptions, typeService); + } + + @Test + void resolveReferences_WithCustomTypeId_ShouldNotResolveCustomTypeReferenceWithKey() { + final String customTypeId = "customTypeId"; + final CustomFieldsDraft customFieldsDraft = CustomFieldsDraft + .ofTypeIdAndJson(customTypeId, new HashMap<>()); + + final TextLineItemDraft textLineItemDraft = + TextLineItemDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "dummy-custom-key"), 10L) + .custom(customFieldsDraft) + .build(); + + final TextLineItemDraft resolvedDraft = referenceResolver + .resolveReferences(textLineItemDraft) + .toCompletableFuture() + .join(); + + assertThat(resolvedDraft.getCustom()).isNotNull(); + assertThat(resolvedDraft.getCustom()).isEqualTo(customFieldsDraft); + } + + @Test + void resolveReferences_WithNonNullKeyOnCustomTypeResId_ShouldResolveCustomTypeReference() { + final CustomFieldsDraft customFieldsDraft = CustomFieldsDraft + .ofTypeKeyAndJson("customTypeKey", new HashMap<>()); + + final TextLineItemDraft textLineItemDraft = + TextLineItemDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "dummy-custom-key"), 10L) + .custom(customFieldsDraft) + .build(); + + final TextLineItemDraft resolvedDraft = referenceResolver + .resolveReferences(textLineItemDraft) + .toCompletableFuture() + .join(); + + assertThat(resolvedDraft.getCustom()).isNotNull(); + assertThat(resolvedDraft.getCustom().getType().getId()).isEqualTo("typeId"); + } + + @Test + void resolveReferences_WithExceptionOnCustomTypeFetch_ShouldNotResolveReferences() { + when(typeService.fetchCachedTypeId(anyString())) + .thenReturn(CompletableFutureUtils.failed(new SphereException("CTP error on fetch"))); + + final String customTypeKey = "customTypeKey"; + final CustomFieldsDraft customFieldsDraft = + CustomFieldsDraft.ofTypeKeyAndJson(customTypeKey, new HashMap<>()); + + final TextLineItemDraft textLineItemDraft = + TextLineItemDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "dummy-custom-key"), 10L) + .custom(customFieldsDraft) + .build(); + + assertThat(referenceResolver.resolveReferences(textLineItemDraft)) + .hasFailedWithThrowableThat() + .isExactlyInstanceOf(SphereException.class) + .hasMessageContaining("CTP error on fetch"); + } + + @Test + void resolveReferences_WithNonExistentCustomType_ShouldCompleteExceptionally() { + final String customTypeKey = "customTypeKey"; + final CustomFieldsDraft customFieldsDraft = + CustomFieldsDraft.ofTypeKeyAndJson(customTypeKey, new HashMap<>()); + + final TextLineItemDraft textLineItemDraft = + TextLineItemDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "dummy-custom-key"), 10L) + .custom(customFieldsDraft) + .build(); + + when(typeService.fetchCachedTypeId(anyString())) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + final String expectedExceptionMessage = format(FAILED_TO_RESOLVE_CUSTOM_TYPE, textLineItemDraft.getName()); + + final String expectedMessageWithCause = + format("%s Reason: %s", expectedExceptionMessage, format(TYPE_DOES_NOT_EXIST, customTypeKey)); + + assertThat(referenceResolver.resolveReferences(textLineItemDraft)) + .hasFailedWithThrowableThat() + .isExactlyInstanceOf(ReferenceResolutionException.class) + .hasMessage(expectedMessageWithCause); + } + + @Test + void resolveReferences_WithEmptyKeyOnCustomTypeResId_ShouldCompleteExceptionally() { + final CustomFieldsDraft customFieldsDraft = CustomFieldsDraft + .ofTypeKeyAndJson("", new HashMap<>()); + + final TextLineItemDraft textLineItemDraft = + TextLineItemDraftBuilder + .of(LocalizedString.of(Locale.ENGLISH, "dummy-custom-key"), 10L) + .custom(customFieldsDraft) + .build(); + + assertThat(referenceResolver.resolveReferences(textLineItemDraft)) + .hasFailedWithThrowableThat() + .isExactlyInstanceOf(ReferenceResolutionException.class) + .hasMessage(format("Failed to resolve custom type reference on TextLineItemDraft" + + " with name: '%s'. Reason: %s", LocalizedString.of(Locale.ENGLISH, "dummy-custom-key"), + BLANK_KEY_VALUE_ON_RESOURCE_IDENTIFIER)); + + } +} diff --git a/src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListsReferenceResolutionUtilsTest.java b/src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListsReferenceResolutionUtilsTest.java new file mode 100644 index 0000000000..2653619f13 --- /dev/null +++ b/src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListsReferenceResolutionUtilsTest.java @@ -0,0 +1,193 @@ +package com.commercetools.sync.shoppinglists.utils; + +import io.sphere.sdk.customers.Customer; +import io.sphere.sdk.expansion.ExpansionPath; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.models.Reference; +import io.sphere.sdk.products.ProductVariant; +import io.sphere.sdk.shoppinglists.LineItem; +import io.sphere.sdk.shoppinglists.LineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.ShoppingListDraftBuilder; +import io.sphere.sdk.shoppinglists.TextLineItem; +import io.sphere.sdk.shoppinglists.TextLineItemDraftBuilder; +import io.sphere.sdk.types.CustomFields; +import io.sphere.sdk.types.CustomFieldsDraft; +import io.sphere.sdk.types.Type; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static com.commercetools.sync.commons.MockUtils.getMockCustomer; +import static com.commercetools.sync.commons.MockUtils.getTypeMock; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListReferenceResolutionUtils.buildShoppingListQuery; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListReferenceResolutionUtils.mapToShoppingListDrafts; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ShoppingListsReferenceResolutionUtilsTest { + + @Test + void mapToShoppingListDrafts_WithExpandedReferences_ShouldReturnResourceIdentifiersWithKeys() { + final Type mockCustomType = getTypeMock(UUID.randomUUID().toString(), "customTypeKey"); + final Customer mockCustomer = getMockCustomer(UUID.randomUUID().toString(), "customerKey"); + final ProductVariant mockProductVariant = mock(ProductVariant.class); + when(mockProductVariant.getSku()).thenReturn("variant-sku"); + + final List mockShoppingLists = new ArrayList<>(); + mockShoppingLists.add(null); + + for (int i = 0; i < 3; i++) { + final ShoppingList mockShoppingList = mock(ShoppingList.class); + + final Reference customerReference = + Reference.ofResourceTypeIdAndObj(Customer.referenceTypeId(), mockCustomer); + when(mockShoppingList.getCustomer()).thenReturn(customerReference); + + final CustomFields mockCustomFields = mock(CustomFields.class); + final Reference typeReference = + Reference.ofResourceTypeIdAndObj(Type.referenceTypeId(), mockCustomType); + + when(mockCustomFields.getType()).thenReturn(typeReference); + when(mockShoppingList.getCustom()).thenReturn(mockCustomFields); + + final LineItem mockLineItem = mock(LineItem.class); + when(mockLineItem.getVariant()).thenReturn(mockProductVariant); + when(mockLineItem.getCustom()).thenReturn(mockCustomFields); + + when(mockShoppingList.getLineItems()) + .thenReturn(singletonList(mockLineItem)); + + final TextLineItem mockTextLineItem = mock(TextLineItem.class); + when(mockTextLineItem.getName()).thenReturn(LocalizedString.ofEnglish("textLineItemName")); + when(mockTextLineItem.getCustom()).thenReturn(mockCustomFields); + + when(mockShoppingList.getTextLineItems()).thenReturn(singletonList(mockTextLineItem)); + + mockShoppingLists.add(mockShoppingList); + } + + final List shoppingListDrafts = mapToShoppingListDrafts(mockShoppingLists); + + assertThat(shoppingListDrafts).hasSize(3); + shoppingListDrafts.forEach(draft -> { + assertThat(draft.getCustomer().getKey()).isEqualTo("customerKey"); + assertThat(draft.getCustom().getType().getKey()).isEqualTo("customTypeKey"); + + assertThat(draft.getLineItems()).containsExactly( + LineItemDraftBuilder + .ofSku("variant-sku", 0L) + .custom(CustomFieldsDraft.ofTypeKeyAndJson("customTypeKey", emptyMap())) + .build()); + + assertThat(draft.getTextLineItems()).containsExactly( + TextLineItemDraftBuilder + .of(LocalizedString.ofEnglish("textLineItemName"), 0L) + .custom(CustomFieldsDraft.ofTypeKeyAndJson("customTypeKey", emptyMap())) + .build()); + }); + } + + @Test + void mapToShoppingListDrafts_WithNonExpandedReferences_ShouldReturnResourceIdentifiersWithoutReferencedKeys() { + final String customTypeId = UUID.randomUUID().toString(); + final String customerId = UUID.randomUUID().toString(); + + + final List mockShoppingLists = new ArrayList<>(); + mockShoppingLists.add(null); + + for (int i = 0; i < 3; i++) { + final ShoppingList mockShoppingList = mock(ShoppingList.class); + + final Reference customerReference = + Reference.ofResourceTypeIdAndId(Customer.referenceTypeId(), customerId); + when(mockShoppingList.getCustomer()).thenReturn(customerReference); + + final CustomFields mockCustomFields = mock(CustomFields.class); + final Reference typeReference = Reference.ofResourceTypeIdAndId(Type.referenceTypeId(), + customTypeId); + when(mockCustomFields.getType()).thenReturn(typeReference); + when(mockShoppingList.getCustom()).thenReturn(mockCustomFields); + + final LineItem mockLineItemWithNullVariant = mock(LineItem.class); + when(mockLineItemWithNullVariant.getVariant()).thenReturn(null); + + when(mockShoppingList.getLineItems()) + .thenReturn(singletonList(mockLineItemWithNullVariant)); + + final TextLineItem mockTextLineItem = mock(TextLineItem.class); + when(mockTextLineItem.getName()).thenReturn(LocalizedString.ofEnglish("textLineItemName")); + when(mockTextLineItem.getCustom()).thenReturn(mockCustomFields); + + when(mockShoppingList.getTextLineItems()).thenReturn(singletonList(mockTextLineItem)); + + mockShoppingLists.add(mockShoppingList); + } + + final List shoppingListDrafts = mapToShoppingListDrafts(mockShoppingLists); + + assertThat(shoppingListDrafts).hasSize(3); + shoppingListDrafts.forEach(draft -> { + assertThat(draft.getCustomer().getId()).isEqualTo(customerId); + assertThat(draft.getCustom().getType().getKey()).isNull(); + + assertThat(draft.getLineItems()).isEqualTo(emptyList()); + + assertThat(draft.getTextLineItems()).containsExactly( + TextLineItemDraftBuilder + .of(LocalizedString.ofEnglish("textLineItemName"), 0L) + .custom(CustomFieldsDraft.ofTypeIdAndJson(customTypeId, emptyMap())) + .build()); + }); + } + + @Test + void mapToShoppingListDrafts_WithOtherFields_ShouldReturnDraftsCorrectly() { + + final ShoppingList mockShoppingList = mock(ShoppingList.class); + when(mockShoppingList.getName()).thenReturn(LocalizedString.ofEnglish("name")); + when(mockShoppingList.getDescription()).thenReturn(LocalizedString.ofEnglish("desc")); + when(mockShoppingList.getKey()).thenReturn("key"); + when(mockShoppingList.getSlug()).thenReturn(LocalizedString.ofEnglish("slug")); + when(mockShoppingList.getDeleteDaysAfterLastModification()).thenReturn(2); + when(mockShoppingList.getAnonymousId()).thenReturn("anonymousId"); + + when(mockShoppingList.getCustomer()).thenReturn(null); + when(mockShoppingList.getCustom()).thenReturn(null); + when(mockShoppingList.getLineItems()).thenReturn(null); + when(mockShoppingList.getTextLineItems()).thenReturn(null); + + final List shoppingListDrafts = + mapToShoppingListDrafts(singletonList(mockShoppingList)); + + assertThat(shoppingListDrafts).containsExactly( + ShoppingListDraftBuilder + .of(LocalizedString.ofEnglish("name")) + .description(LocalizedString.ofEnglish("desc")) + .key("key") + .slug(LocalizedString.ofEnglish("slug")) + .deleteDaysAfterLastModification(2) + .anonymousId("anonymousId") + .build() + ); + } + + @Test + void buildShoppingListQuery_Always_ShouldReturnQueryWithAllNeededReferencesExpanded() { + assertThat(buildShoppingListQuery().expansionPaths()) + .containsExactly( + ExpansionPath.of("customer"), + ExpansionPath.of("custom.type"), + ExpansionPath.of("lineItems[*].variant"), + ExpansionPath.of("lineItems[*].custom.type"), + ExpansionPath.of("textLineItems[*].custom.type")); + } +} From a01d00996d0d8072b9e8090371834c027b33a6a6 Mon Sep 17 00:00:00 2001 From: salander85 <70885646+salander85@users.noreply.github.com> Date: Thu, 22 Oct 2020 16:27:56 +0200 Subject: [PATCH 02/12] Create shoppinglist sync utils and helper (#595) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add shoppingList sync options, statistics and validator * Add ShoppingListService * Add user guide for CustomerSync * Start release notes for customer sync * Fix typos * Improvements to user guide and release notes * Correct javadoc * Reformatting * Modify indentation in ShoppingListSyncOptionsBuilderTest * Collect all referenced keys and validate name * Validate lineItems and textlineItems * Unit tests coverage * Add shoppingList sync options, statistics and validator * Correct javadoc * Reformatting * Modify indentation in ShoppingListSyncOptionsBuilderTest * Collect all referenced keys and validate name * Validate lineItems and textlineItems * Unit tests coverage * Correct reference type * Fix checkstyle violations * Cover more test cases * Correct merge conflicts * Update javadoc Co-authored-by: Ahmet Öz * Validate LocalizedString in separate method * Cleanup code * Remove nullchecks * Make checkstyle happy * Renaming * One callback for lineItem and textlineItem errors * Implement shoppinglist service (#605) * Add shoppingList sync options, statistics and validator * Correct javadoc * Implement shoppinglist service * Correct javadoc Co-authored-by: King-Hin Leung <35692276+leungkinghin@users.noreply.github.com> * Use method reference to shorten code * Change using not deprecated method Co-authored-by: King-Hin Leung <35692276+leungkinghin@users.noreply.github.com> * Update test scenarios * Formatting Co-authored-by: King-Hin Leung <35692276+leungkinghin@users.noreply.github.com> Co-authored-by: aoz Co-authored-by: King-Hin Leung Co-authored-by: ninalindl Co-authored-by: Ahmet Öz Co-authored-by: King-Hin Leung <35692276+leungkinghin@users.noreply.github.com> --- .../commons/utils/ShoppingListITUtils.java | 60 ++++ .../impl/ShoppingListServiceImplIT.java | 274 +++++++++++++++ .../sync/services/ShoppingListService.java | 97 ++++++ .../impl/ShoppingListServiceImpl.java | 80 +++++ .../ShoppingListSyncOptionsBuilder.java | 16 +- .../helpers/ShoppingListBatchValidator.java | 225 +++++++++++++ .../helpers/ShoppingListSyncStatistics.java | 18 + .../impl/ShoppingListServiceImplTest.java | 274 +++++++++++++++ .../ShoppingListSyncOptionsBuilderTest.java | 285 ++++++++++++++++ .../ShoppingListBatchValidatorTest.java | 314 ++++++++++++++++++ .../ShoppingListSyncStatisticsTest.java | 27 ++ 11 files changed, 1662 insertions(+), 8 deletions(-) create mode 100644 src/integration-test/java/com/commercetools/sync/integration/commons/utils/ShoppingListITUtils.java create mode 100644 src/integration-test/java/com/commercetools/sync/integration/services/impl/ShoppingListServiceImplIT.java create mode 100644 src/main/java/com/commercetools/sync/services/ShoppingListService.java create mode 100644 src/main/java/com/commercetools/sync/services/impl/ShoppingListServiceImpl.java create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidator.java create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListSyncStatistics.java create mode 100644 src/test/java/com/commercetools/sync/services/impl/ShoppingListServiceImplTest.java create mode 100644 src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptionsBuilderTest.java create mode 100644 src/test/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidatorTest.java create mode 100644 src/test/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListSyncStatisticsTest.java diff --git a/src/integration-test/java/com/commercetools/sync/integration/commons/utils/ShoppingListITUtils.java b/src/integration-test/java/com/commercetools/sync/integration/commons/utils/ShoppingListITUtils.java new file mode 100644 index 0000000000..f1b54f1ac6 --- /dev/null +++ b/src/integration-test/java/com/commercetools/sync/integration/commons/utils/ShoppingListITUtils.java @@ -0,0 +1,60 @@ +package com.commercetools.sync.integration.commons.utils; + +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.ShoppingListDraftBuilder; +import io.sphere.sdk.shoppinglists.commands.ShoppingListCreateCommand; +import io.sphere.sdk.shoppinglists.commands.ShoppingListDeleteCommand; +import io.sphere.sdk.shoppinglists.queries.ShoppingListQuery; + +import javax.annotation.Nonnull; + +import static com.commercetools.sync.integration.commons.utils.ITUtils.queryAndExecute; +import static com.commercetools.sync.integration.commons.utils.ProductITUtils.deleteAllProducts; +import static com.commercetools.sync.integration.commons.utils.ProductTypeITUtils.deleteProductTypes; +import static com.commercetools.tests.utils.CompletionStageUtil.executeBlocking; + +public final class ShoppingListITUtils { + + /** + * Deletes all shopping lists, products and product types from the CTP project defined by the {@code ctpClient}. + * + * @param ctpClient defines the CTP project to delete test data from. + */ + public static void deleteShoppingListTestData(@Nonnull final SphereClient ctpClient) { + deleteShoppingLists(ctpClient); + deleteAllProducts(ctpClient); + deleteProductTypes(ctpClient); + } + + /** + * Deletes all ShoppingLists from the CTP project defined by the {@code ctpClient}. + * + * @param ctpClient defines the CTP project to delete the ShoppingLists from. + */ + public static void deleteShoppingLists(@Nonnull final SphereClient ctpClient) { + queryAndExecute(ctpClient, ShoppingListQuery.of(), ShoppingListDeleteCommand::of); + } + + /** + * Creates a {@link ShoppingList} in the CTP project defined byf the {@code ctpClient} in a blocking fashion. + * + * @param ctpClient defines the CTP project to create the ShoppingList in. + * @param name the name of the ShoppingList to create. + * @param key the key of the ShoppingList to create. + * @return the created ShoppingList. + */ + public static ShoppingList createShoppingList(@Nonnull final SphereClient ctpClient, @Nonnull final String name, + @Nonnull final String key) { + final ShoppingListDraft shoppingListDraft = ShoppingListDraftBuilder.of(LocalizedString.ofEnglish(name)) + .key(key) + .build(); + + return executeBlocking(ctpClient.execute(ShoppingListCreateCommand.of(shoppingListDraft))); + } + + private ShoppingListITUtils() { + } +} diff --git a/src/integration-test/java/com/commercetools/sync/integration/services/impl/ShoppingListServiceImplIT.java b/src/integration-test/java/com/commercetools/sync/integration/services/impl/ShoppingListServiceImplIT.java new file mode 100644 index 0000000000..c3965f6a6b --- /dev/null +++ b/src/integration-test/java/com/commercetools/sync/integration/services/impl/ShoppingListServiceImplIT.java @@ -0,0 +1,274 @@ +package com.commercetools.sync.integration.services.impl; + +import com.commercetools.sync.services.ShoppingListService; +import com.commercetools.sync.services.impl.ShoppingListServiceImpl; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.models.ResourceIdentifier; +import io.sphere.sdk.products.ProductDraft; +import io.sphere.sdk.products.ProductDraftBuilder; +import io.sphere.sdk.products.ProductVariantDraftBuilder; +import io.sphere.sdk.products.commands.ProductCreateCommand; +import io.sphere.sdk.producttypes.ProductType; +import io.sphere.sdk.shoppinglists.LineItemDraft; +import io.sphere.sdk.shoppinglists.LineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.ShoppingListDraftBuilder; +import io.sphere.sdk.shoppinglists.ShoppingListDraftDsl; +import io.sphere.sdk.shoppinglists.TextLineItemDraft; +import io.sphere.sdk.shoppinglists.TextLineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeName; +import io.sphere.sdk.shoppinglists.queries.ShoppingListQuery; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.commercetools.sync.integration.commons.utils.ProductTypeITUtils.createProductType; +import static com.commercetools.sync.integration.commons.utils.ShoppingListITUtils.createShoppingList; +import static com.commercetools.sync.integration.commons.utils.ShoppingListITUtils.deleteShoppingListTestData; +import static com.commercetools.sync.integration.commons.utils.ShoppingListITUtils.deleteShoppingLists; +import static com.commercetools.sync.integration.commons.utils.SphereClientUtils.CTP_TARGET_CLIENT; +import static com.commercetools.sync.products.ProductSyncMockUtils.PRODUCT_TYPE_RESOURCE_PATH; +import static com.commercetools.tests.utils.CompletionStageUtil.executeBlocking; +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class ShoppingListServiceImplIT { + private ShoppingListService shoppingListService; + + private ShoppingList shoppingList; + private List errorCallBackMessages; + private List errorCallBackExceptions; + + /** + * Deletes shopping list and products from the target CTP projects, then it populates the project with test data. + */ + @BeforeEach + void setup() { + deleteShoppingListTestData(CTP_TARGET_CLIENT); + errorCallBackMessages = new ArrayList<>(); + errorCallBackExceptions = new ArrayList<>(); + shoppingList = createShoppingList(CTP_TARGET_CLIENT, "name", "key"); + final ShoppingListSyncOptions options = + ShoppingListSyncOptionsBuilder.of(CTP_TARGET_CLIENT) + .errorCallback((exception, oldResource, newResource, actions) -> { + errorCallBackMessages.add(exception.getMessage()); + errorCallBackExceptions.add(exception); + }) + .build(); + shoppingListService = new ShoppingListServiceImpl(options); + } + + /** + * Cleans up the target test data that were built in this test class. + */ + @AfterAll + static void tearDown() { + deleteShoppingLists(CTP_TARGET_CLIENT); + } + + @Test + void fetchShoppingList_WithNonExistingShoppingList_ShouldReturnEmptyOptional() { + final Optional shoppingList = + shoppingListService.fetchShoppingList("not-existing-key").toCompletableFuture().join(); + + assertThat(shoppingList).isEmpty(); + assertThat(errorCallBackExceptions).isEmpty(); + assertThat(errorCallBackMessages).isEmpty(); + + } + + @Test + void fetchShoppingList_WithExistingShoppingList_ShouldFetchShoppingList() { + final Optional shoppingList = + shoppingListService.fetchShoppingList(this.shoppingList.getKey()).toCompletableFuture().join(); + + assertThat(shoppingList).isNotEmpty(); + assertThat(errorCallBackExceptions).isEmpty(); + assertThat(errorCallBackMessages).isEmpty(); + } + + @Test + void fetchMatchingShoppingListsByKeys_WithNotExistingKeys_ShouldReturnEmptySet() { + final Set shoppingListKeys = new HashSet<>(); + shoppingListKeys.add("not_existing_key_1"); + shoppingListKeys.add("not_existing_key_2"); + Set shoppingLists = + shoppingListService.fetchMatchingShoppingListsByKeys(shoppingListKeys).toCompletableFuture().join(); + + assertThat(shoppingLists).isEmpty(); + assertThat(errorCallBackMessages).isEmpty(); + assertThat(errorCallBackExceptions).isEmpty(); + } + + @Test + void fetchMatchingShoppingListsByKeys_WithExistingShoppingListsKeys_ShouldReturnShoppingLists() { + ShoppingList otherShoppingList = createShoppingList(CTP_TARGET_CLIENT, "other_name", "other_key"); + final Set shoppingListKeys = new HashSet<>(); + shoppingListKeys.add(shoppingList.getKey()); + shoppingListKeys.add(otherShoppingList.getKey()); + Set shoppingLists = + shoppingListService.fetchMatchingShoppingListsByKeys(shoppingListKeys) + .toCompletableFuture() + .join(); + + + assertThat(shoppingLists).hasSize(2); + assertThat(errorCallBackMessages).isEmpty(); + assertThat(errorCallBackExceptions).isEmpty(); + } + + @Test + void cacheKeysToIds_WithEmptyKeys_ShouldReturnCurrentCache() { + Map cache = shoppingListService.cacheKeysToIds(emptySet()).toCompletableFuture().join(); + assertThat(cache).hasSize(0); + + cache = shoppingListService.cacheKeysToIds(singleton(shoppingList.getKey())).toCompletableFuture().join(); + assertThat(cache).hasSize(1); + + cache = shoppingListService.cacheKeysToIds(emptySet()).toCompletableFuture().join(); + assertThat(cache).hasSize(1); + + assertThat(errorCallBackExceptions).isEmpty(); + assertThat(errorCallBackMessages).isEmpty(); + } + + @Test + void cacheKeysToIds_WithCachedKeys_ShouldReturnCachedKeysWithoutRequest() { + final SphereClient spyClient = spy(CTP_TARGET_CLIENT); + final ShoppingListSyncOptions shoppingListSyncOptions = + ShoppingListSyncOptionsBuilder.of(spyClient) + .errorCallback((exception, oldResource, newResource, updateActions) -> { + errorCallBackMessages.add(exception.getMessage()); + errorCallBackExceptions.add(exception.getCause()); + }) + .build(); + final ShoppingListService shoppingListService = new ShoppingListServiceImpl(shoppingListSyncOptions); + + + Map cache = shoppingListService.cacheKeysToIds(singleton(shoppingList.getKey())) + .toCompletableFuture().join(); + assertThat(cache).hasSize(1); + + cache = shoppingListService.cacheKeysToIds(singleton(shoppingList.getKey())) + .toCompletableFuture().join(); + assertThat(cache).hasSize(1); + + verify(spyClient, times(1)).execute(any()); + assertThat(errorCallBackExceptions).isEmpty(); + assertThat(errorCallBackMessages).isEmpty(); + } + + @Test + void createShoppingList_WithValidShoppingList_ShouldCreateShoppingList() { + //preparation + ProductType productType = createProductType(PRODUCT_TYPE_RESOURCE_PATH, CTP_TARGET_CLIENT); + final ProductDraft productDraft = ProductDraftBuilder + .of(ResourceIdentifier.ofKey(productType.getKey()), LocalizedString.ofEnglish("newProduct"), + LocalizedString.ofEnglish("foo"), + ProductVariantDraftBuilder.of().key("foo-new").sku("sku-new").build()) + .key("newProduct") + .build(); + executeBlocking(CTP_TARGET_CLIENT.execute(ProductCreateCommand.of(productDraft))); + LineItemDraft lineItemDraft = LineItemDraftBuilder.ofSku("sku-new", Long.valueOf(1)).build(); + TextLineItemDraft textLineItemDraft = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("text"), 1L).build(); + final ShoppingListDraftDsl newShoppingListDraft = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("new_name")) + .key("new_key") + .plusLineItems(lineItemDraft) + .plusTextLineItems(textLineItemDraft) + .build(); + + final SphereClient spyClient = spy(CTP_TARGET_CLIENT); + + final ShoppingListSyncOptions options = + ShoppingListSyncOptionsBuilder.of(spyClient) + .errorCallback((exception, oldResource, newResource, actions) -> { + errorCallBackMessages.add(exception.getMessage()); + errorCallBackExceptions.add(exception); + }) + .build(); + ShoppingListService spyShoppingListService = new ShoppingListServiceImpl(options); + + //test + final Optional createdShoppingList = + spyShoppingListService.createShoppingList(newShoppingListDraft).toCompletableFuture().join(); + + final Optional queriedOptional = CTP_TARGET_CLIENT + .execute(ShoppingListQuery.of().withPredicates(shoppingListQueryModel -> + shoppingListQueryModel.key().is("new_key"))) + .toCompletableFuture().join().head(); + + assertThat(queriedOptional).hasValueSatisfying(queried -> + assertThat(createdShoppingList).hasValueSatisfying(created -> { + assertThat(created.getKey()).isEqualTo(queried.getKey()); + assertThat(created.getName()).isEqualTo(queried.getName()); + assertThat(created.getLineItems()).hasSize(1); + assertThat(created.getTextLineItems()).hasSize(1); + })); + } + + @Test + void createShoppingList_WithNotExistingSkuInLineItem_ShouldNotCreateShoppingList() { + // preparation + LineItemDraft lineItemDraft = LineItemDraftBuilder.ofSku("unknownSku", Long.valueOf(1)).build(); + final ShoppingListDraft newShoppingListDraft = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("new_name")) + .key("new_key") + .plusLineItems(lineItemDraft) + .build(); + + final Optional createdShoppingListOptional = shoppingListService + .createShoppingList(newShoppingListDraft) + .toCompletableFuture().join(); + + assertThat(createdShoppingListOptional).isEmpty(); + assertThat(errorCallBackExceptions).hasSize(1); + assertThat(errorCallBackMessages) + .hasSize(1); + assertThat(errorCallBackMessages.get(0)).contains("Failed to create draft with key: 'new_key'. Reason: " + + "detailMessage: No published product with an sku 'unknownSku' exists."); + } + + @Test + void updateCustomer_WithValidChanges_ShouldUpdateCustomerCorrectly() { + final ChangeName updatedName = ChangeName.of(LocalizedString.ofEnglish("updated_name")); + + final ShoppingList updatedShoppingList = + shoppingListService.updateShoppingList(shoppingList, singletonList(updatedName)) + .toCompletableFuture().join(); + assertThat(updatedShoppingList).isNotNull(); + + final Optional queried = CTP_TARGET_CLIENT + .execute(ShoppingListQuery.of().withPredicates(shoppingListQueryModel -> + shoppingListQueryModel.key().is(shoppingList.getKey()))) + .toCompletableFuture().join().head(); + + assertThat(errorCallBackExceptions).isEmpty(); + assertThat(errorCallBackMessages).isEmpty(); + assertThat(queried).isNotEmpty(); + final ShoppingList fetchedShoppingList = queried.get(); + assertThat(fetchedShoppingList.getKey()).isEqualTo(updatedShoppingList.getKey()); + assertThat(fetchedShoppingList.getName()).isEqualTo(updatedShoppingList.getName()); + + } + + +} diff --git a/src/main/java/com/commercetools/sync/services/ShoppingListService.java b/src/main/java/com/commercetools/sync/services/ShoppingListService.java new file mode 100644 index 0000000000..a5824f7e4f --- /dev/null +++ b/src/main/java/com/commercetools/sync/services/ShoppingListService.java @@ -0,0 +1,97 @@ +package com.commercetools.sync.services; + +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletionStage; + +public interface ShoppingListService { + + /** + * Filters out the keys which are already cached and fetches only the not-cached shopping list keys from the CTP + * project defined in an injected {@link SphereClient} and stores a mapping for every shopping list to id in + * the cached map of keys -> ids and returns this cached map. + * + *

Note: If all the supplied keys are already cached, the cached map is returned right away with no request to + * CTP. + * + * @param shoppingListKeys the shopping list keys to fetch and cache the ids for. + * + * @return {@link CompletionStage}<{@link Map}> in which the result of it's completion contains a map of all + * shopping list keys -> ids + */ + @Nonnull + CompletionStage> cacheKeysToIds(@Nonnull Set shoppingListKeys); + + /** + * Given a {@link Set} of shopping list keys, this method fetches a set of all the shopping lists, matching given + * set of keys in the CTP project, defined in an injected {@link SphereClient}. A mapping of the key to the id + * of the fetched shopping lists is persisted in an in-memory map. + * + * @param keys set of shopping list keys to fetch matching shopping lists by. + * @return {@link CompletionStage}<{@link Map}> in which the result of it's completion contains a {@link Set} + * of all matching shopping lists. + */ + @Nonnull + CompletionStage> fetchMatchingShoppingListsByKeys(@Nonnull final Set keys); + + /** + * Given a shopping list key, this method fetches a shopping list that matches given key in the CTP project defined + * in a potentially injected {@link SphereClient}. If there is no matching shopping list, an empty {@link Optional} + * will be returned in the returned future. A mapping of the key to the id of the fetched shopping list is persisted + * in an in-memory map. + * + * @param key the key of the shopping list to fetch. + * @return {@link CompletionStage}<{@link Optional}> in which the result of it's completion contains an + * {@link Optional} that contains the matching {@link ShoppingList} if exists, otherwise empty. + */ + @Nonnull + CompletionStage> fetchShoppingList(@Nullable final String key); + + /** + * Given a resource draft of type {@link ShoppingListDraft}, this method attempts to create a resource + * {@link ShoppingList} based on it in the CTP project defined by the sync options. + * + *

A completion stage containing an empty option and the error callback will be triggered in those cases: + *

    + *
  • the draft has a blank key
  • + *
  • the create request fails on CTP
  • + *
+ * + *

On the other hand, if the resource gets created successfully on CTP, then the created resource's id and + * key are cached and the method returns a {@link CompletionStage} in which the result of it's completion + * contains an instance {@link Optional} of the resource which was created. + * + * @param shoppingListDraft the resource draft to create a resource based off of. + * @return a {@link CompletionStage} containing an optional with the created resource if successful otherwise an + * empty optional. + */ + @Nonnull + CompletionStage> createShoppingList(@Nonnull final ShoppingListDraft shoppingListDraft); + + /** + * Given a {@link ShoppingList} and a {@link List}<{@link UpdateAction}<{@link ShoppingList}>>, this + * method issues an update request with these update actions on this {@link ShoppingList} in the CTP project defined + * in a potentially injected {@link SphereClient}. This method returns {@link CompletionStage}< + * {@link ShoppingList}> in which the result of it's completion contains an instance of + * the {@link ShoppingList} which was updated in the CTP project. + * + * @param shoppingList the {@link ShoppingList} to update. + * @param updateActions the update actions to update the {@link ShoppingList} with. + * @return {@link CompletionStage}<{@link ShoppingList}> containing as a result of it's completion an instance + * of the {@link ShoppingList} which was updated in the CTP project or a + * {@link io.sphere.sdk.models.SphereException}. + */ + @Nonnull + CompletionStage updateShoppingList(@Nonnull final ShoppingList shoppingList, + @Nonnull final List> updateActions); + +} diff --git a/src/main/java/com/commercetools/sync/services/impl/ShoppingListServiceImpl.java b/src/main/java/com/commercetools/sync/services/impl/ShoppingListServiceImpl.java new file mode 100644 index 0000000000..935c7bd7f8 --- /dev/null +++ b/src/main/java/com/commercetools/sync/services/impl/ShoppingListServiceImpl.java @@ -0,0 +1,80 @@ +package com.commercetools.sync.services.impl; + +import com.commercetools.sync.services.ShoppingListService; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.commands.ShoppingListCreateCommand; +import io.sphere.sdk.shoppinglists.commands.ShoppingListUpdateCommand; +import io.sphere.sdk.shoppinglists.expansion.ShoppingListExpansionModel; +import io.sphere.sdk.shoppinglists.queries.ShoppingListQuery; +import io.sphere.sdk.shoppinglists.queries.ShoppingListQueryBuilder; +import io.sphere.sdk.shoppinglists.queries.ShoppingListQueryModel; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletionStage; + +/** + * Implementation of ShoppingListService interface. + */ +public final class ShoppingListServiceImpl extends BaseService> + implements ShoppingListService { + + public ShoppingListServiceImpl(@Nonnull final ShoppingListSyncOptions syncOptions) { + super(syncOptions); + } + + @Nonnull + @Override + public CompletionStage> cacheKeysToIds(@Nonnull final Set shoppingListKeys) { + + return cacheKeysToIds( + shoppingListKeys, ShoppingList::getKey, keysNotCached -> ShoppingListQueryBuilder + .of() + .plusPredicates(shoppingListQueryModel -> shoppingListQueryModel.key().isIn(keysNotCached)) + .build()); + } + + @Nonnull + @Override + public CompletionStage> fetchMatchingShoppingListsByKeys(@Nonnull final Set keys) { + + return fetchMatchingResources(keys, ShoppingList::getKey, + () -> ShoppingListQueryBuilder + .of() + .plusPredicates(queryModel -> queryModel.key().isIn(keys)) + .build()); + } + + @Nonnull + @Override + public CompletionStage> fetchShoppingList(@Nullable final String key) { + + return fetchResource(key, + () -> ShoppingListQueryBuilder.of().plusPredicates(queryModel -> queryModel.key().is(key)).build()); + } + + @Nonnull + @Override + public CompletionStage> createShoppingList( + @Nonnull final ShoppingListDraft shoppingListDraft) { + + return createResource(shoppingListDraft, ShoppingListDraft::getKey, ShoppingListCreateCommand::of); + } + + @Nonnull + @Override + public CompletionStage updateShoppingList( + @Nonnull final ShoppingList shoppingList, + @Nonnull final List> updateActions) { + + return updateResource(shoppingList, ShoppingListUpdateCommand::of, updateActions); + } +} diff --git a/src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptionsBuilder.java b/src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptionsBuilder.java index 87198b375b..49b780ea96 100644 --- a/src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptionsBuilder.java +++ b/src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptionsBuilder.java @@ -8,8 +8,8 @@ import javax.annotation.Nonnull; public final class ShoppingListSyncOptionsBuilder - extends BaseSyncOptionsBuilder { + extends BaseSyncOptionsBuilder { public static final int BATCH_SIZE_DEFAULT = 50; @@ -36,12 +36,12 @@ public static ShoppingListSyncOptionsBuilder of(@Nonnull final SphereClient ctpC @Override public ShoppingListSyncOptions build() { return new ShoppingListSyncOptions( - ctpClient, - errorCallback, - warningCallback, - batchSize, - beforeUpdateCallback, - beforeCreateCallback + ctpClient, + errorCallback, + warningCallback, + batchSize, + beforeUpdateCallback, + beforeCreateCallback ); } diff --git a/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidator.java b/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidator.java new file mode 100644 index 0000000000..b11cb47bbe --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidator.java @@ -0,0 +1,225 @@ +package com.commercetools.sync.shoppinglists.helpers; + +import com.commercetools.sync.commons.helpers.BaseBatchValidator; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.shoppinglists.LineItemDraft; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.TextLineItemDraft; +import org.apache.commons.lang3.tuple.ImmutablePair; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; +import static org.apache.commons.lang3.StringUtils.isBlank; + +public class ShoppingListBatchValidator + extends BaseBatchValidator { + + static final String SHOPPING_LIST_DRAFT_KEY_NOT_SET = "ShoppingListDraft with name: %s doesn't have a key. " + + "Please make sure all shopping list drafts have keys."; + static final String SHOPPING_LIST_DRAFT_IS_NULL = "ShoppingListDraft is null."; + static final String SHOPPING_LIST_DRAFT_NAME_NOT_SET = "ShoppingListDraft with key: %s doesn't have a name. " + + "Please make sure all shopping list drafts have names."; + static final String LINE_ITEM_DRAFT_IS_NULL = "LineItemDraft at position '%d' of ShoppingListDraft " + + "with key '%s' is null."; + static final String LINE_ITEM_DRAFT_SKU_NOT_SET = "LineItemDraft at position '%d' of " + + "ShoppingListDraft with key '%s' has no SKU set. Please make sure all lineItems have SKUs."; + static final String TEXT_LINE_ITEM_DRAFT_IS_NULL = "TextLineItemDraft at position '%d' of ShoppingListDraft " + + "with key '%s' is null."; + static final String TEXT_LINE_ITEM_DRAFT_NAME_NOT_SET = "TextLineItemDraft at position '%d' of " + + "ShoppingListDraft with key '%s' has no name set. Please make sure all textLineItems have names."; + + public ShoppingListBatchValidator(@Nonnull final ShoppingListSyncOptions syncOptions, + @Nonnull final ShoppingListSyncStatistics syncStatistics) { + super(syncOptions, syncStatistics); + } + + /** + * Given the {@link List}<{@link ShoppingListDraft}> of drafts this method attempts to validate + * drafts and collect referenced type keys from the draft and return an {@link ImmutablePair}<{@link Set}< + * {@link ShoppingListDraft}>,{@link ReferencedKeys}> which contains the {@link Set} of valid drafts and + * referenced keys. + * + *

A valid shopping list draft is one which satisfies the following conditions: + *

    + *
  1. It is not null
  2. + *
  3. It has a key which is not blank (null/empty)
  4. + *
  5. It has a name which is not null
  6. + *
  7. It has all lineItems AND textLineItems valid
  8. + *
  9. A lineItem is valid if it satisfies the following conditions: + *
      + *
    1. It has a SKU which is not blank (null/empty)
    2. + *
    + *
  10. A textLineItem is valid if it satisfies the following conditions: + *
      + *
    1. It has a name which is not blank (null/empty)
    2. + *
    + *
  11. + *
+ * + * @param shoppingListDrafts the shopping list drafts to validate and collect referenced keys. + * @return {@link ImmutablePair}<{@link Set}<{@link ShoppingListDraft}>, + * {@link ReferencedKeys}> which contains the {@link Set} of valid drafts and + * referenced keys within a wrapper. + */ + @Override + public ImmutablePair, ReferencedKeys> validateAndCollectReferencedKeys( + @Nonnull final List shoppingListDrafts) { + final ReferencedKeys referencedKeys = new ReferencedKeys(); + final Set validDrafts = shoppingListDrafts + .stream() + .filter(this::isValidShoppingListDraft) + .peek(shoppingListDraft -> + collectReferencedKeys(referencedKeys, shoppingListDraft)) + .collect(toSet()); + + return ImmutablePair.of(validDrafts, referencedKeys); + } + + private boolean isValidShoppingListDraft( + @Nullable final ShoppingListDraft shoppingListDraft) { + + if (shoppingListDraft == null) { + handleError(SHOPPING_LIST_DRAFT_IS_NULL); + } else if (isBlank(shoppingListDraft.getKey())) { + handleError(format(SHOPPING_LIST_DRAFT_KEY_NOT_SET, shoppingListDraft.getName())); + } else if (isNullOrEmptyLocalizedString(shoppingListDraft.getName())) { + handleError(format(SHOPPING_LIST_DRAFT_NAME_NOT_SET, shoppingListDraft.getKey())); + } else { + final List draftErrors = getErrorsInAllLineItemsAndTextLineItems(shoppingListDraft); + if (!draftErrors.isEmpty()) { + final String concatenatedErrors = draftErrors.stream().collect(joining(",")); + this.handleError(concatenatedErrors); + } else { + return true; + } + } + + return false; + } + + + @Nonnull + private List getErrorsInAllLineItemsAndTextLineItems(@Nonnull final ShoppingListDraft shoppingListDraft) { + final List errorMessages = new ArrayList<>(); + + if (shoppingListDraft.getLineItems() != null) { + final List lineItemDrafts = shoppingListDraft.getLineItems(); + for (int i = 0; i < lineItemDrafts.size(); i++) { + errorMessages.addAll(getLineItemDraftErrorsInAllLineItems(lineItemDrafts.get(i), + i, shoppingListDraft.getKey())); + } + } + + if (shoppingListDraft.getTextLineItems() != null) { + final List textLineItems = shoppingListDraft.getTextLineItems(); + for (int i = 0; i < textLineItems.size(); i++) { + errorMessages.addAll(getTextLineItemDraftErrorsInAllTextLineItems(textLineItems.get(i), + i, shoppingListDraft.getKey())); + } + } + + return errorMessages; + } + + @Nonnull + private List getLineItemDraftErrorsInAllLineItems(@Nullable final LineItemDraft lineItemDraft, + final int itemPosition, + @Nonnull final String shoppingListDraftKey) { + final List errorMessages = new ArrayList<>(); + if (lineItemDraft != null) { + if (isBlank(lineItemDraft.getSku())) { + errorMessages.add(format(LINE_ITEM_DRAFT_SKU_NOT_SET, itemPosition, shoppingListDraftKey)); + } + } else { + errorMessages.add(format(LINE_ITEM_DRAFT_IS_NULL, itemPosition, shoppingListDraftKey)); + } + return errorMessages; + } + + @Nonnull + private List getTextLineItemDraftErrorsInAllTextLineItems( + @Nullable final TextLineItemDraft textLineItemDraft, final int itemPosition, + @Nonnull final String shoppingListDraftKey) { + final List errorMessages = new ArrayList<>(); + if (textLineItemDraft != null) { + if (isNullOrEmptyLocalizedString(textLineItemDraft.getName())) { + errorMessages.add(format(TEXT_LINE_ITEM_DRAFT_NAME_NOT_SET, itemPosition, shoppingListDraftKey)); + } + } else { + errorMessages.add(format(TEXT_LINE_ITEM_DRAFT_IS_NULL, itemPosition, shoppingListDraftKey)); + } + return errorMessages; + } + + private boolean isNullOrEmptyLocalizedString(@Nonnull final LocalizedString localizedString) { + return localizedString == null || localizedString.getLocales().isEmpty(); + } + + private void collectReferencedKeys( + @Nonnull final ReferencedKeys referencedKeys, + @Nonnull final ShoppingListDraft shoppingListDraft) { + + collectReferencedKeyFromResourceIdentifier(shoppingListDraft.getCustomer(), + referencedKeys.customerKeys::add); + collectReferencedKeyFromCustomFieldsDraft(shoppingListDraft.getCustom(), + referencedKeys.typeKeys::add); + collectReferencedKeysInLineItems(referencedKeys, shoppingListDraft); + collectReferencedKeysInTextLineItems(referencedKeys, shoppingListDraft); + } + + private void collectReferencedKeysInLineItems( + @Nonnull final ReferencedKeys referencedKeys, + @Nonnull final ShoppingListDraft shoppingListDraft) { + + if (shoppingListDraft.getLineItems() == null) { + return; + } + + shoppingListDraft + .getLineItems() + .stream() + .forEach(lineItemDraft -> { + collectReferencedKeyFromCustomFieldsDraft(lineItemDraft.getCustom(), + referencedKeys.typeKeys::add); + }); + } + + private void collectReferencedKeysInTextLineItems( + @Nonnull final ReferencedKeys referencedKeys, + @Nonnull final ShoppingListDraft shoppingListDraft) { + + if (shoppingListDraft.getTextLineItems() == null) { + return; + } + + shoppingListDraft + .getTextLineItems() + .stream() + .forEach(textLineItemDraft -> { + collectReferencedKeyFromCustomFieldsDraft(textLineItemDraft.getCustom(), + referencedKeys.typeKeys::add); + }); + } + + public static class ReferencedKeys { + private final Set customerKeys = new HashSet<>(); + private final Set typeKeys = new HashSet<>(); + + public Set getTypeKeys() { + return typeKeys; + } + + public Set getCustomerKeys() { + return customerKeys; + } + } +} diff --git a/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListSyncStatistics.java b/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListSyncStatistics.java new file mode 100644 index 0000000000..eee55b0235 --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListSyncStatistics.java @@ -0,0 +1,18 @@ +package com.commercetools.sync.shoppinglists.helpers; + +import com.commercetools.sync.commons.helpers.BaseSyncStatistics; + +public final class ShoppingListSyncStatistics extends BaseSyncStatistics { + + /** + * Builds a summary of the shopping list sync statistics instance that looks like the following example: + * + *

"Summary: 2 shopping lists were processed in total (0 created, 0 updated and 0 failed to sync)." + * + * @return a summary message of the shopping list sync statistics instance. + */ + @Override + public String getReportMessage() { + return getDefaultReportMessageForResource("shopping lists"); + } +} diff --git a/src/test/java/com/commercetools/sync/services/impl/ShoppingListServiceImplTest.java b/src/test/java/com/commercetools/sync/services/impl/ShoppingListServiceImplTest.java new file mode 100644 index 0000000000..f0b16db73d --- /dev/null +++ b/src/test/java/com/commercetools/sync/services/impl/ShoppingListServiceImplTest.java @@ -0,0 +1,274 @@ +package com.commercetools.sync.services.impl; + +import com.commercetools.sync.commons.exceptions.SyncException; +import com.commercetools.sync.services.ShoppingListService; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; +import io.sphere.sdk.client.BadGatewayException; +import io.sphere.sdk.client.InternalServerErrorException; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.queries.PagedQueryResult; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.commands.ShoppingListUpdateCommand; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeName; +import io.sphere.sdk.shoppinglists.queries.ShoppingListQuery; +import io.sphere.sdk.utils.CompletableFutureUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletionStage; + +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.only; + +class ShoppingListServiceImplTest { + + private ShoppingListServiceImpl service; + private ShoppingListSyncOptions shoppingListSyncOptions; + private List errorMessages; + private List errorExceptions; + + @BeforeEach + void setUp() { + errorMessages = new ArrayList<>(); + errorExceptions = new ArrayList<>(); + shoppingListSyncOptions = ShoppingListSyncOptionsBuilder + .of(mock(SphereClient.class)) + .errorCallback((exception, oldResource, newResource, updateActions) -> { + errorMessages.add(exception.getMessage()); + errorExceptions.add(exception.getCause()); + }) + .build(); + service = new ShoppingListServiceImpl(shoppingListSyncOptions); + } + + @Test + void fetchShoppingList_WithEmptyKey_ShouldNotFetchAnyShoppingList() { + // test + final Optional result = service.fetchShoppingList("").toCompletableFuture().join(); + + + // assertions + assertThat(result).isEmpty(); + assertThat(errorExceptions).isEmpty(); + assertThat(errorMessages).isEmpty(); + verify(shoppingListSyncOptions.getCtpClient(), never()).execute(any()); + } + + @Test + void fetchShoppingList_WithNullKey_ShouldNotFetchAnyShoppingList() { + // test + final Optional result = service.fetchShoppingList(null).toCompletableFuture().join(); + + // assertions + assertThat(result).isEmpty(); + assertThat(errorExceptions).isEmpty(); + assertThat(errorMessages).isEmpty(); + verify(shoppingListSyncOptions.getCtpClient(), never()).execute(any()); + } + + @Test + void fetchShoppingList_WithValidKey_ShouldReturnMockShoppingList() { + // preparation + final ShoppingList mockShoppingList = mock(ShoppingList.class); + when(mockShoppingList.getId()).thenReturn("testId"); + when(mockShoppingList.getKey()).thenReturn("any_key"); + + @SuppressWarnings("unchecked") + final PagedQueryResult pagedQueryResult = mock(PagedQueryResult.class); + when(pagedQueryResult.head()).thenReturn(Optional.of(mockShoppingList)); + when(shoppingListSyncOptions.getCtpClient().execute(any(ShoppingListQuery.class))) + .thenReturn(completedFuture(pagedQueryResult)); + + // test + final Optional result = + service.fetchShoppingList("any_key").toCompletableFuture().join(); + + // assertions + assertThat(result).containsSame(mockShoppingList); + assertThat(errorExceptions).isEmpty(); + assertThat(errorMessages).isEmpty(); + verify(shoppingListSyncOptions.getCtpClient(), only()).execute(any()); + } + + @Test + void fetchMatchingShoppingListsByKeys_WithUnexpectedException_ShouldFail() { + when(shoppingListSyncOptions.getCtpClient().execute(any())).thenReturn( + CompletableFutureUtils.failed(new BadGatewayException("bad gateway"))); + + assertThat(service.fetchMatchingShoppingListsByKeys(singleton("key"))) + .hasFailedWithThrowableThat() + .isExactlyInstanceOf(BadGatewayException.class); + assertThat(errorExceptions).isEmpty(); + assertThat(errorMessages).isEmpty(); + } + + @Test + void fetchMatchingShoppingListsByKeys_WithEmptyKeys_ShouldReturnEmptyOptional() { + Set customer = service.fetchMatchingShoppingListsByKeys(emptySet()).toCompletableFuture().join(); + + assertThat(customer).isEmpty(); + assertThat(errorExceptions).isEmpty(); + assertThat(errorMessages).isEmpty(); + verifyNoInteractions(shoppingListSyncOptions.getCtpClient()); + } + + + @Test + void createShoppingList_WithNullShoppingListKey_ShouldNotCreateShoppingList() { + // preparation + final ShoppingListDraft mockShoppingListDraft = mock(ShoppingListDraft.class); + final Map errors = new HashMap<>(); + when(mockShoppingListDraft.getKey()).thenReturn(null); + + final ShoppingListSyncOptions shoppingListSyncOptions = ShoppingListSyncOptionsBuilder + .of(mock(SphereClient.class)) + .errorCallback((exception, oldResource, newResource, actions) -> + errors.put(exception.getMessage(), exception)) + .build(); + final ShoppingListService shoppingListService = new ShoppingListServiceImpl(shoppingListSyncOptions); + + // test + final CompletionStage> result = shoppingListService + .createShoppingList(mockShoppingListDraft); + + // assertions + assertThat(result).isCompletedWithValue(Optional.empty()); + assertThat(errors.keySet()) + .containsExactly("Failed to create draft with key: 'null'. Reason: Draft key is blank!"); + verify(shoppingListSyncOptions.getCtpClient(), times(0)).execute(any()); + } + + @Test + void createShoppingList_WithEmptyShoppingListKey_ShouldHaveEmptyOptionalAsAResult() { + //preparation + final SphereClient sphereClient = mock(SphereClient.class); + final ShoppingListDraft shoppingListDraft = mock(ShoppingListDraft.class); + final Map errors = new HashMap<>(); + when(shoppingListDraft.getKey()).thenReturn(""); + + final ShoppingListSyncOptions options = ShoppingListSyncOptionsBuilder + .of(sphereClient) + .errorCallback((exception, oldResource, newResource, actions) -> + errors.put(exception.getMessage(), exception)) + .build(); + + final ShoppingListService shoppingListService = new ShoppingListServiceImpl(options); + + // test + final CompletionStage> result = shoppingListService + .createShoppingList(shoppingListDraft); + + // assertion + assertThat(result).isCompletedWithValue(Optional.empty()); + assertThat(errors.keySet()) + .containsExactly("Failed to create draft with key: ''. Reason: Draft key is blank!"); + verify(options.getCtpClient(), times(0)).execute(any()); + } + + @Test + void createShoppingList_WithUnsuccessfulMockCtpResponse_ShouldNotCreateShoppingList() { + // preparation + final ShoppingListDraft shoppingListDraft = mock(ShoppingListDraft.class); + final Map errors = new HashMap<>(); + when(shoppingListDraft.getKey()).thenReturn("key"); + + final ShoppingListSyncOptions options = ShoppingListSyncOptionsBuilder + .of(mock(SphereClient.class)) + .errorCallback((exception, oldResource, newResource, actions) -> + errors.put(exception.getMessage(), exception)) + .build(); + + final ShoppingListService shoppingListService = new ShoppingListServiceImpl(options); + + when(options.getCtpClient().execute(any())) + .thenReturn(CompletableFutureUtils.failed(new InternalServerErrorException())); + + // test + final CompletionStage> result = + shoppingListService.createShoppingList(shoppingListDraft); + + // assertions + assertThat(result).isCompletedWithValue(Optional.empty()); + assertThat(errors.keySet()) + .hasSize(1) + .hasOnlyOneElementSatisfying(message -> { + assertThat(message).contains("Failed to create draft with key: 'key'."); + }); + + assertThat(errors.values()) + .hasSize(1) + .singleElement().satisfies(exception -> { + assertThat(exception).isExactlyInstanceOf(SyncException.class); + assertThat(exception.getCause()).isExactlyInstanceOf(InternalServerErrorException.class); + }); + } + + @Test + void updateShoppingList_WithMockSuccessfulCtpResponse_ShouldCallShoppingListUpdateCommand() { + // preparation + final ShoppingList shoppingList = mock(ShoppingList.class); + final ShoppingListSyncOptions options = ShoppingListSyncOptionsBuilder + .of(mock(SphereClient.class)) + .build(); + + when(options.getCtpClient().execute(any())).thenReturn(completedFuture(shoppingList)); + final ShoppingListService shoppingListService = new ShoppingListServiceImpl(options); + + final List> updateActions = + singletonList(ChangeName.of(LocalizedString.ofEnglish("new_name"))); + // test + final CompletionStage result = + shoppingListService.updateShoppingList(shoppingList, updateActions); + + // assertions + assertThat(result).isCompletedWithValue(shoppingList); + verify(options.getCtpClient()) + .execute(eq(ShoppingListUpdateCommand.of(shoppingList, updateActions))); + } + + @Test + void updateShoppingList_WithMockUnsuccessfulCtpResponse_ShouldCompleteExceptionally() { + // preparation + final ShoppingList shoppingList = mock(ShoppingList.class); + final ShoppingListSyncOptions shoppingListSyncOptions = ShoppingListSyncOptionsBuilder + .of(mock(SphereClient.class)) + .build(); + + when(shoppingListSyncOptions.getCtpClient().execute(any())) + .thenReturn(CompletableFutureUtils.exceptionallyCompletedFuture(new InternalServerErrorException())); + + final ShoppingListService shoppingListService = new ShoppingListServiceImpl(shoppingListSyncOptions); + + final List> updateActions = + singletonList(ChangeName.of(LocalizedString.ofEnglish("new_name"))); + // test + final CompletionStage result = + shoppingListService.updateShoppingList(shoppingList, updateActions); + + // assertions + assertThat(result).hasFailedWithThrowableThat() + .isExactlyInstanceOf(InternalServerErrorException.class); + } +} diff --git a/src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptionsBuilderTest.java b/src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptionsBuilderTest.java new file mode 100644 index 0000000000..1e385bc4a4 --- /dev/null +++ b/src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncOptionsBuilderTest.java @@ -0,0 +1,285 @@ +package com.commercetools.sync.shoppinglists; + +import com.commercetools.sync.commons.exceptions.SyncException; +import com.commercetools.sync.commons.utils.QuadConsumer; +import com.commercetools.sync.commons.utils.TriConsumer; +import com.commercetools.sync.commons.utils.TriFunction; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.ShoppingListDraftBuilder; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import static io.sphere.sdk.models.LocalizedString.ofEnglish; +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ShoppingListSyncOptionsBuilderTest { + + private static final SphereClient CTP_CLIENT = mock(SphereClient.class); + private ShoppingListSyncOptionsBuilder shoppingListSyncOptionsBuilder = + ShoppingListSyncOptionsBuilder.of(CTP_CLIENT); + + @Test + void of_WithClient_ShouldCreateShoppingListSyncOptionsBuilder() { + assertThat(shoppingListSyncOptionsBuilder).isNotNull(); + } + + @Test + void build_WithClient_ShouldBuildSyncOptions() { + final ShoppingListSyncOptions shoppingListSyncOptions = shoppingListSyncOptionsBuilder.build(); + assertThat(shoppingListSyncOptions).isNotNull(); + assertThat(shoppingListSyncOptions.getBeforeUpdateCallback()).isNull(); + assertThat(shoppingListSyncOptions.getBeforeCreateCallback()).isNull(); + assertThat(shoppingListSyncOptions.getErrorCallback()).isNull(); + assertThat(shoppingListSyncOptions.getWarningCallback()).isNull(); + assertThat(shoppingListSyncOptions.getCtpClient()).isEqualTo(CTP_CLIENT); + assertThat(shoppingListSyncOptions.getBatchSize()).isEqualTo(ShoppingListSyncOptionsBuilder.BATCH_SIZE_DEFAULT); + } + + @Test + void beforeUpdateCallback_WithFilterAsCallback_ShouldSetCallback() { + final TriFunction>, ShoppingListDraft, ShoppingList, + List>> beforeUpdateCallback = + (updateActions, newShoppingList, oldShoppingList) -> emptyList(); + + shoppingListSyncOptionsBuilder.beforeUpdateCallback(beforeUpdateCallback); + + final ShoppingListSyncOptions shoppingListSyncOptions = shoppingListSyncOptionsBuilder.build(); + assertThat(shoppingListSyncOptions.getBeforeUpdateCallback()).isNotNull(); + } + + @Test + void beforeCreateCallback_WithFilterAsCallback_ShouldSetCallback() { + shoppingListSyncOptionsBuilder.beforeCreateCallback((newShoppingList) -> null); + + final ShoppingListSyncOptions shoppingListSyncOptions = shoppingListSyncOptionsBuilder.build(); + assertThat(shoppingListSyncOptions.getBeforeCreateCallback()).isNotNull(); + } + + @Test + void errorCallBack_WithCallBack_ShouldSetCallBack() { + final QuadConsumer, + Optional, List>> mockErrorCallBack + = (syncException, draft, shoppingList, updateActions) -> { + }; + shoppingListSyncOptionsBuilder.errorCallback(mockErrorCallBack); + + final ShoppingListSyncOptions shoppingListSyncOptions = shoppingListSyncOptionsBuilder.build(); + assertThat(shoppingListSyncOptions.getErrorCallback()).isNotNull(); + } + + @Test + void warningCallBack_WithCallBack_ShouldSetCallBack() { + final TriConsumer, Optional> mockWarningCallBack + = (syncException, draft, shoppingList) -> { }; + shoppingListSyncOptionsBuilder.warningCallback(mockWarningCallBack); + + final ShoppingListSyncOptions shoppingListSyncOptions = shoppingListSyncOptionsBuilder.build(); + assertThat(shoppingListSyncOptions.getWarningCallback()).isNotNull(); + } + + @Test + void getThis_ShouldReturnCorrectInstance() { + final ShoppingListSyncOptionsBuilder instance = shoppingListSyncOptionsBuilder.getThis(); + assertThat(instance).isNotNull(); + assertThat(instance).isInstanceOf(ShoppingListSyncOptionsBuilder.class); + assertThat(instance).isEqualTo(shoppingListSyncOptionsBuilder); + } + + @Test + void shoppingListSyncOptionsBuilderSetters_ShouldBeCallableAfterBaseSyncOptionsBuildSetters() { + final ShoppingListSyncOptions shoppingListSyncOptions = + ShoppingListSyncOptionsBuilder.of(CTP_CLIENT) + .batchSize(30) + .beforeCreateCallback((newShoppingList) -> null) + .beforeUpdateCallback( + (updateActions, newShoppingList, oldShoppingList) -> emptyList()) + .build(); + + assertThat(shoppingListSyncOptions).isNotNull(); + } + + @Test + void batchSize_WithPositiveValue_ShouldSetBatchSize() { + final ShoppingListSyncOptions shoppingListSyncOptions = + ShoppingListSyncOptionsBuilder + .of(CTP_CLIENT) + .batchSize(10) + .build(); + + assertThat(shoppingListSyncOptions.getBatchSize()).isEqualTo(10); + } + + @Test + void batchSize_WithZeroOrNegativeValue_ShouldFallBackToDefaultValue() { + final ShoppingListSyncOptions shoppingListSyncOptionsWithZeroBatchSize = + ShoppingListSyncOptionsBuilder.of(CTP_CLIENT) + .batchSize(0) + .build(); + + assertThat(shoppingListSyncOptionsWithZeroBatchSize.getBatchSize()) + .isEqualTo(ShoppingListSyncOptionsBuilder.BATCH_SIZE_DEFAULT); + + final ShoppingListSyncOptions shoppingListSyncOptionsWithNegativeBatchSize = + ShoppingListSyncOptionsBuilder.of(CTP_CLIENT) + .batchSize(-100) + .build(); + + assertThat(shoppingListSyncOptionsWithNegativeBatchSize.getBatchSize()) + .isEqualTo(ShoppingListSyncOptionsBuilder.BATCH_SIZE_DEFAULT); + } + + @Test + void applyBeforeUpdateCallBack_WithNullCallback_ShouldReturnIdenticalList() { + final ShoppingListSyncOptions shoppingListSyncOptions = + ShoppingListSyncOptionsBuilder.of(CTP_CLIENT).build(); + + assertThat(shoppingListSyncOptions.getBeforeUpdateCallback()).isNull(); + + final List> updateActions = singletonList(ChangeName.of(ofEnglish("name"))); + + final List> filteredList = + shoppingListSyncOptions.applyBeforeUpdateCallback( + updateActions, mock(ShoppingListDraft.class), mock(ShoppingList.class)); + + assertThat(filteredList).isSameAs(updateActions); + } + + @Test + void applyBeforeUpdateCallBack_WithNullReturnCallback_ShouldReturnEmptyList() { + final TriFunction>, ShoppingListDraft, + ShoppingList, List>> beforeUpdateCallback = + (updateActions, newShoppingList, oldShoppingList) -> null; + + final ShoppingListSyncOptions shoppingListSyncOptions = + ShoppingListSyncOptionsBuilder + .of(CTP_CLIENT) + .beforeUpdateCallback(beforeUpdateCallback) + .build(); + + assertThat(shoppingListSyncOptions.getBeforeUpdateCallback()).isNotNull(); + + final List> updateActions = singletonList(ChangeName.of(ofEnglish("name"))); + final List> filteredList = + shoppingListSyncOptions + .applyBeforeUpdateCallback(updateActions, mock(ShoppingListDraft.class), mock(ShoppingList.class)); + assertThat(filteredList).isNotEqualTo(updateActions); + assertThat(filteredList).isEmpty(); + } + + private interface MockTriFunction extends + TriFunction>, ShoppingListDraft, + ShoppingList, List>> { + } + + @Test + void applyBeforeUpdateCallBack_WithEmptyUpdateActions_ShouldNotApplyBeforeUpdateCallback() { + final MockTriFunction beforeUpdateCallback = mock(MockTriFunction.class); + + final ShoppingListSyncOptions shoppingListSyncOptions = + ShoppingListSyncOptionsBuilder.of(CTP_CLIENT) + .beforeUpdateCallback(beforeUpdateCallback) + .build(); + + assertThat(shoppingListSyncOptions.getBeforeUpdateCallback()).isNotNull(); + + final List> updateActions = emptyList(); + final List> filteredList = + shoppingListSyncOptions + .applyBeforeUpdateCallback(updateActions, mock(ShoppingListDraft.class), mock(ShoppingList.class)); + + assertThat(filteredList).isEmpty(); + verify(beforeUpdateCallback, never()).apply(any(), any(), any()); + } + + @Test + void applyBeforeUpdateCallBack_WithCallback_ShouldReturnFilteredList() { + final TriFunction>, ShoppingListDraft, + ShoppingList, List>> beforeUpdateCallback = + (updateActions, newShoppingList, oldShoppingList) -> emptyList(); + + final ShoppingListSyncOptions shoppingListSyncOptions = + ShoppingListSyncOptionsBuilder + .of(CTP_CLIENT) + .beforeUpdateCallback(beforeUpdateCallback) + .build(); + + assertThat(shoppingListSyncOptions.getBeforeUpdateCallback()).isNotNull(); + + final List> updateActions = singletonList(ChangeName.of(ofEnglish("name"))); + final List> filteredList = + shoppingListSyncOptions + .applyBeforeUpdateCallback(updateActions, mock(ShoppingListDraft.class), mock(ShoppingList.class)); + assertThat(filteredList).isNotEqualTo(updateActions); + assertThat(filteredList).isEmpty(); + } + + @Test + void applyBeforeCreateCallBack_WithCallback_ShouldReturnFilteredDraft() { + final Function draftFunction = + shoppingListDraft -> + ShoppingListDraftBuilder.of(shoppingListDraft) + .key(format("%s_filteredKey", shoppingListDraft.getKey())) + .build(); + + final ShoppingListSyncOptions shoppingListSyncOptions = + ShoppingListSyncOptionsBuilder.of(CTP_CLIENT) + .beforeCreateCallback(draftFunction) + .build(); + + assertThat(shoppingListSyncOptions.getBeforeCreateCallback()).isNotNull(); + + final ShoppingListDraft resourceDraft = mock(ShoppingListDraft.class); + when(resourceDraft.getKey()).thenReturn("myKey"); + + + final Optional filteredDraft = shoppingListSyncOptions + .applyBeforeCreateCallback(resourceDraft); + + assertThat(filteredDraft).hasValueSatisfying(shoppingListDraft -> + assertThat(shoppingListDraft.getKey()).isEqualTo("myKey_filteredKey")); + } + + @Test + void applyBeforeCreateCallBack_WithNullCallback_ShouldReturnIdenticalDraftInOptional() { + final ShoppingListSyncOptions shoppingListSyncOptions = ShoppingListSyncOptionsBuilder.of(CTP_CLIENT).build(); + assertThat(shoppingListSyncOptions.getBeforeCreateCallback()).isNull(); + + final ShoppingListDraft resourceDraft = mock(ShoppingListDraft.class); + final Optional filteredDraft = shoppingListSyncOptions.applyBeforeCreateCallback( + resourceDraft); + + assertThat(filteredDraft).containsSame(resourceDraft); + } + + @Test + void applyBeforeCreateCallBack_WithCallbackReturningNull_ShouldReturnEmptyOptional() { + final Function draftFunction = shoppingListDraft -> null; + final ShoppingListSyncOptions shoppingListSyncOptions = + ShoppingListSyncOptionsBuilder.of(CTP_CLIENT) + .beforeCreateCallback(draftFunction) + .build(); + + assertThat(shoppingListSyncOptions.getBeforeCreateCallback()).isNotNull(); + + final ShoppingListDraft resourceDraft = mock(ShoppingListDraft.class); + final Optional filteredDraft = + shoppingListSyncOptions.applyBeforeCreateCallback(resourceDraft); + + assertThat(filteredDraft).isEmpty(); + } +} diff --git a/src/test/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidatorTest.java b/src/test/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidatorTest.java new file mode 100644 index 0000000000..3cf43cb1f7 --- /dev/null +++ b/src/test/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidatorTest.java @@ -0,0 +1,314 @@ +package com.commercetools.sync.shoppinglists.helpers; + +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.customers.Customer; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.models.ResourceIdentifier; +import io.sphere.sdk.shoppinglists.LineItemDraft; +import io.sphere.sdk.shoppinglists.LineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.TextLineItemDraft; +import io.sphere.sdk.shoppinglists.TextLineItemDraftBuilder; +import io.sphere.sdk.types.CustomFieldsDraft; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static com.commercetools.sync.shoppinglists.helpers.ShoppingListBatchValidator.LINE_ITEM_DRAFT_IS_NULL; +import static com.commercetools.sync.shoppinglists.helpers.ShoppingListBatchValidator.LINE_ITEM_DRAFT_SKU_NOT_SET; +import static com.commercetools.sync.shoppinglists.helpers.ShoppingListBatchValidator.SHOPPING_LIST_DRAFT_IS_NULL; +import static com.commercetools.sync.shoppinglists.helpers.ShoppingListBatchValidator.SHOPPING_LIST_DRAFT_KEY_NOT_SET; +import static com.commercetools.sync.shoppinglists.helpers.ShoppingListBatchValidator.SHOPPING_LIST_DRAFT_NAME_NOT_SET; +import static com.commercetools.sync.shoppinglists.helpers.ShoppingListBatchValidator.TEXT_LINE_ITEM_DRAFT_IS_NULL; +import static com.commercetools.sync.shoppinglists.helpers.ShoppingListBatchValidator.TEXT_LINE_ITEM_DRAFT_NAME_NOT_SET; +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static org.apache.commons.lang3.StringUtils.EMPTY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ShoppingListBatchValidatorTest { + + private ShoppingListSyncOptions syncOptions; + private ShoppingListSyncStatistics syncStatistics; + private List errorCallBackMessages; + + @BeforeEach + void setup() { + errorCallBackMessages = new ArrayList<>(); + final SphereClient ctpClient = mock(SphereClient.class); + + syncOptions = ShoppingListSyncOptionsBuilder + .of(ctpClient) + .errorCallback((exception, oldResource, newResource, updateActions) -> + errorCallBackMessages.add(exception.getMessage())) + .build(); + syncStatistics = mock(ShoppingListSyncStatistics.class); + } + + @Test + void validateAndCollectReferencedKeys_WithEmptyDraft_ShouldHaveEmptyResult() { + final Set validDrafts = getValidDrafts(emptyList()); + + assertThat(errorCallBackMessages).hasSize(0); + assertThat(validDrafts).isEmpty(); + } + + @Test + void validateAndCollectReferencedKeys_WithNullShoppingListDraft_ShouldHaveValidationErrorAndEmptyResult() { + final Set validDrafts = getValidDrafts(singletonList(null)); + + assertThat(errorCallBackMessages).hasSize(1); + assertThat(errorCallBackMessages.get(0)).isEqualTo(SHOPPING_LIST_DRAFT_IS_NULL); + assertThat(validDrafts).isEmpty(); + } + + @Test + void validateAndCollectReferencedKeys_WithShoppingListDraftWithNullKey_ShouldHaveValidationErrorAndEmptyResult() { + final ShoppingListDraft shoppingListDraft = mock(ShoppingListDraft.class); + final Set validDrafts = getValidDrafts(singletonList(shoppingListDraft)); + + assertThat(errorCallBackMessages).hasSize(1); + assertThat(errorCallBackMessages.get(0)) + .isEqualTo(format(SHOPPING_LIST_DRAFT_KEY_NOT_SET, shoppingListDraft.getName())); + assertThat(validDrafts).isEmpty(); + } + + @Test + void validateAndCollectReferencedKeys_WithShoppingListDraftWithEmptyKey_ShouldHaveValidationErrorAndEmptyResult() { + final ShoppingListDraft shoppingListDraft = mock(ShoppingListDraft.class); + when(shoppingListDraft.getKey()).thenReturn(EMPTY); + when(shoppingListDraft.getName()).thenReturn(LocalizedString.empty()); + final Set validDrafts = getValidDrafts(singletonList(shoppingListDraft)); + + assertThat(errorCallBackMessages).hasSize(1); + assertThat(errorCallBackMessages.get(0)) + .isEqualTo(format(SHOPPING_LIST_DRAFT_KEY_NOT_SET, shoppingListDraft.getName())); + assertThat(validDrafts).isEmpty(); + } + + @Test + void validateAndCollectReferencedKeys_WithShoppingListDraftWithNullName_ShouldHaveValidationErrorAndEmptyResult() { + final ShoppingListDraft shoppingListDraft = mock(ShoppingListDraft.class); + when(shoppingListDraft.getKey()).thenReturn("validDraftKey"); + final Set validDrafts = getValidDrafts(singletonList(shoppingListDraft)); + + assertThat(errorCallBackMessages).hasSize(1); + assertThat(errorCallBackMessages.get(0)) + .isEqualTo(format(SHOPPING_LIST_DRAFT_NAME_NOT_SET, shoppingListDraft.getKey())); + assertThat(validDrafts).isEmpty(); + } + + @Test + void validateAndCollectReferencedKeys_WithShoppingListDraftWithEmptyName_ShouldHaveValidationErrorAndEmptyResult() { + final ShoppingListDraft shoppingListDraft = mock(ShoppingListDraft.class); + when(shoppingListDraft.getKey()).thenReturn("validDraftKey"); + when(shoppingListDraft.getName()).thenReturn(LocalizedString.of()); + final Set validDrafts = getValidDrafts(singletonList(shoppingListDraft)); + + assertThat(errorCallBackMessages).hasSize(1); + assertThat(errorCallBackMessages.get(0)) + .isEqualTo(format(SHOPPING_LIST_DRAFT_NAME_NOT_SET, shoppingListDraft.getKey())); + assertThat(validDrafts).isEmpty(); + } + + @Test + void validateAndCollectReferencedKeys_WithLineItemDraftIsNull_ShouldHaveValidationErrorAndEmptyResult() { + final ShoppingListDraft shoppingListDraft = mock(ShoppingListDraft.class); + when(shoppingListDraft.getKey()).thenReturn("validDraftKey"); + when(shoppingListDraft.getName()).thenReturn(LocalizedString.ofEnglish("validDraftName")); + when(shoppingListDraft.getLineItems()).thenReturn(singletonList(null)); + + final Set validDrafts = getValidDrafts(singletonList(shoppingListDraft)); + + assertThat(errorCallBackMessages).hasSize(1); + assertThat(errorCallBackMessages.get(0)) + .isEqualTo(format(LINE_ITEM_DRAFT_IS_NULL, 0, shoppingListDraft.getKey())); + assertThat(validDrafts).isEmpty(); + } + + @Test + void validateAndCollectReferencedKeys_WithLineItemDraftWithNullSku_ShouldHaveValidationErrorAndEmptyResult() { + final ShoppingListDraft shoppingListDraft = mock(ShoppingListDraft.class); + when(shoppingListDraft.getKey()).thenReturn("validDraftKey"); + when(shoppingListDraft.getName()).thenReturn(LocalizedString.ofEnglish("validDraftName")); + when(shoppingListDraft.getLineItems()).thenReturn(singletonList(LineItemDraftBuilder.ofSku("", + 1L).build())); + + final Set validDrafts = getValidDrafts(singletonList(shoppingListDraft)); + + assertThat(errorCallBackMessages).hasSize(1); + assertThat(errorCallBackMessages.get(0)) + .isEqualTo(format(LINE_ITEM_DRAFT_SKU_NOT_SET, 0, shoppingListDraft.getKey())); + assertThat(validDrafts).isEmpty(); + } + + @Test + void validateAndCollectReferencedKeys_WithTextLineItemDraftIsNull_ShouldHaveValidationErrorAndEmptyResult() { + final ShoppingListDraft shoppingListDraft = mock(ShoppingListDraft.class); + when(shoppingListDraft.getKey()).thenReturn("validDraftKey"); + when(shoppingListDraft.getName()).thenReturn(LocalizedString.ofEnglish("validDraftName")); + when(shoppingListDraft.getLineItems()).thenReturn(singletonList(LineItemDraftBuilder.ofSku("123", + 1L).build())); + when(shoppingListDraft.getTextLineItems()).thenReturn(singletonList(null)); + final Set validDrafts = getValidDrafts(singletonList(shoppingListDraft)); + + assertThat(errorCallBackMessages).hasSize(1); + assertThat(errorCallBackMessages.get(0)) + .isEqualTo(format(TEXT_LINE_ITEM_DRAFT_IS_NULL, 0, shoppingListDraft.getKey())); + assertThat(validDrafts).isEmpty(); + } + + @Test + void validateAndCollectReferencedKeys_WithLineItemAndTextLineItemError_ShouldHaveOneCallback() { + final ShoppingListDraft shoppingListDraft = mock(ShoppingListDraft.class); + when(shoppingListDraft.getKey()).thenReturn("validDraftKey"); + when(shoppingListDraft.getName()).thenReturn(LocalizedString.ofEnglish("validDraftName")); + when(shoppingListDraft.getLineItems()).thenReturn(singletonList(LineItemDraftBuilder.ofSku("", + 1L).build())); + when(shoppingListDraft.getTextLineItems()).thenReturn(singletonList(null)); + + final Set validDrafts = getValidDrafts(singletonList(shoppingListDraft)); + + assertThat(errorCallBackMessages).hasSize(1); + assertThat(errorCallBackMessages.get(0)) + .isEqualTo(format("%s,%s", format(LINE_ITEM_DRAFT_SKU_NOT_SET, 0, shoppingListDraft.getKey()), + format(TEXT_LINE_ITEM_DRAFT_IS_NULL,0, shoppingListDraft.getKey()))); + assertThat(validDrafts).isEmpty(); + } + + @Test + void validateAndCollectReferencedKeys_WithTextLineItemDraftWithEmptyName_ShouldHaveValidationErrorAndEmptyResult() { + final ShoppingListDraft shoppingListDraft = mock(ShoppingListDraft.class); + when(shoppingListDraft.getKey()).thenReturn("validDraftKey"); + when(shoppingListDraft.getName()).thenReturn(LocalizedString.ofEnglish("validDraftName")); + when(shoppingListDraft.getLineItems()).thenReturn(singletonList(LineItemDraftBuilder.ofSku("123", + 1L).build())); + when(shoppingListDraft.getTextLineItems()).thenReturn( + singletonList(TextLineItemDraftBuilder.of(LocalizedString.empty(), + 1L).build())); + final Set validDrafts = getValidDrafts(singletonList(shoppingListDraft)); + + assertThat(errorCallBackMessages).hasSize(1); + assertThat(errorCallBackMessages.get(0)) + .isEqualTo(format(TEXT_LINE_ITEM_DRAFT_NAME_NOT_SET, 0, shoppingListDraft.getKey())); + assertThat(validDrafts).isEmpty(); + } + + @Test + void validateAndCollectReferencedKeys_WithTextLineItemDraftWithNullName_ShouldHaveValidationErrorAndEmptyResult() { + final ShoppingListDraft shoppingListDraft = mock(ShoppingListDraft.class); + when(shoppingListDraft.getKey()).thenReturn("validDraftKey"); + when(shoppingListDraft.getName()).thenReturn(LocalizedString.ofEnglish("validDraftName")); + when(shoppingListDraft.getLineItems()).thenReturn(singletonList(LineItemDraftBuilder.ofSku("123", + 1L).build())); + when(shoppingListDraft.getTextLineItems()).thenReturn( + singletonList(TextLineItemDraftBuilder.of(null, + 1L).build())); + final Set validDrafts = getValidDrafts(singletonList(shoppingListDraft)); + + assertThat(errorCallBackMessages).hasSize(1); + assertThat(errorCallBackMessages.get(0)) + .isEqualTo(format(TEXT_LINE_ITEM_DRAFT_NAME_NOT_SET, 0, shoppingListDraft.getKey())); + assertThat(validDrafts).isEmpty(); + } + + @Test + void validateAndCollectReferencedKeys_WithValidDrafts_ShouldReturnCorrectResults() { + final ShoppingListDraft validShoppingListDraft = mock(ShoppingListDraft.class); + when(validShoppingListDraft.getKey()).thenReturn("validDraftKey"); + when(validShoppingListDraft.getName()).thenReturn(LocalizedString.ofEnglish("name")); + when(validShoppingListDraft.getCustom()) + .thenReturn(CustomFieldsDraft.ofTypeKeyAndJson("typeKey", emptyMap())); + LineItemDraft lineItem = mock(LineItemDraft.class); + when(lineItem.getCustom()).thenReturn(CustomFieldsDraft.ofTypeKeyAndJson("lineItemTypeKey", + emptyMap())); + when(lineItem.getSku()).thenReturn("validSku"); + when(validShoppingListDraft.getLineItems()).thenReturn(singletonList(lineItem)); + TextLineItemDraft textLineItem = mock(TextLineItemDraft.class); + when(textLineItem.getCustom()).thenReturn(CustomFieldsDraft.ofTypeKeyAndJson("textLineItemTypeKey", + emptyMap())); + when(textLineItem.getName()).thenReturn(LocalizedString.ofEnglish("validName")); + when(validShoppingListDraft.getTextLineItems()).thenReturn(singletonList(textLineItem)); + Customer customer = mock(Customer.class); + when(customer.getKey()).thenReturn("customerKey"); + final ResourceIdentifier customerResourceIdentifier = ResourceIdentifier.ofKey(customer.getKey()); + when(validShoppingListDraft.getCustomer()).thenReturn(customerResourceIdentifier); + + final ShoppingListDraft validShoppingListDraftWithoutReferences = mock(ShoppingListDraft.class); + when(validShoppingListDraftWithoutReferences.getKey()).thenReturn("validDraftKey"); + when(validShoppingListDraftWithoutReferences.getName()).thenReturn(LocalizedString.ofEnglish("name")); + when(validShoppingListDraftWithoutReferences.getLineItems()).thenReturn(null); + when(validShoppingListDraftWithoutReferences.getTextLineItems()).thenReturn(null); + + final ShoppingListDraft invalidShoppingListDraft = mock(ShoppingListDraft.class); + + final ShoppingListBatchValidator shoppingListBatchValidator = + new ShoppingListBatchValidator(syncOptions, syncStatistics); + final ImmutablePair, ShoppingListBatchValidator.ReferencedKeys> pair + = shoppingListBatchValidator.validateAndCollectReferencedKeys( + Arrays.asList(validShoppingListDraft, invalidShoppingListDraft, validShoppingListDraftWithoutReferences)); + + assertThat(errorCallBackMessages).hasSize(1); + assertThat(errorCallBackMessages.get(0)) + .isEqualTo(format(SHOPPING_LIST_DRAFT_KEY_NOT_SET, invalidShoppingListDraft.getName())); + assertThat(pair.getLeft()) + .containsExactlyInAnyOrder(validShoppingListDraft, validShoppingListDraftWithoutReferences); + assertThat(pair.getRight().getTypeKeys()) + .containsExactlyInAnyOrder("typeKey", "lineItemTypeKey", "textLineItemTypeKey"); + assertThat(pair.getRight().getCustomerKeys()) + .containsExactlyInAnyOrder("customerKey"); + } + + @Test + void validateAndCollectReferencedKeys_WithEmptyKeys_ShouldNotCollectKeys() { + final ShoppingListDraft validShoppingListDraft = mock(ShoppingListDraft.class); + when(validShoppingListDraft.getKey()).thenReturn("validDraftKey"); + when(validShoppingListDraft.getName()).thenReturn(LocalizedString.ofEnglish("name")); + when(validShoppingListDraft.getCustom()) + .thenReturn(CustomFieldsDraft.ofTypeKeyAndJson(EMPTY, emptyMap())); + LineItemDraft lineItem = mock(LineItemDraft.class); + when(lineItem.getCustom()).thenReturn(CustomFieldsDraft.ofTypeKeyAndJson(EMPTY, + emptyMap())); + when(lineItem.getSku()).thenReturn("validSku"); + when(validShoppingListDraft.getLineItems()).thenReturn(singletonList(lineItem)); + TextLineItemDraft textLineItem = mock(TextLineItemDraft.class); + when(textLineItem.getCustom()).thenReturn(CustomFieldsDraft.ofTypeKeyAndJson(EMPTY, + emptyMap())); + when(textLineItem.getName()).thenReturn(LocalizedString.ofEnglish("validName")); + when(validShoppingListDraft.getTextLineItems()).thenReturn(singletonList(textLineItem)); + final ResourceIdentifier customerResourceIdentifier = ResourceIdentifier.ofKey(EMPTY); + when(validShoppingListDraft.getCustomer()).thenReturn(customerResourceIdentifier); + + final ShoppingListBatchValidator shoppingListBatchValidator = + new ShoppingListBatchValidator(syncOptions, syncStatistics); + final ImmutablePair, ShoppingListBatchValidator.ReferencedKeys> pair + = shoppingListBatchValidator.validateAndCollectReferencedKeys( + Arrays.asList(validShoppingListDraft)); + + assertThat(pair.getLeft()).contains(validShoppingListDraft); + assertThat(pair.getRight().getCustomerKeys()).isEmpty(); + assertThat(pair.getRight().getTypeKeys()).isEmpty(); + assertThat(errorCallBackMessages).hasSize(0); + } + + @Nonnull + private Set getValidDrafts(@Nonnull final List shoppingListDrafts) { + final ShoppingListBatchValidator shoppingListBatchValidator = + new ShoppingListBatchValidator(syncOptions, syncStatistics); + final ImmutablePair, ShoppingListBatchValidator.ReferencedKeys> pair = + shoppingListBatchValidator.validateAndCollectReferencedKeys(shoppingListDrafts); + return pair.getLeft(); + } +} diff --git a/src/test/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListSyncStatisticsTest.java b/src/test/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListSyncStatisticsTest.java new file mode 100644 index 0000000000..2fcccdcdf1 --- /dev/null +++ b/src/test/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListSyncStatisticsTest.java @@ -0,0 +1,27 @@ +package com.commercetools.sync.shoppinglists.helpers; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ShoppingListSyncStatisticsTest { + private ShoppingListSyncStatistics shoppingListSyncStatistics; + + @BeforeEach + void setup() { + shoppingListSyncStatistics = new ShoppingListSyncStatistics(); + } + + @Test + void getReportMessage_WithIncrementedStats_ShouldGetCorrectMessage() { + shoppingListSyncStatistics.incrementCreated(1); + shoppingListSyncStatistics.incrementFailed(2); + shoppingListSyncStatistics.incrementUpdated(3); + shoppingListSyncStatistics.incrementProcessed(6); + + assertThat(shoppingListSyncStatistics.getReportMessage()) + .isEqualTo("Summary: 6 shopping lists were processed in total " + + "(1 created, 3 updated and 2 failed to sync)."); + } +} From d6d517b499cb5f21ede7231af3eddd8ce63d0f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahmet=20=C3=96z?= Date: Wed, 4 Nov 2020 09:18:56 +0100 Subject: [PATCH 03/12] Add shopping list update actions (without line items) (#611) Co-authored-by: ninalindl --- .../ShoppingListCustomActionBuilder.java | 57 +++ .../utils/ShoppingListSyncUtils.java | 91 +++++ .../utils/ShoppingListUpdateActionUtils.java | 164 +++++++++ ...oppingListCustomUpdateActionUtilsTest.java | 59 ++++ .../utils/ShoppingListSyncUtilsTest.java | 163 +++++++++ .../ShoppingListUpdateActionUtilsTest.java | 326 ++++++++++++++++++ 6 files changed, 860 insertions(+) create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListCustomActionBuilder.java create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtils.java create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListUpdateActionUtils.java create mode 100644 src/test/java/com/commercetools/sync/commons/utils/ShoppingListCustomUpdateActionUtilsTest.java create mode 100644 src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtilsTest.java create mode 100644 src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListUpdateActionUtilsTest.java diff --git a/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListCustomActionBuilder.java b/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListCustomActionBuilder.java new file mode 100644 index 0000000000..e8a4b701ac --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListCustomActionBuilder.java @@ -0,0 +1,57 @@ +package com.commercetools.sync.shoppinglists.utils; + +import com.commercetools.sync.commons.helpers.GenericCustomActionBuilder; +import com.fasterxml.jackson.databind.JsonNode; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomField; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomType; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Map; + +public final class ShoppingListCustomActionBuilder implements GenericCustomActionBuilder { + + private static final ShoppingListCustomActionBuilder builder = new ShoppingListCustomActionBuilder(); + + private ShoppingListCustomActionBuilder() { + super(); + } + + @Nonnull + public static ShoppingListCustomActionBuilder of() { + return builder; + } + + @Nonnull + @Override + public UpdateAction buildRemoveCustomTypeAction( + @Nullable final Integer variantId, + @Nullable final String objectId) { + + return SetCustomType.ofRemoveType(); + } + + @Nonnull + @Override + public UpdateAction buildSetCustomTypeAction( + @Nullable final Integer variantId, + @Nullable final String objectId, + @Nonnull final String customTypeId, + @Nullable final Map customFieldsJsonMap) { + + return SetCustomType.ofTypeIdAndJson(customTypeId, customFieldsJsonMap); + } + + @Nonnull + @Override + public UpdateAction buildSetCustomFieldAction( + @Nullable final Integer variantId, + @Nullable final String objectId, + @Nullable final String customFieldName, + @Nullable final JsonNode customFieldValue) { + + return SetCustomField.ofJson(customFieldName, customFieldValue); + } +} diff --git a/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtils.java b/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtils.java new file mode 100644 index 0000000000..f080a027f8 --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtils.java @@ -0,0 +1,91 @@ +package com.commercetools.sync.shoppinglists.utils; + +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.types.CustomDraft; +import io.sphere.sdk.types.CustomFieldsDraft; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; + +import static com.commercetools.sync.commons.utils.CustomUpdateActionUtils.buildPrimaryResourceCustomUpdateActions; +import static com.commercetools.sync.commons.utils.OptionalUtils.filterEmptyOptionals; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildChangeNameUpdateAction; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildSetAnonymousIdUpdateAction; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildSetCustomerUpdateAction; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildSetDeleteDaysAfterLastModificationUpdateAction; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildSetDescriptionUpdateAction; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildSetSlugUpdateAction; + +public final class ShoppingListSyncUtils { + + private static final ShoppingListCustomActionBuilder shoppingListCustomActionBuilder = + ShoppingListCustomActionBuilder.of(); + + /** + * Compares all the fields of a {@link ShoppingList} and a {@link ShoppingListDraft}. It returns a {@link List} of + * {@link UpdateAction}<{@link ShoppingList}> as a result. If no update action is needed, for example in + * case where both the {@link ShoppingListDraft} and the {@link ShoppingList} have the same fields, an empty + * {@link List} is returned. + * + * @param oldShoppingList the shopping list which should be updated. + * @param newShoppingList the shopping list draft where we get the new data. + * @param syncOptions the sync options wrapper which contains options related to the sync process supplied + * by the user. For example, custom callbacks to call in case of warnings or errors occurring + * on the build update action process. And other options (See {@link ShoppingListSyncOptions} + * for more info. + * @return A list of shopping list specific update actions. + */ + @Nonnull + public static List> buildActions( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingList, + @Nonnull final ShoppingListSyncOptions syncOptions) { + + final List> updateActions = filterEmptyOptionals( + buildSetSlugUpdateAction(oldShoppingList, newShoppingList), + buildChangeNameUpdateAction(oldShoppingList, newShoppingList), + buildSetDescriptionUpdateAction(oldShoppingList, newShoppingList), + buildSetCustomerUpdateAction(oldShoppingList, newShoppingList), + buildSetAnonymousIdUpdateAction(oldShoppingList, newShoppingList), + buildSetDeleteDaysAfterLastModificationUpdateAction(oldShoppingList, newShoppingList) + ); + + final List> shoppingListCustomUpdateActions = + buildPrimaryResourceCustomUpdateActions(oldShoppingList, + new CustomShoppingListDraft(newShoppingList), + shoppingListCustomActionBuilder, + syncOptions); + + updateActions.addAll(shoppingListCustomUpdateActions); + + return updateActions; + } + + /** + * The class is needed by `buildPrimaryResourceCustomUpdateActions` generic utility method, + * because required generic type `S` is based on the CustomDraft interface (S extends CustomDraft). + * + *

TODO (JVM-SDK): Missing the interface CustomDraft. + * See for more details: https://github.com/commercetools/commercetools-jvm-sdk/issues/2073 + */ + private static class CustomShoppingListDraft implements CustomDraft { + private final ShoppingListDraft shoppingListDraft; + + public CustomShoppingListDraft(@Nonnull final ShoppingListDraft shoppingListDraft) { + this.shoppingListDraft = shoppingListDraft; + } + + @Nullable + @Override + public CustomFieldsDraft getCustom() { + return shoppingListDraft.getCustom(); + } + } + + private ShoppingListSyncUtils() { + } +} diff --git a/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListUpdateActionUtils.java b/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListUpdateActionUtils.java new file mode 100644 index 0000000000..b749bfd504 --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListUpdateActionUtils.java @@ -0,0 +1,164 @@ +package com.commercetools.sync.shoppinglists.utils; + +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.customers.Customer; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.models.Reference; +import io.sphere.sdk.models.Referenceable; +import io.sphere.sdk.models.ResourceIdentifier; +import io.sphere.sdk.models.ResourceImpl; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeName; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetAnonymousId; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomer; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetDeleteDaysAfterLastModification; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetDescription; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetSlug; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Optional; + +import static com.commercetools.sync.commons.utils.CommonTypeUpdateActionUtils.buildUpdateAction; +import static com.commercetools.sync.commons.utils.CommonTypeUpdateActionUtils.buildUpdateActionForReferences; + +public final class ShoppingListUpdateActionUtils { + + private ShoppingListUpdateActionUtils() { + } + + /** + * Compares the {@link LocalizedString} slugs of a {@link ShoppingList} and a {@link ShoppingListDraft} and returns + * an {@link UpdateAction}<{@link ShoppingList}> as a result in an {@link Optional}. If both the + * {@link ShoppingList} and the {@link ShoppingListDraft} have the same slug, then no update action is needed and + * hence an empty {@link Optional} is returned. + * + * @param oldShoppingList the shopping list which should be updated. + * @param newShoppingList the shopping list draft where we get the new slug. + * @return A filled optional with the update action or an empty optional if the slugs are identical. + */ + @Nonnull + public static Optional> buildSetSlugUpdateAction( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingList) { + + return buildUpdateAction(oldShoppingList.getSlug(), + newShoppingList.getSlug(), () -> SetSlug.of(newShoppingList.getSlug())); + } + + /** + * Compares the {@link LocalizedString} names of a {@link ShoppingList} and a {@link ShoppingListDraft} and returns + * an {@link UpdateAction}<{@link ShoppingList}> as a result in an {@link Optional}. If both the + * {@link ShoppingList} and the {@link ShoppingListDraft} have the same name, then no update action is needed and + * hence an empty {@link Optional} is returned. + * + * @param oldShoppingList the shopping list which should be updated. + * @param newShoppingList the shopping list draft where we get the new name. + * @return A filled optional with the update action or an empty optional if the names are identical. + */ + @Nonnull + public static Optional> buildChangeNameUpdateAction( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingList) { + + return buildUpdateAction(oldShoppingList.getName(), + newShoppingList.getName(), () -> ChangeName.of(newShoppingList.getName())); + } + + /** + * Compares the {@link LocalizedString} descriptions of {@link ShoppingList} and a {@link ShoppingListDraft} and + * returns an {@link UpdateAction}<{@link ShoppingList}> as a result in an {@link Optional}. If both the + * {@link ShoppingList} and the {@link ShoppingListDraft} have the same description, then no update action is needed + * and hence an empty {@link Optional} is returned. + * + * @param oldShoppingList the shopping list which should be updated. + * @param newShoppingList the shopping list draft where we get the new description. + * @return A filled optional with the update action or an empty optional if the descriptions are identical. + */ + @Nonnull + public static Optional> buildSetDescriptionUpdateAction( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingList) { + + return buildUpdateAction(oldShoppingList.getDescription(), + newShoppingList.getDescription(), () -> SetDescription.of(newShoppingList.getDescription())); + } + + /** + * Compares the customer references of a {@link ShoppingList} and a {@link ShoppingListDraft} and + * returns an {@link UpdateAction}<{@link ShoppingList}> as a result in an {@link Optional}. If both the + * {@link ShoppingList} and the {@link ShoppingListDraft} have the same customer, then no update action is needed + * and hence an empty {@link Optional} is returned. + * + * @param oldShoppingList the shopping list which should be updated. + * @param newShoppingList the shopping list draft which holds the new customer. + * @return A filled optional with the update action or an empty optional if the customers are identical. + */ + @Nonnull + public static Optional> buildSetCustomerUpdateAction( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingList) { + + return buildUpdateActionForReferences(oldShoppingList.getCustomer(), newShoppingList.getCustomer(), + () -> SetCustomer.of(mapResourceIdentifierToReferenceable(newShoppingList.getCustomer()))); + } + + @Nullable + private static Referenceable mapResourceIdentifierToReferenceable( + @Nullable final ResourceIdentifier resourceIdentifier) { + + if (resourceIdentifier == null) { + return null; // unset + } + + // TODO (JVM-SDK), see: SUPPORT-10261 SetCustomerGroup could be created with a ResourceIdentifier + // https://github.com/commercetools/commercetools-jvm-sdk/issues/2072 + return new ResourceImpl(null, null, null, null) { + @Override + public Reference toReference() { + return Reference.of(Customer.referenceTypeId(), resourceIdentifier.getId()); + } + }; + } + + /** + * Compares the anonymousIds of {@link ShoppingList} and a {@link ShoppingListDraft} and + * returns an {@link UpdateAction}<{@link ShoppingList}> as a result in an {@link Optional}. If both the + * {@link ShoppingList} and the {@link ShoppingListDraft} have the same anonymousId, then no update action is needed + * and hence an empty {@link Optional} is returned. + * + * @param oldShoppingList the shopping list which should be updated. + * @param newShoppingList the shopping list draft which holds the new anonymousId. + * @return A filled optional with the update action or an empty optional if the anonymousIds are identical. + */ + @Nonnull + public static Optional> buildSetAnonymousIdUpdateAction( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingList) { + + return buildUpdateAction(oldShoppingList.getAnonymousId(), newShoppingList.getAnonymousId(), + () -> SetAnonymousId.of(newShoppingList.getAnonymousId())); + } + + /** + * Compares the deleteDaysAfterLastModification of {@link ShoppingList} and a {@link ShoppingListDraft} and + * returns an {@link UpdateAction}<{@link ShoppingList}> as a result in an {@link Optional}. If both the + * {@link ShoppingList} and the {@link ShoppingListDraft} have the same deleteDaysAfterLastModification, then no + * update action is needed and hence an empty {@link Optional} is returned. + * + * @param oldShoppingList the shopping list which should be updated. + * @param newShoppingList the shopping list draft which holds the new deleteDaysAfterLastModification. + * @return A filled optional with the update action or an empty optional if the deleteDaysAfterLastModifications + * are identical. + */ + @Nonnull + public static Optional> buildSetDeleteDaysAfterLastModificationUpdateAction( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingList) { + + return buildUpdateAction(oldShoppingList.getDeleteDaysAfterLastModification(), + newShoppingList.getDeleteDaysAfterLastModification(), () -> SetDeleteDaysAfterLastModification.of( + newShoppingList.getDeleteDaysAfterLastModification())); + } +} diff --git a/src/test/java/com/commercetools/sync/commons/utils/ShoppingListCustomUpdateActionUtilsTest.java b/src/test/java/com/commercetools/sync/commons/utils/ShoppingListCustomUpdateActionUtilsTest.java new file mode 100644 index 0000000000..98b1bb8731 --- /dev/null +++ b/src/test/java/com/commercetools/sync/commons/utils/ShoppingListCustomUpdateActionUtilsTest.java @@ -0,0 +1,59 @@ +package com.commercetools.sync.commons.utils; + +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; +import com.commercetools.sync.shoppinglists.utils.ShoppingListCustomActionBuilder; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomField; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomType; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.UUID; + +import static com.commercetools.sync.commons.asserts.actions.AssertionsForUpdateActions.assertThat; +import static io.sphere.sdk.models.ResourceIdentifier.ofId; +import static java.util.Collections.emptyMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class ShoppingListCustomUpdateActionUtilsTest { + + @Test + void buildTypedSetCustomTypeUpdateAction_WithCustomerResource_ShouldBuildCustomerUpdateAction() { + final String newCustomTypeId = UUID.randomUUID().toString(); + + final UpdateAction updateAction = + GenericUpdateActionUtils.buildTypedSetCustomTypeUpdateAction(newCustomTypeId, new HashMap<>(), + mock(ShoppingList.class), ShoppingListCustomActionBuilder.of(), null, ShoppingList::getId, + shoppingListResource -> shoppingListResource.toReference().getTypeId(), shoppingListResource -> null, + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)).build()).orElse(null); + + assertThat(updateAction).isInstanceOf(SetCustomType.class); + assertThat((SetCustomType) updateAction).hasValues("setCustomType", emptyMap(), ofId(newCustomTypeId)); + } + + @Test + void buildRemoveCustomTypeAction_WithShoppingListResource_ShouldBuildShoppingListUpdateAction() { + final UpdateAction updateAction = + ShoppingListCustomActionBuilder.of().buildRemoveCustomTypeAction(null, null); + + assertThat(updateAction).isInstanceOf(SetCustomType.class); + assertThat((SetCustomType) updateAction).hasValues("setCustomType", null, ofId(null)); + } + + @Test + void buildSetCustomFieldAction_WithShoppingListResource_ShouldBuildShoppingListUpdateAction() { + final JsonNode customFieldValue = JsonNodeFactory.instance.textNode("foo"); + final String customFieldName = "name"; + + final UpdateAction updateAction = ShoppingListCustomActionBuilder.of() + .buildSetCustomFieldAction(null, null, customFieldName, customFieldValue); + + assertThat(updateAction).isInstanceOf(SetCustomField.class); + assertThat((SetCustomField) updateAction).hasValues("setCustomField", customFieldName, customFieldValue); + } +} diff --git a/src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtilsTest.java b/src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtilsTest.java new file mode 100644 index 0000000000..b3ee626468 --- /dev/null +++ b/src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtilsTest.java @@ -0,0 +1,163 @@ +package com.commercetools.sync.shoppinglists.utils; + +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.ShoppingListDraftBuilder; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeName; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetAnonymousId; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomField; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomType; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetDeleteDaysAfterLastModification; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetDescription; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetSlug; +import io.sphere.sdk.types.CustomFields; +import io.sphere.sdk.types.CustomFieldsDraft; +import io.sphere.sdk.types.CustomFieldsDraftBuilder; +import io.sphere.sdk.types.Type; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static com.commercetools.sync.shoppinglists.utils.ShoppingListSyncUtils.buildActions; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ShoppingListSyncUtilsTest { + private static final Locale LOCALE = Locale.GERMAN; + + private static final String CUSTOM_TYPE_ID = "id"; + private static final String CUSTOM_FIELD_NAME = "field"; + private static final String CUSTOM_FIELD_VALUE = "value"; + + private ShoppingList oldShoppingList; + + @BeforeEach + void setup() { + oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getName()).thenReturn(LocalizedString.ofEnglish("name")); + when(oldShoppingList.getDeleteDaysAfterLastModification()).thenReturn(null); + + final CustomFields customFields = mock(CustomFields.class); + when(customFields.getType()).thenReturn(Type.referenceOfId(CUSTOM_TYPE_ID)); + + final Map customFieldsJsonMapMock = new HashMap<>(); + customFieldsJsonMapMock.put(CUSTOM_FIELD_NAME, JsonNodeFactory.instance.textNode(CUSTOM_FIELD_VALUE)); + when(customFields.getFieldsJsonMap()).thenReturn(customFieldsJsonMapMock); + + when(oldShoppingList.getCustom()).thenReturn(customFields); + } + + @Test + void buildActions_WithDifferentValues_ShouldReturnActions() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getSlug()).thenReturn(LocalizedString.of(LOCALE, "oldSlug")); + when(oldShoppingList.getName()).thenReturn(LocalizedString.of(LOCALE, "oldName")); + when(oldShoppingList.getDescription()).thenReturn(LocalizedString.of(LOCALE, "oldDescription")); + when(oldShoppingList.getAnonymousId()).thenReturn("oldAnonymousId"); + when(oldShoppingList.getDeleteDaysAfterLastModification()).thenReturn(50); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getSlug()).thenReturn(LocalizedString.of(LOCALE, "newSlug")); + when(newShoppingList.getName()).thenReturn(LocalizedString.of(LOCALE, "newName")); + when(newShoppingList.getDescription()).thenReturn(LocalizedString.of(LOCALE, "newDescription")); + when(newShoppingList.getAnonymousId()).thenReturn("newAnonymousId"); + when(newShoppingList.getDeleteDaysAfterLastModification()).thenReturn(70); + + final List> updateActions = buildActions(oldShoppingList, newShoppingList, + mock(ShoppingListSyncOptions.class)); + + assertThat(updateActions).isNotEmpty(); + assertThat(updateActions).contains(SetSlug.of(newShoppingList.getSlug())); + assertThat(updateActions).contains(ChangeName.of(newShoppingList.getName())); + assertThat(updateActions).contains(SetDescription.of(newShoppingList.getDescription())); + assertThat(updateActions).contains(SetAnonymousId.of(newShoppingList.getAnonymousId())); + assertThat(updateActions).contains( + SetDeleteDaysAfterLastModification.of(newShoppingList.getDeleteDaysAfterLastModification())); + } + + @Test + void buildActions_WithDifferentCustomType_ShouldBuildUpdateAction() { + final CustomFieldsDraft customFieldsDraft = + CustomFieldsDraftBuilder.ofTypeId("newId") + .addObject("newField", "newValue") + .build(); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("name")) + .custom(customFieldsDraft) + .build(); + + final List> actions = buildActions(oldShoppingList, newShoppingList, + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)).build()); + + Assertions.assertThat(actions).containsExactly( + SetCustomType.ofTypeIdAndJson(customFieldsDraft.getType().getId(), customFieldsDraft.getFields())); + } + + @Test + void buildActions_WithSameCustomTypeWithNewCustomFields_ShouldBuildUpdateAction() { + final CustomFieldsDraft sameCustomFieldDraftWithNewCustomField = + CustomFieldsDraftBuilder.ofTypeId(CUSTOM_TYPE_ID) + .addObject(CUSTOM_FIELD_NAME, CUSTOM_FIELD_VALUE) + .addObject("name_2", "value_2") + .build(); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("name")) + .custom(sameCustomFieldDraftWithNewCustomField) + .build(); + + final List> actions = buildActions(oldShoppingList, newShoppingList, + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)).build()); + + Assertions.assertThat(actions).containsExactly( + SetCustomField.ofJson("name_2", JsonNodeFactory.instance.textNode("value_2"))); + } + + @Test + void buildActions_WithSameCustomTypeWithDifferentCustomFieldValues_ShouldBuildUpdateAction() { + + final CustomFieldsDraft sameCustomFieldDraftWithNewValue = + CustomFieldsDraftBuilder.ofTypeId(CUSTOM_TYPE_ID) + .addObject(CUSTOM_FIELD_NAME, + "newValue") + .build(); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("name")) + .custom(sameCustomFieldDraftWithNewValue) + .build(); + + final List> actions = buildActions(oldShoppingList, newShoppingList, + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)).build()); + + Assertions.assertThat(actions).containsExactly( + SetCustomField.ofJson(CUSTOM_FIELD_NAME, JsonNodeFactory.instance.textNode("newValue"))); + } + + @Test + void buildActions_WithJustNewShoppingListHasNullCustomType_ShouldBuildUpdateAction() { + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("name")) + .custom(null) + .build(); + + final List> actions = buildActions(oldShoppingList, newShoppingList, + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)).build()); + + Assertions.assertThat(actions).containsExactly(SetCustomType.ofRemoveType()); + } +} diff --git a/src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListUpdateActionUtilsTest.java b/src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListUpdateActionUtilsTest.java new file mode 100644 index 0000000000..1e1b09eb6f --- /dev/null +++ b/src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListUpdateActionUtilsTest.java @@ -0,0 +1,326 @@ +package com.commercetools.sync.shoppinglists.utils; + +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.customers.Customer; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.models.Reference; +import io.sphere.sdk.models.ResourceIdentifier; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeName; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetAnonymousId; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomer; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetDeleteDaysAfterLastModification; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetDescription; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetSlug; +import org.junit.jupiter.api.Test; + +import java.util.Locale; +import java.util.Optional; +import java.util.UUID; + +import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildChangeNameUpdateAction; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildSetAnonymousIdUpdateAction; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildSetCustomerUpdateAction; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildSetDeleteDaysAfterLastModificationUpdateAction; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildSetDescriptionUpdateAction; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildSetSlugUpdateAction; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ShoppingListUpdateActionUtilsTest { + private static final Locale LOCALE = Locale.GERMAN; + + @Test + void buildSetSlugUpdateAction_WithDifferentValues_ShouldBuildUpdateAction() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getSlug()).thenReturn(LocalizedString.of(LOCALE, "oldSlug")); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getSlug()).thenReturn(LocalizedString.of(LOCALE, "newSlug")); + + final UpdateAction setSlugUpdateAction = + buildSetSlugUpdateAction(oldShoppingList, newShoppingList).orElse(null); + + assertThat(setSlugUpdateAction).isNotNull(); + assertThat(setSlugUpdateAction.getAction()).isEqualTo("setSlug"); + assertThat(((SetSlug) setSlugUpdateAction).getSlug()) + .isEqualTo(LocalizedString.of(LOCALE, "newSlug")); + } + + @Test + void buildSetSlugUpdateAction_WithNullValues_ShouldBuildUpdateAction() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getSlug()).thenReturn(LocalizedString.of(LOCALE, "oldSlug")); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getSlug()).thenReturn(null); + + final UpdateAction setSlugUpdateAction = + buildSetSlugUpdateAction(oldShoppingList, newShoppingList).orElse(null); + + assertThat(setSlugUpdateAction).isNotNull(); + assertThat(setSlugUpdateAction.getAction()).isEqualTo("setSlug"); + assertThat(((SetSlug) setSlugUpdateAction).getSlug()).isNull(); + } + + @Test + void buildSetSlugUpdateAction_WithSameValues_ShouldNotBuildUpdateAction() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getSlug()).thenReturn(LocalizedString.of(LOCALE, "oldSlug")); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getSlug()).thenReturn(LocalizedString.of(LOCALE, "oldSlug")); + + final Optional> setSlugUpdateAction = + buildSetSlugUpdateAction(oldShoppingList, newShoppingList); + + assertThat(setSlugUpdateAction).isNotNull(); + assertThat(setSlugUpdateAction).isNotPresent(); + } + + @Test + void buildChangeNameUpdateAction_WithDifferentValues_ShouldBuildUpdateAction() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getName()).thenReturn(LocalizedString.of(LOCALE, "oldName")); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getName()).thenReturn(LocalizedString.of(LOCALE, "newName")); + + final UpdateAction changeNameUpdateAction = + buildChangeNameUpdateAction(oldShoppingList, newShoppingList).orElse(null); + + assertThat(changeNameUpdateAction).isNotNull(); + assertThat(changeNameUpdateAction.getAction()).isEqualTo("changeName"); + assertThat(((ChangeName) changeNameUpdateAction).getName()).isEqualTo(LocalizedString.of(LOCALE, "newName")); + } + + @Test + void buildChangeNameUpdateAction_WithNullValues_ShouldBuildUpdateAction() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getName()).thenReturn(LocalizedString.of(LOCALE, "oldName")); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getName()).thenReturn(null); + + final UpdateAction changeNameUpdateAction = + buildChangeNameUpdateAction(oldShoppingList, newShoppingList).orElse(null); + + assertThat(changeNameUpdateAction).isNotNull(); + assertThat(changeNameUpdateAction.getAction()).isEqualTo("changeName"); + assertThat(((ChangeName) changeNameUpdateAction).getName()).isNull(); + } + + @Test + void buildChangeNameUpdateAction_WithSameValues_ShouldNotBuildUpdateAction() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getName()).thenReturn(LocalizedString.of(LOCALE, "oldName")); + + final ShoppingListDraft newShoppingListWithSameName = mock(ShoppingListDraft.class); + when(newShoppingListWithSameName.getName()) + .thenReturn(LocalizedString.of(LOCALE, "oldName")); + + final Optional> changeNameUpdateAction = + buildChangeNameUpdateAction(oldShoppingList, newShoppingListWithSameName); + + assertThat(changeNameUpdateAction).isNotNull(); + assertThat(changeNameUpdateAction).isNotPresent(); + } + + @Test + void buildSetDescriptionUpdateAction_WithDifferentValues_ShouldBuildUpdateAction() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getDescription()).thenReturn(LocalizedString.of(LOCALE, "oldDescription")); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getDescription()).thenReturn(LocalizedString.of(LOCALE, "newDescription")); + + final UpdateAction setDescriptionUpdateAction = + buildSetDescriptionUpdateAction(oldShoppingList, newShoppingList).orElse(null); + + assertThat(setDescriptionUpdateAction).isNotNull(); + assertThat(setDescriptionUpdateAction.getAction()).isEqualTo("setDescription"); + assertThat(((SetDescription) setDescriptionUpdateAction).getDescription()) + .isEqualTo(LocalizedString.of(LOCALE, "newDescription")); + } + + @Test + void buildSetDescriptionUpdateAction_WithNullValues_ShouldBuildUpdateAction() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getDescription()).thenReturn(LocalizedString.of(LOCALE, "oldDescription")); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getDescription()).thenReturn(null); + + final UpdateAction setDescriptionUpdateAction = + buildSetDescriptionUpdateAction(oldShoppingList, newShoppingList).orElse(null); + + assertThat(setDescriptionUpdateAction).isNotNull(); + assertThat(setDescriptionUpdateAction.getAction()).isEqualTo("setDescription"); + assertThat(((SetDescription) setDescriptionUpdateAction).getDescription()).isEqualTo(null); + } + + @Test + void buildSetDescriptionUpdateAction_WithSameValues_ShouldNotBuildUpdateAction() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getDescription()).thenReturn(LocalizedString.of(LOCALE, "oldDescription")); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getDescription()) + .thenReturn(LocalizedString.of(LOCALE, "oldDescription")); + + final Optional> setDescriptionUpdateAction = + buildSetDescriptionUpdateAction(oldShoppingList, newShoppingList); + + assertThat(setDescriptionUpdateAction).isNotNull(); + assertThat(setDescriptionUpdateAction).isNotPresent(); + } + + @Test + void buildSetCustomerUpdateAction_WithDifferentReference_ShouldBuildUpdateAction() { + final String customerId = UUID.randomUUID().toString(); + final Reference customerReference = Reference.of(Customer.referenceTypeId(), customerId); + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getCustomer()).thenReturn(customerReference); + + final String resolvedCustomerId = UUID.randomUUID().toString(); + final Reference newCustomerReference = Reference.of(Customer.referenceTypeId(), resolvedCustomerId); + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getCustomer()).thenReturn(newCustomerReference); + + final Optional> setCustomerUpdateAction = + buildSetCustomerUpdateAction(oldShoppingList, newShoppingList); + + assertThat(setCustomerUpdateAction).isNotEmpty(); + assertThat(setCustomerUpdateAction).containsInstanceOf(SetCustomer.class); + assertThat(((SetCustomer)setCustomerUpdateAction.get()).getCustomer()) + .isEqualTo(Reference.of(Customer.referenceTypeId(), resolvedCustomerId)); + } + + @Test + void buildSetCustomerUpdateAction_WithSameReference_ShouldNotBuildUpdateAction() { + final String customerId = UUID.randomUUID().toString(); + final Reference customerReference = Reference.of(Customer.referenceTypeId(), customerId); + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getCustomer()).thenReturn(customerReference); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getCustomer()).thenReturn(ResourceIdentifier.ofId(customerId)); + + final Optional> setCustomerUpdateAction = + buildSetCustomerUpdateAction(oldShoppingList, newShoppingList); + + assertThat(setCustomerUpdateAction).isEmpty(); + } + + @Test + void buildSetCustomerUpdateAction_WithOnlyNewReference_ShouldBuildUpdateAction() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + + final String newCustomerId = UUID.randomUUID().toString(); + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getCustomer()).thenReturn(ResourceIdentifier.ofId(newCustomerId)); + + final Optional> setCustomerUpdateAction = + buildSetCustomerUpdateAction(oldShoppingList, newShoppingList); + + assertThat(setCustomerUpdateAction).isNotEmpty(); + assertThat(setCustomerUpdateAction).containsInstanceOf(SetCustomer.class); + assertThat(((SetCustomer)setCustomerUpdateAction.get()).getCustomer()) + .isEqualTo(Reference.of(Customer.referenceTypeId(), newCustomerId)); + } + + @Test + void buildSetCustomerUpdateAction_WithoutNewReference_ShouldReturnUnsetAction() { + final String customerId = UUID.randomUUID().toString(); + final Reference customerReference = Reference.of(Customer.referenceTypeId(), customerId); + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getCustomer()).thenReturn(customerReference); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getCustomer()).thenReturn(null); + + final Optional> setCustomerUpdateAction = + buildSetCustomerUpdateAction(oldShoppingList, newShoppingList); + + assertThat(setCustomerUpdateAction).isNotEmpty(); + assertThat(setCustomerUpdateAction).containsInstanceOf(SetCustomer.class); + //Note: If the old value is set, but the new one is empty - the command will unset the customer. + assertThat(((SetCustomer) setCustomerUpdateAction.get()).getCustomer()).isNull(); + } + + @Test + void buildSetAnonymousId_WithDifferentValues_ShouldBuildUpdateAction() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getAnonymousId()).thenReturn("123"); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getAnonymousId()).thenReturn("567"); + + final Optional> setAnonymousIdUpdateAction = + buildSetAnonymousIdUpdateAction(oldShoppingList, newShoppingList); + + assertThat(setAnonymousIdUpdateAction).isNotNull(); + assertThat(setAnonymousIdUpdateAction).containsInstanceOf(SetAnonymousId.class); + } + + @Test + void buildSetAnonymousId_WithNullValues_ShouldBuildUpdateActions() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getAnonymousId()).thenReturn("123"); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getAnonymousId()).thenReturn(null); + + final Optional> setAnonymousIdUpdateAction = + buildSetAnonymousIdUpdateAction(oldShoppingList, newShoppingList); + + assertThat(setAnonymousIdUpdateAction).isNotNull(); + assertThat(setAnonymousIdUpdateAction).containsInstanceOf(SetAnonymousId.class); + } + + @Test + void buildSetAnonymousId_WithSameValues_ShouldNotBuildUpdateActions() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getAnonymousId()).thenReturn("123"); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getAnonymousId()).thenReturn("123"); + + final Optional> setAnonymousIdUpdateAction = + buildSetAnonymousIdUpdateAction(oldShoppingList, newShoppingList); + + assertThat(setAnonymousIdUpdateAction).isEmpty(); + } + + @Test + void buildSetDeleteDaysAfterLastModificationUpdateAction_WithDifferentValues_ShouldBuildUpdateAction() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getDeleteDaysAfterLastModification()).thenReturn(50); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getDeleteDaysAfterLastModification()).thenReturn(70); + + final Optional> setDeleteDaysUpdateAction = + buildSetDeleteDaysAfterLastModificationUpdateAction(oldShoppingList, newShoppingList); + + assertThat(setDeleteDaysUpdateAction).isNotNull(); + assertThat(setDeleteDaysUpdateAction).containsInstanceOf(SetDeleteDaysAfterLastModification.class); + } + + @Test + void buildSetDeleteDaysAfterLastModificationUpdateAction_WithNullValues_ShouldBuildUpdateAction() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getDeleteDaysAfterLastModification()).thenReturn(50); + + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + when(newShoppingList.getDeleteDaysAfterLastModification()).thenReturn(null); + + final Optional> setDeleteDaysUpdateAction = + buildSetDeleteDaysAfterLastModificationUpdateAction(oldShoppingList, newShoppingList); + + assertThat(setDeleteDaysUpdateAction).isNotNull(); + assertThat(setDeleteDaysUpdateAction).containsInstanceOf(SetDeleteDaysAfterLastModification.class); + } +} From 9f7eb31db704dd9b5a296615fba7ba3afeadcce0 Mon Sep 17 00:00:00 2001 From: King-Hin Leung <35692276+leungkinghin@users.noreply.github.com> Date: Mon, 9 Nov 2020 10:59:13 +0100 Subject: [PATCH 04/12] Implement Shoppinglist Sync class with IT/UT (exclude LineItem and TextLineItem) (#615) * Add ShoppingListSync without update cases * Add some unittests * Modify ShoppingListSyncTest * Add unit test case for update LineItem and TextLineItem * Add ShoppingListSyncIT for ctp project source * Remove redundant import * Add ShoppingListSyncIT for externalsource * Implement integration test cases * Refactor ShoppingListSyncIT for ctpprojectsource * Implement buildActionsAndUpdate method in Sync class * Fix problems in ShoppingListSyncTest * Fix assertion of ShoppingListSyncIT * Fix integration test case for update customer reference * Modify ShoppingListSyncIT for ctpprojectsource * Fix ShoppingListSyncIT * Fix checkstyle issue * Disable test case for TextLineItem and LineItem * Fix unused variable in lambda for buildActionsAndUpdate * Change javadoc * Remove blank line * Rename variables * Fix CustomerSyncOptions initialisation in ShoppingListSync Co-authored-by: salander85 --- .../commons/utils/ProductITUtils.java | 17 + .../commons/utils/ShoppingListITUtils.java | 114 ++- .../shoppinglists/ShoppingListSyncIT.java | 255 +++++++ .../shoppinglists/ShoppingListSyncIT.java | 311 ++++++++ .../services/impl/CustomerServiceImpl.java | 2 +- .../sync/shoppinglists/ShoppingListSync.java | 299 ++++++++ .../helpers/ShoppingListBatchValidator.java | 6 +- .../ShoppingListReferenceResolver.java | 38 + .../statistics/AssertionsForStatistics.java | 12 + .../ShoppingListSyncStatisticsAssert.java | 13 + .../shoppinglists/ShoppingListSyncTest.java | 669 ++++++++++++++++++ 11 files changed, 1728 insertions(+), 8 deletions(-) create mode 100644 src/integration-test/java/com/commercetools/sync/integration/ctpprojectsource/shoppinglists/ShoppingListSyncIT.java create mode 100644 src/integration-test/java/com/commercetools/sync/integration/externalsource/shoppinglists/ShoppingListSyncIT.java create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSync.java create mode 100644 src/test/java/com/commercetools/sync/commons/asserts/statistics/ShoppingListSyncStatisticsAssert.java create mode 100644 src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncTest.java diff --git a/src/integration-test/java/com/commercetools/sync/integration/commons/utils/ProductITUtils.java b/src/integration-test/java/com/commercetools/sync/integration/commons/utils/ProductITUtils.java index c9e96e565f..5fa42da2e1 100644 --- a/src/integration-test/java/com/commercetools/sync/integration/commons/utils/ProductITUtils.java +++ b/src/integration-test/java/com/commercetools/sync/integration/commons/utils/ProductITUtils.java @@ -12,6 +12,7 @@ import io.sphere.sdk.products.ProductDraftBuilder; import io.sphere.sdk.products.ProductVariantDraft; import io.sphere.sdk.products.ProductVariantDraftBuilder; +import io.sphere.sdk.products.commands.ProductCreateCommand; import io.sphere.sdk.products.commands.ProductDeleteCommand; import io.sphere.sdk.products.commands.ProductUpdateCommand; import io.sphere.sdk.products.commands.updateactions.Unpublish; @@ -36,12 +37,28 @@ import static com.commercetools.sync.integration.commons.utils.ProductTypeITUtils.deleteProductTypes; import static com.commercetools.sync.integration.commons.utils.StateITUtils.deleteStates; import static com.commercetools.sync.integration.commons.utils.TaxCategoryITUtils.deleteTaxCategories; +import static com.commercetools.tests.utils.CompletionStageUtil.executeBlocking; import static java.util.Collections.singletonList; import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.toList; public final class ProductITUtils { + /** + * Create a new product in the CTP project defined by the {@code ctpClient}. + * + * @param ctpClient defines the CTP project to delete the products from. + * @param productDraft product draft to be created. + */ + public static Product createProduct( + @Nonnull final SphereClient ctpClient, + @Nonnull final ProductDraft productDraft) { + + final Product product = executeBlocking( + ctpClient.execute(ProductCreateCommand.of(productDraft))); + return product; + } + /** * Deletes all products, product types, categories and types from the CTP project defined by the {@code ctpClient}. * diff --git a/src/integration-test/java/com/commercetools/sync/integration/commons/utils/ShoppingListITUtils.java b/src/integration-test/java/com/commercetools/sync/integration/commons/utils/ShoppingListITUtils.java index f1b54f1ac6..360440993a 100644 --- a/src/integration-test/java/com/commercetools/sync/integration/commons/utils/ShoppingListITUtils.java +++ b/src/integration-test/java/com/commercetools/sync/integration/commons/utils/ShoppingListITUtils.java @@ -2,20 +2,29 @@ import io.sphere.sdk.client.SphereClient; import io.sphere.sdk.models.LocalizedString; + +import io.sphere.sdk.models.ResourceIdentifier; +import io.sphere.sdk.customers.Customer; +import io.sphere.sdk.shoppinglists.LineItemDraft; import io.sphere.sdk.shoppinglists.ShoppingList; import io.sphere.sdk.shoppinglists.ShoppingListDraft; import io.sphere.sdk.shoppinglists.ShoppingListDraftBuilder; +import io.sphere.sdk.shoppinglists.TextLineItemDraft; import io.sphere.sdk.shoppinglists.commands.ShoppingListCreateCommand; import io.sphere.sdk.shoppinglists.commands.ShoppingListDeleteCommand; import io.sphere.sdk.shoppinglists.queries.ShoppingListQuery; - import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; import static com.commercetools.sync.integration.commons.utils.ITUtils.queryAndExecute; +import static com.commercetools.sync.integration.commons.utils.CustomerITUtils.deleteCustomers; import static com.commercetools.sync.integration.commons.utils.ProductITUtils.deleteAllProducts; import static com.commercetools.sync.integration.commons.utils.ProductTypeITUtils.deleteProductTypes; + import static com.commercetools.tests.utils.CompletionStageUtil.executeBlocking; + public final class ShoppingListITUtils { /** @@ -25,6 +34,7 @@ public final class ShoppingListITUtils { */ public static void deleteShoppingListTestData(@Nonnull final SphereClient ctpClient) { deleteShoppingLists(ctpClient); + deleteCustomers(ctpClient); deleteAllProducts(ctpClient); deleteProductTypes(ctpClient); } @@ -38,8 +48,9 @@ public static void deleteShoppingLists(@Nonnull final SphereClient ctpClient) { queryAndExecute(ctpClient, ShoppingListQuery.of(), ShoppingListDeleteCommand::of); } + /** - * Creates a {@link ShoppingList} in the CTP project defined byf the {@code ctpClient} in a blocking fashion. + * Creates a {@link ShoppingList} in the CTP project defined by the {@code ctpClient} in a blocking fashion. * * @param ctpClient defines the CTP project to create the ShoppingList in. * @param name the name of the ShoppingList to create. @@ -48,9 +59,104 @@ public static void deleteShoppingLists(@Nonnull final SphereClient ctpClient) { */ public static ShoppingList createShoppingList(@Nonnull final SphereClient ctpClient, @Nonnull final String name, @Nonnull final String key) { + + return createShoppingList(ctpClient, name, key, null, null, null, null); + } + + /** + * Creates a {@link ShoppingList} in the CTP project defined by the {@code ctpClient} in a blocking fashion. + * + * @param ctpClient defines the CTP project to create the ShoppingList in. + * @param name the name of the ShoppingList to create. + * @param key the key of the ShoppingList to create. + * @param desc the description of the ShoppingList to create. + * @param anonymousId the anonymous ID of the ShoppingList to create. + * @param slug the slug of the ShoppingList to create. + * @param deleteDaysAfterLastModification the deleteDaysAfterLastModification of the ShoppingList to create. + * @return the created ShoppingList. + */ + public static ShoppingList createShoppingList(@Nonnull final SphereClient ctpClient, @Nonnull final String name, + @Nonnull final String key, @Nullable final String desc, + @Nullable final String anonymousId, @Nullable final String slug, + @Nullable final Integer deleteDaysAfterLastModification) { + + final ShoppingListDraft shoppingListDraft = ShoppingListDraftBuilder.of(LocalizedString.ofEnglish(name)) + .key(key) + .description(desc == null ? null : LocalizedString.ofEnglish(desc)) + .anonymousId(anonymousId) + .slug(slug == null ? null : LocalizedString.ofEnglish(slug)) + .deleteDaysAfterLastModification(deleteDaysAfterLastModification) + .build(); + + return executeBlocking(ctpClient.execute(ShoppingListCreateCommand.of(shoppingListDraft))); + } + + /** + * Creates a {@link ShoppingList} in the CTP project defined by the {@code ctpClient} in a blocking fashion. + * + * @param ctpClient defines the CTP project to create the ShoppingList in. + * @param name the name of the ShoppingList to create. + * @param key the key of the ShoppingList to create. + * @param customer the Customer which ShoppingList refers to. + * @return the created ShoppingList. + */ + public static ShoppingList createShoppingListWithCustomer( + @Nonnull final SphereClient ctpClient, + @Nonnull final String name, + @Nonnull final String key, + @Nonnull final Customer customer) { + + final ResourceIdentifier customerResourceIdentifier = customer.toResourceIdentifier(); + final ShoppingListDraft shoppingListDraft = ShoppingListDraftBuilder.of(LocalizedString.ofEnglish(name)) + .key(key) + .customer(customerResourceIdentifier) + .build(); + + return executeBlocking(ctpClient.execute(ShoppingListCreateCommand.of(shoppingListDraft))); + } + + /** + * Creates a {@link ShoppingList} in the CTP project defined by the {@code ctpClient} in a blocking fashion. + * + * @param ctpClient defines the CTP project to create the ShoppingList in. + * @param name the name of the ShoppingList to create. + * @param key the key of the ShoppingList to create. + * @param textLineItems the list of TextLineItemDraft which ShoppingList contains. + * @return the created ShoppingList. + */ + public static ShoppingList createShoppingListWithTextLineItems( + @Nonnull final SphereClient ctpClient, + @Nonnull final String name, + @Nonnull final String key, + @Nonnull final List textLineItems) { + + final ShoppingListDraft shoppingListDraft = ShoppingListDraftBuilder.of(LocalizedString.ofEnglish(name)) + .key(key) + .textLineItems(textLineItems) + .build(); + + return executeBlocking(ctpClient.execute(ShoppingListCreateCommand.of(shoppingListDraft))); + } + + /** + * Creates a {@link ShoppingList} in the CTP project defined by the {@code ctpClient} in a blocking fashion. + * + * @param ctpClient defines the CTP project to create the ShoppingList in. + * @param name the name of the ShoppingList to create. + * @param key the key of the ShoppingList to create. + * @param lineItems the list of LineItemDraft which ShoppingList contains. + * @return the created ShoppingList. + */ + public static ShoppingList createShoppingListWithLineItems( + @Nonnull final SphereClient ctpClient, + @Nonnull final String name, + @Nonnull final String key, + @Nonnull final List lineItems) { + final ShoppingListDraft shoppingListDraft = ShoppingListDraftBuilder.of(LocalizedString.ofEnglish(name)) - .key(key) - .build(); + .key(key) + .lineItems(lineItems) + .build(); return executeBlocking(ctpClient.execute(ShoppingListCreateCommand.of(shoppingListDraft))); } diff --git a/src/integration-test/java/com/commercetools/sync/integration/ctpprojectsource/shoppinglists/ShoppingListSyncIT.java b/src/integration-test/java/com/commercetools/sync/integration/ctpprojectsource/shoppinglists/ShoppingListSyncIT.java new file mode 100644 index 0000000000..6a9ee0ce2b --- /dev/null +++ b/src/integration-test/java/com/commercetools/sync/integration/ctpprojectsource/shoppinglists/ShoppingListSyncIT.java @@ -0,0 +1,255 @@ +package com.commercetools.sync.integration.ctpprojectsource.shoppinglists; + +import com.commercetools.sync.commons.asserts.statistics.AssertionsForStatistics; + +import com.commercetools.sync.shoppinglists.ShoppingListSync; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; +import com.commercetools.sync.shoppinglists.helpers.ShoppingListSyncStatistics; + +import io.sphere.sdk.customers.Customer; +import io.sphere.sdk.customers.CustomerDraft; +import io.sphere.sdk.customers.CustomerDraftBuilder; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.shoppinglists.LineItemDraft; +import io.sphere.sdk.shoppinglists.LineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.TextLineItemDraft; +import io.sphere.sdk.shoppinglists.TextLineItemDraftBuilder; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static com.commercetools.sync.commons.asserts.statistics.AssertionsForStatistics.assertThat; +import static com.commercetools.sync.integration.commons.utils.CustomerITUtils.createCustomer; +import static com.commercetools.sync.integration.commons.utils.ShoppingListITUtils.createShoppingList; +import static com.commercetools.sync.integration.commons.utils.ShoppingListITUtils.createShoppingListWithCustomer; +import static com.commercetools.sync.integration.commons.utils.ShoppingListITUtils.createShoppingListWithLineItems; +import static com.commercetools.sync.integration.commons.utils.ShoppingListITUtils.createShoppingListWithTextLineItems; +import static com.commercetools.sync.integration.commons.utils.ShoppingListITUtils.deleteShoppingListTestData; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListReferenceResolutionUtils.buildShoppingListQuery; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListReferenceResolutionUtils.mapToShoppingListDrafts; + +import static com.commercetools.sync.integration.commons.utils.SphereClientUtils.CTP_SOURCE_CLIENT; +import static com.commercetools.sync.integration.commons.utils.SphereClientUtils.CTP_TARGET_CLIENT; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +class ShoppingListSyncIT { + private List errorMessages; + private List exceptions; + private ShoppingListSync shoppingListSync; + + @BeforeEach + void setup() { + deleteShoppingListTestData(CTP_SOURCE_CLIENT); + deleteShoppingListTestData(CTP_TARGET_CLIENT); + createShoppingList(CTP_SOURCE_CLIENT, "name-1", "key-1" ); + setUpShoppingListSync(); + } + + @AfterAll + static void tearDown() { + deleteShoppingListTestData(CTP_SOURCE_CLIENT); + deleteShoppingListTestData(CTP_TARGET_CLIENT); + } + + private void setUpShoppingListSync() { + errorMessages = new ArrayList<>(); + exceptions = new ArrayList<>(); + final ShoppingListSyncOptions shoppingListSyncOptions = ShoppingListSyncOptionsBuilder + .of(CTP_TARGET_CLIENT) + .errorCallback((exception, oldResource, newResource, actions) -> { + errorMessages.add(exception.getMessage()); + exceptions.add(exception); + }) + .build(); + shoppingListSync = new ShoppingListSync(shoppingListSyncOptions); + } + + @Test + void sync_WithoutUpdates_ShouldReturnProperStatistics() { + + final List shoppingLists = CTP_SOURCE_CLIENT + .execute(buildShoppingListQuery()) + .toCompletableFuture() + .join() + .getResults(); + + final List shoppingListDrafts = mapToShoppingListDrafts(shoppingLists); + + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(shoppingListDrafts) + .toCompletableFuture() + .join(); + + assertThat(errorMessages).isEmpty(); + assertThat(exceptions).isEmpty(); + + assertThat(shoppingListSyncStatistics).hasValues(1, 1, 0, 0); + assertThat(shoppingListSyncStatistics + .getReportMessage()) + .isEqualTo("Summary: 1 shopping lists were processed in total " + + "(1 created, 0 updated and 0 failed to sync)."); + } + + @Test + void sync_WithUpdatedShoppingList_ShouldReturnProperStatistics() { + + createShoppingList(CTP_SOURCE_CLIENT, "name-2", "key-2", "desc-2", + "anonymousId-2", "slug-2", 180); + createShoppingList(CTP_TARGET_CLIENT, "name-2", "key-2"); + + final List shoppingLists = CTP_SOURCE_CLIENT + .execute(buildShoppingListQuery()) + .toCompletableFuture() + .join() + .getResults(); + + final List shoppingListDrafts = mapToShoppingListDrafts(shoppingLists); + + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(shoppingListDrafts) + .toCompletableFuture() + .join(); + + assertThat(errorMessages).isEmpty(); + assertThat(exceptions).isEmpty(); + + AssertionsForStatistics.assertThat(shoppingListSyncStatistics).hasValues(2, 1, 1, 0); + assertThat(shoppingListSyncStatistics + .getReportMessage()) + .isEqualTo("Summary: 2 shopping lists were processed in total " + + "(1 created, 1 updated and 0 failed to sync)."); + } + + @Test + void sync_WithUpdatedCustomerReference_ShouldReturnProperStatistics() { + + final CustomerDraft customerDraft = + CustomerDraftBuilder.of("dummy-email", "dummy-password") + .key("dummy-customer-key") + .build(); + + final Customer sourceCustomer = + createCustomer(CTP_SOURCE_CLIENT, customerDraft); + + createCustomer(CTP_TARGET_CLIENT, customerDraft); + createShoppingListWithCustomer(CTP_SOURCE_CLIENT, "name-2", "key-2", sourceCustomer); + createShoppingList(CTP_TARGET_CLIENT, "name-2", "key-2"); + + final List shoppingLists = CTP_SOURCE_CLIENT + .execute(buildShoppingListQuery()) + .toCompletableFuture() + .join() + .getResults(); + + final List shoppingListDrafts = mapToShoppingListDrafts(shoppingLists); + + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(shoppingListDrafts) + .toCompletableFuture() + .join(); + + assertThat(errorMessages).isEmpty(); + assertThat(exceptions).isEmpty(); + + AssertionsForStatistics.assertThat(shoppingListSyncStatistics).hasValues(2, 1, 1, 0); + assertThat(shoppingListSyncStatistics + .getReportMessage()) + .isEqualTo("Summary: 2 shopping lists were processed in total " + + "(1 created, 1 updated and 0 failed to sync)."); + } + + // TODO - Enable test case when Textlineitem is available in UpdateActionUtils. + @Disabled + @Test + void sync_WithUpdatedTextLineItem_ShouldReturnProperStatistics() { + + // prepare shoppinglist with textlineitem in source/target project + List sourceTextLineItemDrafts = + singletonList( + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("dummy-name"), 20L).build()); + + createShoppingListWithTextLineItems(CTP_SOURCE_CLIENT, "name-2", "key-2", sourceTextLineItemDrafts); + + List targetTextLineItemDrafts = + singletonList( + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("dummy-name"), 10L).build()); + + createShoppingListWithTextLineItems(CTP_TARGET_CLIENT, "name-2", "key-2", targetTextLineItemDrafts); + + // prepare list of shoppinglist for sync + final List shoppingLists = CTP_SOURCE_CLIENT + .execute(buildShoppingListQuery()) + .toCompletableFuture() + .join() + .getResults(); + + final List updatedShoppingListDraft = mapToShoppingListDrafts(shoppingLists); + + // test and assertion + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(updatedShoppingListDraft) + .toCompletableFuture() + .join(); + + assertThat(errorMessages).isEmpty(); + assertThat(exceptions).isEmpty(); + + AssertionsForStatistics.assertThat(shoppingListSyncStatistics).hasValues(2, 1, 1, 0); + assertThat(shoppingListSyncStatistics + .getReportMessage()) + .isEqualTo("Summary: 2 shopping lists were processed in total " + + "(1 created, 1 updated and 0 failed to sync)."); + } + + // TODO - Enable test case when Lineitem is available in UpdateActionUtils. + @Disabled + @Test + void sync_WithUpdatedLineItem_ShouldReturnProperStatistics() { + + // prepare shoppinglist with textlineitem in source/target project + List sourceLineItemDrafts = + singletonList( + LineItemDraftBuilder.ofSku("dummy-sku", 20L).build()); + + createShoppingListWithLineItems(CTP_SOURCE_CLIENT, "name-2", "key-2", sourceLineItemDrafts); + + List lineItemDrafts = + singletonList( + LineItemDraftBuilder.ofSku("dummy-sku", 10L).build()); + + createShoppingListWithLineItems(CTP_TARGET_CLIENT, "name-2", "key-2", lineItemDrafts); + + // prepare list of shoppinglist for sync + final List shoppingLists = CTP_SOURCE_CLIENT + .execute(buildShoppingListQuery()) + .toCompletableFuture() + .join() + .getResults(); + + final List updatedShoppingListDraft = mapToShoppingListDrafts(shoppingLists); + + // test and assertion + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(updatedShoppingListDraft) + .toCompletableFuture() + .join(); + + assertThat(errorMessages).isEmpty(); + assertThat(exceptions).isEmpty(); + + AssertionsForStatistics.assertThat(shoppingListSyncStatistics).hasValues(1, 0, 1, 0); + assertThat(shoppingListSyncStatistics + .getReportMessage()) + .isEqualTo("Summary: 2 shopping lists were processed in total " + + "(1 created, 1 updated and 0 failed to sync)."); + } + +} diff --git a/src/integration-test/java/com/commercetools/sync/integration/externalsource/shoppinglists/ShoppingListSyncIT.java b/src/integration-test/java/com/commercetools/sync/integration/externalsource/shoppinglists/ShoppingListSyncIT.java new file mode 100644 index 0000000000..441965b32b --- /dev/null +++ b/src/integration-test/java/com/commercetools/sync/integration/externalsource/shoppinglists/ShoppingListSyncIT.java @@ -0,0 +1,311 @@ +package com.commercetools.sync.integration.externalsource.shoppinglists; + +import com.commercetools.sync.shoppinglists.ShoppingListSync; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; +import com.commercetools.sync.shoppinglists.helpers.ShoppingListSyncStatistics; + +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.customers.Customer; +import io.sphere.sdk.customers.CustomerDraft; +import io.sphere.sdk.customers.CustomerDraftBuilder; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.models.ResourceIdentifier; +import io.sphere.sdk.products.ProductDraft; +import io.sphere.sdk.products.ProductDraftBuilder; +import io.sphere.sdk.products.ProductVariantDraft; +import io.sphere.sdk.products.ProductVariantDraftBuilder; +import io.sphere.sdk.producttypes.ProductType; +import io.sphere.sdk.shoppinglists.LineItemDraft; +import io.sphere.sdk.shoppinglists.LineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.ShoppingListDraftBuilder; +import io.sphere.sdk.shoppinglists.TextLineItemDraft; +import io.sphere.sdk.shoppinglists.TextLineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeName; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetAnonymousId; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomer; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetDeleteDaysAfterLastModification; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetDescription; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetSlug; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static com.commercetools.sync.commons.asserts.statistics.AssertionsForStatistics.assertThat; +import static com.commercetools.sync.integration.commons.utils.CustomerITUtils.createCustomer; +import static com.commercetools.sync.integration.commons.utils.ProductITUtils.createProduct; +import static com.commercetools.sync.integration.commons.utils.ProductTypeITUtils.createProductType; +import static com.commercetools.sync.integration.commons.utils.ShoppingListITUtils.createShoppingList; +import static com.commercetools.sync.integration.commons.utils.ShoppingListITUtils.deleteShoppingListTestData; +import static com.commercetools.sync.integration.commons.utils.SphereClientUtils.CTP_TARGET_CLIENT; +import static com.commercetools.sync.products.ProductSyncMockUtils.PRODUCT_TYPE_RESOURCE_PATH; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListReferenceResolutionUtils.mapToShoppingListDrafts; +import static io.sphere.sdk.models.LocalizedString.ofEnglish; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +class ShoppingListSyncIT { + + private List errorMessages; + private List warningMessages; + private List exceptions; + private List> updateActionList; + + private ShoppingList existingShoppingList; + private ShoppingListSync shoppingListSync; + + @BeforeEach + void setup() { + deleteShoppingListTestData(CTP_TARGET_CLIENT); + existingShoppingList = createShoppingList(CTP_TARGET_CLIENT, "dummy-name-1" , "dummy-key-1"); + setUpCustomerSync(); + } + + private void setUpCustomerSync() { + errorMessages = new ArrayList<>();; + warningMessages = new ArrayList<>();; + exceptions = new ArrayList<>(); + updateActionList = new ArrayList<>(); + + ShoppingListSyncOptions shoppingListSyncOptions = ShoppingListSyncOptionsBuilder + .of(CTP_TARGET_CLIENT) + .errorCallback((exception, oldResource, newResource, actions) -> { + errorMessages.add(exception.getMessage()); + exceptions.add(exception); + }) + .warningCallback((exception, oldResource, newResource) + -> warningMessages.add(exception.getMessage())) + .beforeUpdateCallback((updateActions, customerDraft, customer) -> { + updateActionList.addAll(Objects.requireNonNull(updateActions)); + return updateActions; + }) + .build(); + shoppingListSync = new ShoppingListSync(shoppingListSyncOptions); + } + + @AfterAll + static void tearDown() { + deleteShoppingListTestData(CTP_TARGET_CLIENT); + } + + @Test + void sync_WithSameShoppingList_ShouldNotUpdateCustomer() { + List newShoppingListDrafts = mapToShoppingListDrafts(singletonList(existingShoppingList)); + + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(newShoppingListDrafts) + .toCompletableFuture() + .join(); + + assertThat(errorMessages).isEmpty(); + assertThat(warningMessages).isEmpty(); + assertThat(exceptions).isEmpty(); + assertThat(shoppingListSyncStatistics).hasValues(1, 0, 0, 0); + assertThat(shoppingListSyncStatistics + .getReportMessage()) + .isEqualTo("Summary: 1 shopping lists were processed in total " + + "(0 created, 0 updated and 0 failed to sync)."); + } + + @Test + void sync_WithNewShoppingList_ShouldCreateShoppingList() { + + final ShoppingListDraft newShoppingListDraft = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("dummy-name-2")) + .key("dummy-key-2") + .description(LocalizedString.ofEnglish("new-shoppinglist-description")) + .build(); + + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(newShoppingListDraft)) + .toCompletableFuture() + .join(); + + assertThat(errorMessages).isEmpty(); + assertThat(warningMessages).isEmpty(); + assertThat(exceptions).isEmpty(); + assertThat(updateActionList).isEmpty(); + + assertThat(shoppingListSyncStatistics).hasValues(1, 1, 0, 0); + + } + + @Test + void sync_WithNewShoppingListPlusCustomerReference_ShouldCreateShoppingList() { + + final ShoppingListDraft newShoppingListDraft = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("dummy-name-2")) + .key("dummy-key-2") + .customer(prepareCustomer().toResourceIdentifier()) + .description(LocalizedString.ofEnglish("new-shoppinglist-description")) + .build(); + + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(newShoppingListDraft)) + .toCompletableFuture() + .join(); + + assertThat(errorMessages).isEmpty(); + assertThat(warningMessages).isEmpty(); + assertThat(exceptions).isEmpty(); + assertThat(updateActionList).isEmpty(); + + assertThat(shoppingListSyncStatistics).hasValues(1, 1, 0, 0); + + } + + @Disabled + @Test + void sync_WithNewShoppingListPlusLineItems_ShouldCreateShoppingList() { + + final ShoppingListDraft newShoppingListDraft = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("dummy-name-2")) + .key("dummy-key-2") + .lineItems(prepareLineItemDrafts()) + .textLineItems(prepareTextLineItemDrafts()) + .description(LocalizedString.ofEnglish("new-shoppinglist-description")) + .build(); + + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(newShoppingListDraft)) + .toCompletableFuture() + .join(); + + assertThat(errorMessages).isEmpty(); + assertThat(warningMessages).isEmpty(); + assertThat(exceptions).isEmpty(); + assertThat(updateActionList).isEmpty(); + + assertThat(shoppingListSyncStatistics).hasValues(1, 1, 0, 0); + + } + + @Disabled + @Test + void sync_WithNewShoppingListPlusTextLineItems_ShouldCreateShoppingList() { + + final ShoppingListDraft newShoppingListDraft = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("dummy-name-2")) + .key("dummy-key-2") + .textLineItems(prepareTextLineItemDrafts()) + .description(LocalizedString.ofEnglish("new-shoppinglist-description")) + .build(); + + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(newShoppingListDraft)) + .toCompletableFuture() + .join(); + + assertThat(errorMessages).isEmpty(); + assertThat(warningMessages).isEmpty(); + assertThat(exceptions).isEmpty(); + assertThat(updateActionList).isEmpty(); + + assertThat(shoppingListSyncStatistics).hasValues(1, 1, 0, 0); + + } + + @Test + void sync_WithModifiedShoppingList_ShouldUpdateShoppingList() { + final ShoppingListDraft existingShoppingListDraft = + mapToShoppingListDrafts(singletonList(existingShoppingList)).get(0); + + final ShoppingListDraft updatedShoppingListDraft = ShoppingListDraftBuilder.of(existingShoppingListDraft) + .description(LocalizedString.ofEnglish("new-shoppinglist-description")) + .slug(LocalizedString.ofEnglish("new-shoppinglist-slug")) + .anonymousId("new-shoppinglist-anonymousId") + .deleteDaysAfterLastModification(180) + .name(LocalizedString.ofEnglish("new-shoppinglist-name")) + .build(); + + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(updatedShoppingListDraft)) + .toCompletableFuture() + .join(); + + assertThat(errorMessages).isEmpty(); + assertThat(warningMessages).isEmpty(); + assertThat(exceptions).isEmpty(); + assertThat(updateActionList).contains( + SetDescription.of(LocalizedString.ofEnglish("new-shoppinglist-description")), + SetSlug.of(LocalizedString.ofEnglish("new-shoppinglist-slug")), + SetAnonymousId.of("new-shoppinglist-anonymousId"), + SetDeleteDaysAfterLastModification.of(180), + ChangeName.of(LocalizedString.ofEnglish("new-shoppinglist-name")) + ); + assertThat(shoppingListSyncStatistics).hasValues(1, 0, 1, 0); + + } + + @Test + void sync_WithModifieCustomerReferenceInShoppingList_ShouldUpdateShoppingList() { + + final ShoppingListDraft existingShoppingListDraft = + mapToShoppingListDrafts(singletonList(existingShoppingList)).get(0); + + final Customer customer = prepareCustomer(); + final ResourceIdentifier customerResourceIdentifier = customer.toResourceIdentifier(); + final ShoppingListDraft updatedShoppingListDraft = ShoppingListDraftBuilder.of(existingShoppingListDraft) + .customer(customerResourceIdentifier) + .build(); + + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(updatedShoppingListDraft)) + .toCompletableFuture() + .join(); + + assertThat(errorMessages).isEmpty(); + assertThat(warningMessages).isEmpty(); + assertThat(exceptions).isEmpty(); + assertThat(updateActionList).containsExactly( + SetCustomer.of(customer) + ); + + assertThat(shoppingListSyncStatistics).hasValues(1, 0, 1, 0); + + } + + private Customer prepareCustomer() { + final CustomerDraft existingCustomerDraft = + CustomerDraftBuilder.of("dummy-customer-email", "dummy-customer-password").build(); + + return createCustomer(CTP_TARGET_CLIENT, existingCustomerDraft); + + } + + private List prepareLineItemDrafts() { + final ProductType productType = createProductType(PRODUCT_TYPE_RESOURCE_PATH, CTP_TARGET_CLIENT); + + final ProductVariantDraft masterVariant = ProductVariantDraftBuilder + .of() + .sku("dummy-lineitem-name") + .key("dummy-product-master-variant") + .build(); + + final ProductDraft productDraft = + ProductDraftBuilder.of( + productType, + ofEnglish("dummy-product-name"), + ofEnglish("dummy-product-slug") , masterVariant).build(); + + createProduct(CTP_TARGET_CLIENT, productDraft ); + + final LineItemDraft newLineItemDraft = + LineItemDraftBuilder.ofSku("dummy-lineitem-name", 10L).build(); + + return singletonList(newLineItemDraft); + + } + + private List prepareTextLineItemDrafts() { + final TextLineItemDraft newTextLineItemDraft = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("dummy-textlineitem-name"), 10L).build(); + return singletonList(newTextLineItemDraft); + } +} diff --git a/src/main/java/com/commercetools/sync/services/impl/CustomerServiceImpl.java b/src/main/java/com/commercetools/sync/services/impl/CustomerServiceImpl.java index 195cc5522f..c2c7e676ad 100644 --- a/src/main/java/com/commercetools/sync/services/impl/CustomerServiceImpl.java +++ b/src/main/java/com/commercetools/sync/services/impl/CustomerServiceImpl.java @@ -1,7 +1,7 @@ package com.commercetools.sync.services.impl; -import com.commercetools.sync.commons.exceptions.SyncException; import com.commercetools.sync.customers.CustomerSyncOptions; +import com.commercetools.sync.commons.exceptions.SyncException; import com.commercetools.sync.services.CustomerService; import io.sphere.sdk.commands.UpdateAction; import io.sphere.sdk.customers.Customer; diff --git a/src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSync.java b/src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSync.java new file mode 100644 index 0000000000..62357c5732 --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/ShoppingListSync.java @@ -0,0 +1,299 @@ +package com.commercetools.sync.shoppinglists; + +import com.commercetools.sync.commons.BaseSync; +import com.commercetools.sync.commons.exceptions.SyncException; +import com.commercetools.sync.customers.CustomerSyncOptionsBuilder; +import com.commercetools.sync.services.CustomerService; +import com.commercetools.sync.services.ShoppingListService; +import com.commercetools.sync.services.TypeService; +import com.commercetools.sync.services.impl.CustomerServiceImpl; +import com.commercetools.sync.services.impl.ShoppingListServiceImpl; +import com.commercetools.sync.services.impl.TypeServiceImpl; +import com.commercetools.sync.shoppinglists.helpers.ShoppingListBatchValidator; +import com.commercetools.sync.shoppinglists.helpers.ShoppingListReferenceResolver; +import com.commercetools.sync.shoppinglists.helpers.ShoppingListSyncStatistics; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import org.apache.commons.lang3.tuple.ImmutablePair; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import static com.commercetools.sync.commons.utils.SyncUtils.batchElements; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListSyncUtils.buildActions; +import static java.lang.String.format; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; + +/** + * This class syncs shopping list drafts with corresponding shopping list resources in the CTP project. + */ +public class ShoppingListSync extends BaseSync { + + private static final String CTP_SHOPPING_LIST_UPDATE_FAILED = + "Failed to update shopping lists with key: '%s'. Reason: %s"; + private static final String CTP_SHOPPING_LIST_FETCH_FAILED = "Failed to fetch existing shopping lists with keys: " + + "'%s'."; + private static final String FAILED_TO_PROCESS = "Failed to process the ShoppingListDraft with key:'%s'. Reason: %s"; + + + private final ShoppingListService shoppingListService; + private final ShoppingListReferenceResolver shoppingListReferenceResolver; + private final ShoppingListBatchValidator shoppingListBatchValidator; + + /** + * Takes a {@link ShoppingListSyncOptions} to instantiate a new {@link ShoppingListSync} instance that could be used + * to sync shopping list drafts in the CTP project specified in the injected {@link ShoppingListSyncOptions} + * instance. + * + * @param shoppingListSyncOptions the container of all the options of the sync process including the CTP project + * client and/or configuration and other sync-specific options. + */ + public ShoppingListSync(@Nonnull final ShoppingListSyncOptions shoppingListSyncOptions) { + + this(shoppingListSyncOptions, + new ShoppingListServiceImpl(shoppingListSyncOptions), + new CustomerServiceImpl(CustomerSyncOptionsBuilder.of(shoppingListSyncOptions.getCtpClient()).build()), + new TypeServiceImpl(shoppingListSyncOptions)); + } + + /** + * Takes a {@link ShoppingListSyncOptions} and service instances to instantiate a new {@link ShoppingListSync} + * instance that could be used to sync shopping list drafts in the CTP project specified in the injected + * {@link ShoppingListSyncOptions} instance. + * + *

NOTE: This constructor is mainly to be used for tests where the services can be mocked and passed to. + * + * @param syncOptions the container of all the options of the sync process including the CTP project + * client and/or configuration and other sync-specific options. + * @param shoppingListService the shopping list service which is responsible for fetching/caching the + * ShoppingLists from the CTP project. + * @param customerService the customer service which is responsible for fetching/caching the Customers from the + * CTP project. + * @param typeService the type service which is responsible for fetching/caching the Types from the CTP project. + */ + protected ShoppingListSync(@Nonnull final ShoppingListSyncOptions syncOptions, + @Nonnull final ShoppingListService shoppingListService, + @Nonnull final CustomerService customerService, + @Nonnull final TypeService typeService) { + + super(new ShoppingListSyncStatistics(), syncOptions); + this.shoppingListService = shoppingListService; + this.shoppingListReferenceResolver = new ShoppingListReferenceResolver(getSyncOptions(), customerService, + typeService); + this.shoppingListBatchValidator = new ShoppingListBatchValidator(getSyncOptions(), getStatistics()); + } + + /** + * Iterates through the whole {@code ShoppingListDraft}'s list and accumulates its valid drafts to batches. + * Every batch is then processed by {@link ShoppingListSync#processBatch(List)}. + * + *

Inherited doc: + * {@inheritDoc} + * + * @param shoppingListDrafts {@link List} of {@link ShoppingListDraft}'s that would be synced into CTP project. + * @return {@link CompletionStage} with {@link ShoppingListSyncStatistics} holding statistics of all sync + * processes performed by this sync instance. + */ + @Override + protected CompletionStage process( + @Nonnull final List shoppingListDrafts) { + + List> batches = batchElements(shoppingListDrafts, syncOptions.getBatchSize()); + return syncBatches(batches, completedFuture(statistics)); + } + + @Override + protected CompletionStage processBatch(@Nonnull final List batch) { + + ImmutablePair, ShoppingListBatchValidator.ReferencedKeys> + validationResult = shoppingListBatchValidator.validateAndCollectReferencedKeys(batch); + + final Set shoppingListDrafts = validationResult.getLeft(); + if (shoppingListDrafts.isEmpty()) { + statistics.incrementProcessed(batch.size()); + return completedFuture(statistics); + } + + return shoppingListReferenceResolver + .populateKeyToIdCachesForReferencedKeys(validationResult.getRight()) + .handle(ImmutablePair::new) + .thenCompose(cachingResponse -> { + final Throwable cachingException = cachingResponse.getRight(); + if (cachingException != null) { + handleError(new SyncException("Failed to build a cache of keys to ids.", cachingException), + shoppingListDrafts.size()); + return CompletableFuture.completedFuture(null); + } + + final Set shoppingListDraftKeys = + shoppingListDrafts.stream().map(ShoppingListDraft::getKey).collect(toSet()); + + return shoppingListService + .fetchMatchingShoppingListsByKeys(shoppingListDraftKeys) + .handle(ImmutablePair::new) + .thenCompose(fetchResponse -> { + final Set fetchedShoppingLists = fetchResponse.getLeft(); + final Throwable exception = fetchResponse.getRight(); + + if (exception != null) { + final String errorMessage = format(CTP_SHOPPING_LIST_FETCH_FAILED, shoppingListDraftKeys); + handleError(new SyncException(errorMessage, exception), shoppingListDraftKeys.size()); + return CompletableFuture.completedFuture(null); + } else { + return syncBatch(fetchedShoppingLists, shoppingListDrafts); + } + }); + }) + .thenApply(ignoredResult -> { + statistics.incrementProcessed(batch.size()); + return statistics; + }); + } + + @Nonnull + private CompletionStage syncBatch(@Nonnull final Set oldShoppingLists, + @Nonnull final Set newShoppingListDrafts) { + + Map keyShoppingListsMap = oldShoppingLists + .stream() + .collect(toMap(ShoppingList::getKey, identity())); + + return CompletableFuture.allOf(newShoppingListDrafts + .stream() + .map(shoppingListDraft -> + shoppingListReferenceResolver + .resolveReferences(shoppingListDraft) + .thenCompose(resolvedShoppingListDraft -> syncDraft(keyShoppingListsMap, resolvedShoppingListDraft)) + .exceptionally(completionException -> { + final String errorMessage = format(FAILED_TO_PROCESS, shoppingListDraft.getKey(), + completionException.getMessage()); + handleError(new SyncException(errorMessage, completionException), 1); + return null; + }) + ) + .map(CompletionStage::toCompletableFuture) + .toArray(CompletableFuture[]::new)); + } + + @Nonnull + private CompletionStage syncDraft(@Nonnull final Map keyShoppingListMap, + @Nonnull final ShoppingListDraft newShoppingListDraft) { + + final ShoppingList shoppingListFromMap = keyShoppingListMap.get(newShoppingListDraft.getKey()); + return Optional.ofNullable(shoppingListFromMap) + .map(oldShoppingList -> buildActionsAndUpdate(oldShoppingList, newShoppingListDraft)) + .orElseGet(() -> applyCallbackAndCreate(newShoppingListDraft)); + } + + @SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION") // https://github.com/findbugsproject/findbugs/issues/79 + @Nonnull + private CompletionStage buildActionsAndUpdate( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingListDraft) { + + final List> updateActions = + buildActions(oldShoppingList, newShoppingListDraft, syncOptions); + + final List> updateActionsAfterCallback + = syncOptions.applyBeforeUpdateCallback(updateActions, newShoppingListDraft, oldShoppingList); + + if (!updateActionsAfterCallback.isEmpty()) { + return updateShoppinglist(oldShoppingList, newShoppingListDraft, updateActionsAfterCallback); + } + + return completedFuture(null); + } + + @Nonnull + private CompletionStage updateShoppinglist( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingListDraft, + @Nonnull final List> updateActionsAfterCallback) { + + return shoppingListService + .updateShoppingList(oldShoppingList, updateActionsAfterCallback) + .handle(ImmutablePair::of) + .thenCompose(updateResponse -> { + final Throwable exception = updateResponse.getValue(); + if (exception != null) { + return executeSupplierIfConcurrentModificationException(exception, + () -> fetchAndUpdate(oldShoppingList, newShoppingListDraft), + () -> { + final String errorMessage = + format(CTP_SHOPPING_LIST_UPDATE_FAILED, + newShoppingListDraft.getKey(), + exception.getMessage()); + handleError(new SyncException(errorMessage, exception), 1); + return CompletableFuture.completedFuture(null); + }); + } else { + statistics.incrementUpdated(); + return CompletableFuture.completedFuture(null); + } + }); + } + + @Nonnull + private CompletionStage fetchAndUpdate( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingListDraft) { + + final String shoppingListKey = oldShoppingList.getKey(); + return shoppingListService + .fetchShoppingList(shoppingListKey) + .handle(ImmutablePair::of) + .thenCompose(fetchResponse -> { + final Optional fetchedShoppingListOptional = fetchResponse.getKey(); + final Throwable exception = fetchResponse.getValue(); + + if (exception != null) { + final String errorMessage = format(CTP_SHOPPING_LIST_UPDATE_FAILED, shoppingListKey, + "Failed to fetch from CTP while retrying after concurrency modification."); + handleError(new SyncException(errorMessage, exception), 1); + return CompletableFuture.completedFuture(null); + } + + return fetchedShoppingListOptional + .map(fetchedShoppingList -> buildActionsAndUpdate(fetchedShoppingList, newShoppingListDraft)) + .orElseGet(() -> { + final String errorMessage = format(CTP_SHOPPING_LIST_UPDATE_FAILED, shoppingListKey, + "Not found when attempting to fetch while retrying after concurrency modification."); + handleError(new SyncException(errorMessage, null), 1); + return CompletableFuture.completedFuture(null); + }); + }); + } + + @Nonnull + private CompletionStage applyCallbackAndCreate(@Nonnull final ShoppingListDraft shoppingListDraft) { + + return syncOptions + .applyBeforeCreateCallback(shoppingListDraft) + .map(draft -> shoppingListService + .createShoppingList(draft) + .thenAccept(optional -> { + if (optional.isPresent()) { + statistics.incrementCreated(); + } else { + statistics.incrementFailed(); + } + })) + .orElseGet(() -> CompletableFuture.completedFuture(null)); + } + + private void handleError(@Nonnull final SyncException syncException, final int failedTimes) { + + syncOptions.applyErrorCallback(syncException); + statistics.incrementFailed(failedTimes); + } +} diff --git a/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidator.java b/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidator.java index b11cb47bbe..09cd67426e 100644 --- a/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidator.java +++ b/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidator.java @@ -23,10 +23,10 @@ public class ShoppingListBatchValidator extends BaseBatchValidator { - static final String SHOPPING_LIST_DRAFT_KEY_NOT_SET = "ShoppingListDraft with name: %s doesn't have a key. " + public static final String SHOPPING_LIST_DRAFT_KEY_NOT_SET = "ShoppingListDraft with name: %s doesn't have a key. " + "Please make sure all shopping list drafts have keys."; - static final String SHOPPING_LIST_DRAFT_IS_NULL = "ShoppingListDraft is null."; - static final String SHOPPING_LIST_DRAFT_NAME_NOT_SET = "ShoppingListDraft with key: %s doesn't have a name. " + public static final String SHOPPING_LIST_DRAFT_IS_NULL = "ShoppingListDraft is null."; + public static final String SHOPPING_LIST_DRAFT_NAME_NOT_SET = "ShoppingListDraft with key: %s doesn't have a name. " + "Please make sure all shopping list drafts have names."; static final String LINE_ITEM_DRAFT_IS_NULL = "LineItemDraft at position '%d' of ShoppingListDraft " + "with key '%s' is null."; diff --git a/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListReferenceResolver.java b/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListReferenceResolver.java index 5531ece3c4..e96454caee 100644 --- a/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListReferenceResolver.java +++ b/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListReferenceResolver.java @@ -11,8 +11,14 @@ import io.sphere.sdk.shoppinglists.ShoppingListDraftBuilder; import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import static com.commercetools.sync.commons.utils.CompletableFutureUtils.collectionOfFuturesToFutureOfCollection; import static com.commercetools.sync.commons.utils.CompletableFutureUtils.mapValuesToFutureOfCompletedValues; import static io.sphere.sdk.utils.CompletableFutureUtils.exceptionallyCompletedFuture; import static java.lang.String.format; @@ -29,6 +35,7 @@ public final class ShoppingListReferenceResolver + "ShoppingListDraft with key:'%s'. "; private final CustomerService customerService; + private final TypeService typeService; private final LineItemReferenceResolver lineItemReferenceResolver; private final TextLineItemReferenceResolver textLineItemReferenceResolver; @@ -50,6 +57,7 @@ public ShoppingListReferenceResolver(@Nonnull final ShoppingListSyncOptions shop this.lineItemReferenceResolver = new LineItemReferenceResolver(shoppingListSyncOptions, typeService); this.textLineItemReferenceResolver = new TextLineItemReferenceResolver(shoppingListSyncOptions, typeService); this.customerService = customerService; + this.typeService = typeService; } /** @@ -144,4 +152,34 @@ private CompletionStage resolveTextLineItemReferences( return completedFuture(draftBuilder); } + + /** + * Calls the {@code cacheKeysToIds} service methods to fetch all the referenced keys + * (i.e custom type, customer) from the CTP to populate caches for the reference resolution. + * + *

Note: This method is only to be used internally by the library to improve performance. + * + * @param referencedKeys a wrapper for the custom type and customer references to fetch the keys, and store the + * corresponding keys -> ids into cached maps. + * @return {@link CompletionStage}<{@link Map}<{@link String}>{@link String}>> in which the results + * of its completions contains a map of requested references keys -> ids of customer references. + */ + @Nonnull + public CompletableFuture>> populateKeyToIdCachesForReferencedKeys( + @Nonnull final ShoppingListBatchValidator.ReferencedKeys referencedKeys) { + + final List>> futures = new ArrayList<>(); + + final Set typeKeys = referencedKeys.getTypeKeys(); + if (!typeKeys.isEmpty()) { + futures.add(typeService.cacheKeysToIds(typeKeys)); + } + + final Set customerKeys = referencedKeys.getCustomerKeys(); + if (!customerKeys.isEmpty()) { + futures.add(customerService.cacheKeysToIds(customerKeys)); + } + + return collectionOfFuturesToFutureOfCollection(futures, toList()); + } } diff --git a/src/test/java/com/commercetools/sync/commons/asserts/statistics/AssertionsForStatistics.java b/src/test/java/com/commercetools/sync/commons/asserts/statistics/AssertionsForStatistics.java index 9a5987fb4a..ab42fd47c3 100644 --- a/src/test/java/com/commercetools/sync/commons/asserts/statistics/AssertionsForStatistics.java +++ b/src/test/java/com/commercetools/sync/commons/asserts/statistics/AssertionsForStatistics.java @@ -7,6 +7,7 @@ import com.commercetools.sync.inventories.helpers.InventorySyncStatistics; import com.commercetools.sync.products.helpers.ProductSyncStatistics; import com.commercetools.sync.producttypes.helpers.ProductTypeSyncStatistics; +import com.commercetools.sync.shoppinglists.helpers.ShoppingListSyncStatistics; import com.commercetools.sync.states.helpers.StateSyncStatistics; import com.commercetools.sync.taxcategories.helpers.TaxCategorySyncStatistics; import com.commercetools.sync.types.helpers.TypeSyncStatistics; @@ -127,4 +128,15 @@ public static CustomObjectSyncStatisticsAssert assertThat(@Nullable final Custom public static CustomerSyncStatisticsAssert assertThat(@Nullable final CustomerSyncStatistics statistics) { return new CustomerSyncStatisticsAssert(statistics); } + + /** + * Create assertion for {@link ShoppingListSyncStatistics}. + * + * @param statistics the actual value. + * @return the created assertion object. + */ + @Nonnull + public static ShoppingListSyncStatisticsAssert assertThat(@Nullable final ShoppingListSyncStatistics statistics) { + return new ShoppingListSyncStatisticsAssert(statistics); + } } diff --git a/src/test/java/com/commercetools/sync/commons/asserts/statistics/ShoppingListSyncStatisticsAssert.java b/src/test/java/com/commercetools/sync/commons/asserts/statistics/ShoppingListSyncStatisticsAssert.java new file mode 100644 index 0000000000..00ddf35832 --- /dev/null +++ b/src/test/java/com/commercetools/sync/commons/asserts/statistics/ShoppingListSyncStatisticsAssert.java @@ -0,0 +1,13 @@ +package com.commercetools.sync.commons.asserts.statistics; + +import com.commercetools.sync.shoppinglists.helpers.ShoppingListSyncStatistics; + +import javax.annotation.Nullable; + +public final class ShoppingListSyncStatisticsAssert extends + AbstractSyncStatisticsAssert { + + ShoppingListSyncStatisticsAssert(@Nullable final ShoppingListSyncStatistics actual) { + super(actual, ShoppingListSyncStatisticsAssert.class); + } +} diff --git a/src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncTest.java b/src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncTest.java new file mode 100644 index 0000000000..df5f2300be --- /dev/null +++ b/src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncTest.java @@ -0,0 +1,669 @@ +package com.commercetools.sync.shoppinglists; + +import com.commercetools.sync.commons.asserts.statistics.AssertionsForStatistics; +import com.commercetools.sync.commons.exceptions.SyncException; +import com.commercetools.sync.services.CustomerService; +import com.commercetools.sync.services.ShoppingListService; +import com.commercetools.sync.services.TypeService; +import com.commercetools.sync.shoppinglists.helpers.ShoppingListSyncStatistics; +import io.sphere.sdk.client.BadRequestException; +import io.sphere.sdk.client.ConcurrentModificationException; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.models.ResourceIdentifier; +import io.sphere.sdk.models.SphereException; +import io.sphere.sdk.shoppinglists.LineItem; +import io.sphere.sdk.shoppinglists.LineItemDraft; +import io.sphere.sdk.shoppinglists.LineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.ShoppingListDraftBuilder; +import io.sphere.sdk.shoppinglists.TextLineItem; +import io.sphere.sdk.shoppinglists.TextLineItemDraft; +import io.sphere.sdk.shoppinglists.TextLineItemDraftBuilder; +import io.sphere.sdk.types.CustomFieldsDraft; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletionException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static com.commercetools.sync.shoppinglists.helpers.ShoppingListBatchValidator.SHOPPING_LIST_DRAFT_IS_NULL; +import static com.commercetools.sync.shoppinglists.helpers.ShoppingListBatchValidator.SHOPPING_LIST_DRAFT_KEY_NOT_SET; +import static com.commercetools.sync.shoppinglists.helpers.ShoppingListBatchValidator.SHOPPING_LIST_DRAFT_NAME_NOT_SET; +import static io.sphere.sdk.utils.CompletableFutureUtils.exceptionallyCompletedFuture; +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.concurrent.CompletableFuture.supplyAsync; +import static org.assertj.core.api.Assertions.as; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.STRING; +import static org.assertj.core.api.InstanceOfAssertFactories.THROWABLE; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ShoppingListSyncTest { + + private ShoppingListSyncOptions syncOptions; + private List errorMessages; + private List exceptions; + + @BeforeEach + void setup() { + errorMessages = new ArrayList<>(); + exceptions = new ArrayList<>(); + final SphereClient ctpClient = mock(SphereClient.class); + + syncOptions = ShoppingListSyncOptionsBuilder + .of(ctpClient) + .errorCallback((exception, oldResource, newResource, updateActions) -> { + errorMessages.add(exception.getMessage()); + exceptions.add(exception); + }) + .build(); + } + + @Test + void sync_WithNullDraft_ShouldExecuteCallbackOnErrorAndIncreaseFailedCounter() { + //preparation + ShoppingListSync shoppingListSync = new ShoppingListSync(syncOptions); + + //test + ShoppingListSyncStatistics statistics = shoppingListSync + .sync(singletonList(null)) + .toCompletableFuture() + .join(); + + AssertionsForStatistics.assertThat(statistics).hasValues(1, 0, 0, 1); + + //assertions + assertThat(errorMessages) + .hasSize(1) + .singleElement(as(STRING)) + .isEqualTo(SHOPPING_LIST_DRAFT_IS_NULL); + } + + @Test + void sync_WithNullKeyDraft_ShouldExecuteCallbackOnErrorAndIncreaseFailedCounter() { + //preparation + ShoppingListDraft shoppingListDraft = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("shopping-list-name")).build(); + ShoppingListSync shoppingListSync = new ShoppingListSync(syncOptions); + + //test + ShoppingListSyncStatistics statistics = shoppingListSync + .sync(singletonList(shoppingListDraft)) + .toCompletableFuture() + .join(); + + AssertionsForStatistics.assertThat(statistics).hasValues(1, 0, 0, 1); + + //assertions + assertThat(errorMessages) + .hasSize(1) + .singleElement(as(STRING)) + .isEqualTo(format(SHOPPING_LIST_DRAFT_KEY_NOT_SET, shoppingListDraft.getName())); + } + + @Test + void sync_WithEmptyKeyDraft_ShouldExecuteCallbackOnErrorAndIncreaseFailedCounter() { + //preparation + ShoppingListDraft shoppingListDraft = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("shopping-list-name")).key("").build(); + ShoppingListSync shoppingListSync = new ShoppingListSync(syncOptions); + + //test + ShoppingListSyncStatistics statistics = shoppingListSync + .sync(singletonList(shoppingListDraft)) + .toCompletableFuture() + .join(); + + AssertionsForStatistics.assertThat(statistics).hasValues(1, 0, 0, 1); + + //assertions + assertThat(errorMessages) + .hasSize(1) + .singleElement(as(STRING)) + .isEqualTo(format(SHOPPING_LIST_DRAFT_KEY_NOT_SET, shoppingListDraft.getName())); + } + + @Test + void sync_WithoutName_ShouldExecuteCallbackOnErrorAndIncreaseFailedCounter() { + //preparation + ShoppingListDraft shoppingListDraft = + ShoppingListDraftBuilder.of(LocalizedString.of()).key("shopping-list-key").build(); + ShoppingListSync shoppingListSync = new ShoppingListSync(syncOptions); + + //test + ShoppingListSyncStatistics statistics = shoppingListSync + .sync(singletonList(shoppingListDraft)) + .toCompletableFuture() + .join(); + + AssertionsForStatistics.assertThat(statistics).hasValues(1, 0, 0, 1); + + //assertions + assertThat(errorMessages) + .hasSize(1) + .singleElement(as(STRING)) + .isEqualTo(format(SHOPPING_LIST_DRAFT_NAME_NOT_SET, shoppingListDraft.getKey())); + } + + @Test + void sync_WithExceptionOnCachingKeysToIds_ShouldExecuteCallbackOnErrorAndIncreaseFailedCounter() { + //preparation + final TypeService typeService = mock(TypeService.class); + final CustomerService customerService = mock(CustomerService.class); + + when(typeService.cacheKeysToIds(any())) + .thenReturn(supplyAsync(() -> { + throw new SphereException(); + })); + + when(customerService.cacheKeysToIds(any())) + .thenReturn(supplyAsync(() -> { + throw new SphereException(); + })); + + + final ShoppingListSync shoppingListSync = new ShoppingListSync(syncOptions, mock(ShoppingListService.class), + customerService , typeService); + + ShoppingListDraft shoppingListDraft = + ShoppingListDraftBuilder + .of(LocalizedString.ofEnglish("shopping-list-name")) + .key("shopping-list-key") + .customer(ResourceIdentifier.ofKey("customer-key")) + .custom(CustomFieldsDraft.ofTypeKeyAndJson("typeKey", emptyMap())) + .build(); + + //test + ShoppingListSyncStatistics statistics = shoppingListSync + .sync(singletonList(shoppingListDraft)) + .toCompletableFuture() + .join(); + + AssertionsForStatistics.assertThat(statistics).hasValues(1, 0, 0, 1); + + //assertions + assertThat(errorMessages) + .hasSize(1) + .singleElement(as(STRING)) + .isEqualTo("Failed to build a cache of keys to ids."); + + assertThat(exceptions) + .hasSize(1) + .singleElement(as(THROWABLE)) + .isExactlyInstanceOf(SyncException.class) + .hasCauseExactlyInstanceOf(CompletionException.class) + .hasRootCauseExactlyInstanceOf(SphereException.class); + } + + @Test + void sync_WithErrorFetchingExistingKeys_ShouldExecuteCallbackOnErrorAndIncreaseFailedCounter() { + final ShoppingListService shoppingListService = mock(ShoppingListService.class); + + when(shoppingListService.fetchMatchingShoppingListsByKeys(singleton("shopping-list-key"))) + .thenReturn(supplyAsync(() -> { + throw new SphereException(); + })); + + final ShoppingListSync shoppingListSync = new ShoppingListSync(syncOptions, shoppingListService, + mock(CustomerService.class), mock(TypeService.class)); + + ShoppingListDraft shoppingListDraft = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("shopping-list-name")) + .key("shopping-list-key") + .customer(ResourceIdentifier.ofKey("customer-key")) + .custom(CustomFieldsDraft.ofTypeKeyAndJson("typeKey", emptyMap())) + .build(); + // test + final ShoppingListSyncStatistics statistics = shoppingListSync + .sync(singletonList(shoppingListDraft)) + .toCompletableFuture() + .join(); + + + // assertions + AssertionsForStatistics.assertThat(statistics).hasValues(1, 0, 0, 1); + + assertThat(errorMessages) + .hasSize(1) + .singleElement(as(STRING)) + .isEqualTo("Failed to fetch existing shopping lists with keys: '[shopping-list-key]'."); + + assertThat(exceptions) + .hasSize(1) + .singleElement(as(THROWABLE)) + .isExactlyInstanceOf(SyncException.class) + .hasCauseExactlyInstanceOf(CompletionException.class) + .hasRootCauseExactlyInstanceOf(SphereException.class); + } + + @Test + void sync_WithOnlyDraftsToCreate_ShouldCallBeforeCreateCallbackAndIncrementCreated() { + // preparation + final ShoppingListService mockShoppingListService = mock(ShoppingListService.class); + final ShoppingList mockShoppingList = mock(ShoppingList.class); + when(mockShoppingListService.fetchMatchingShoppingListsByKeys(singleton("shoppingListKey"))) + .thenReturn(completedFuture(new HashSet<>(singletonList(mockShoppingList)))); + + when(mockShoppingListService.createShoppingList(any())) + .thenReturn(completedFuture(Optional.of(mockShoppingList))); + + final ShoppingListSyncOptions spySyncOptions = spy(syncOptions); + final ShoppingListSync shoppingListSync = new ShoppingListSync(spySyncOptions, mockShoppingListService, + mock(CustomerService.class) , mock(TypeService.class)); + + final ShoppingListDraft shoppingListDraft = + ShoppingListDraftBuilder + .of(LocalizedString.ofEnglish("NAME")) + .key("shoppingListKey") + .build(); + + //test + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(shoppingListDraft)) + .toCompletableFuture() + .join(); + + // assertions + AssertionsForStatistics.assertThat(shoppingListSyncStatistics).hasValues(1, 1, 0, 0); + + verify(spySyncOptions).applyBeforeCreateCallback(shoppingListDraft); + verify(spySyncOptions, never()).applyBeforeUpdateCallback(any(), any(), any()); + } + + @Test + void sync_FailedOnCreation_ShouldCallBeforeCreateCallbackAndIncrementFailed() { + // preparation + final ShoppingListService mockShoppingListService = mock(ShoppingListService.class); + final ShoppingList mockShoppingList = mock(ShoppingList.class); + when(mockShoppingListService.fetchMatchingShoppingListsByKeys(singleton("shoppingListKey"))) + .thenReturn(completedFuture(new HashSet<>(singletonList(mockShoppingList)))); + + // simulate an error during create, service will return an empty optional. + when(mockShoppingListService.createShoppingList(any())) + .thenReturn(completedFuture(Optional.empty())); + + final ShoppingListSyncOptions spySyncOptions = spy(syncOptions); + final ShoppingListSync shoppingListSync = new ShoppingListSync(spySyncOptions, mockShoppingListService, + mock(CustomerService.class), mock(TypeService.class)); + + final ShoppingListDraft shoppingListDraft = + ShoppingListDraftBuilder + .of(LocalizedString.ofEnglish("NAME")) + .key("shoppingListKey") + .build(); + + //test + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(shoppingListDraft)) + .toCompletableFuture() + .join(); + + // assertions + AssertionsForStatistics.assertThat(shoppingListSyncStatistics).hasValues(1, 0, 0, 1); + + verify(spySyncOptions).applyBeforeCreateCallback(shoppingListDraft); + verify(spySyncOptions, never()).applyBeforeUpdateCallback(any(), any(), any()); + } + + @Test + void sync_WithOnlyDraftsToUpdate_ShouldOnlyCallBeforeUpdateCallback() { + // preparation + final ShoppingListService mockShoppingListService = mock(ShoppingListService.class); + final ShoppingList mockShoppingList = mock(ShoppingList.class); + when(mockShoppingList.getKey()).thenReturn("shoppingListKey"); + + when(mockShoppingListService.fetchMatchingShoppingListsByKeys(anySet())) + .thenReturn(completedFuture(singleton(mockShoppingList))); + + when(mockShoppingListService.updateShoppingList(any(), anyList())) + .thenReturn(completedFuture(mockShoppingList)); + + final ShoppingListSyncOptions spySyncOptions = spy(syncOptions); + final ShoppingListSync shoppingListSync = new ShoppingListSync(spySyncOptions, mockShoppingListService, + mock(CustomerService.class) , mock(TypeService.class)); + + final ShoppingListDraft shoppingListDraft = + ShoppingListDraftBuilder + .of(LocalizedString.ofEnglish("NAME")) + .key("shoppingListKey") + .build(); + + //test + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(shoppingListDraft)) + .toCompletableFuture() + .join(); + + // assertions + AssertionsForStatistics.assertThat(shoppingListSyncStatistics).hasValues(1, 0, 1, 0); + + verify(spySyncOptions).applyBeforeUpdateCallback(any(), any(), any()); + verify(spySyncOptions, never()).applyBeforeCreateCallback(shoppingListDraft); + } + + // TODO - Enable the test case when LineItem is available in UpdateActionUtils + @Disabled + @Test + void sync_WithUnchangedShoppingListDraftAndUpdatedLineItemDraft_ShouldIncrementUpdated() { + // preparation + final ShoppingListService mockShoppingListService = mock(ShoppingListService.class); + final ShoppingList mockShoppingList = mock(ShoppingList.class); + final LineItem mockLineItem = mock(LineItem.class); + when(mockShoppingList.getKey()).thenReturn("shoppingListKey"); + when(mockShoppingList.getName()).thenReturn(LocalizedString.ofEnglish("shoppingListName")); + when(mockLineItem.getVariant().getSku()).thenReturn("dummy-sku"); + when(mockLineItem.getQuantity()).thenReturn(10L); + when(mockShoppingListService.fetchMatchingShoppingListsByKeys(anySet())) + .thenReturn(completedFuture(singleton(mockShoppingList))); + + final ShoppingListSyncOptions spySyncOptions = spy(syncOptions); + final ShoppingListSync shoppingListSync = new ShoppingListSync(spySyncOptions, mockShoppingListService, + mock(CustomerService.class), mock(TypeService.class)); + + final List lineItemDrafts = singletonList( + LineItemDraftBuilder.ofSku("dummy-sku", 5L).build()); + + final ShoppingListDraft shoppingListDraft = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("shoppingListName")) + .key("shoppingListKey") + .lineItems(lineItemDrafts) + .build(); + + //test + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(shoppingListDraft)) + .toCompletableFuture() + .join(); + + // assertions + AssertionsForStatistics.assertThat(shoppingListSyncStatistics).hasValues(1, 0, 1, 0); + + verify(spySyncOptions).applyBeforeUpdateCallback(any(), any(), any()); + verify(spySyncOptions, never()).applyBeforeCreateCallback(shoppingListDraft); + } + + // TODO - Enable the test case when TextLineItem is available in UpdateActionUtils + @Disabled + @Test + void sync_WithUnchangedShoppingListDraftAndUpdatedTextLineItemDraft_ShouldIncrementUpdated() { + // preparation + final ShoppingListService mockShoppingListService = mock(ShoppingListService.class); + final ShoppingList mockShoppingList = mock(ShoppingList.class); + final TextLineItem mockTextLineItem = mock(TextLineItem.class); + when(mockShoppingList.getKey()).thenReturn("shoppingListKey"); + when(mockShoppingList.getName()).thenReturn(LocalizedString.ofEnglish("shoppingListName")); + when(mockTextLineItem.getName()).thenReturn(LocalizedString.ofEnglish("textLineItemName")); + when(mockTextLineItem.getQuantity()).thenReturn(10L); + when(mockShoppingListService.fetchMatchingShoppingListsByKeys(anySet())) + .thenReturn(completedFuture(singleton(mockShoppingList))); + + final ShoppingListSyncOptions spySyncOptions = spy(syncOptions); + final ShoppingListSync shoppingListSync = new ShoppingListSync(spySyncOptions, mockShoppingListService, + mock(CustomerService.class), mock(TypeService.class)); + + final List textLineItemDrafts = singletonList( + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("textLineItemName"), 5L).build()); + + final ShoppingListDraft shoppingListDraft = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("shoppingListName")) + .key("shoppingListKey") + .textLineItems(textLineItemDrafts) + .build(); + + //test + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(shoppingListDraft)) + .toCompletableFuture() + .join(); + + // assertions + AssertionsForStatistics.assertThat(shoppingListSyncStatistics).hasValues(1, 0, 1, 0); + + verify(spySyncOptions).applyBeforeUpdateCallback(any(), any(), any()); + verify(spySyncOptions, never()).applyBeforeCreateCallback(shoppingListDraft); + } + + @Test + void sync_WithoutUpdateActions_ShouldNotIncrementUpdated() { + // preparation + final ShoppingListService mockShoppingListService = mock(ShoppingListService.class); + final ShoppingList mockShoppingList = mock(ShoppingList.class); + when(mockShoppingList.getKey()).thenReturn("shoppingListKey"); + when(mockShoppingList.getName()).thenReturn(LocalizedString.ofEnglish("shoppingListName")); + when(mockShoppingList.getDescription()).thenReturn(LocalizedString.ofEnglish("shoppingListDesc")); + when(mockShoppingList.getSlug()).thenReturn(LocalizedString.ofEnglish("shoppingListSlug")); + when(mockShoppingList.getAnonymousId()).thenReturn("shoppingListAnonymousId"); + when(mockShoppingList.getDeleteDaysAfterLastModification()).thenReturn(360); + + when(mockShoppingListService.fetchMatchingShoppingListsByKeys(anySet())) + .thenReturn(completedFuture(singleton(mockShoppingList))); + + final ShoppingListSyncOptions spySyncOptions = spy(syncOptions); + final ShoppingListSync shoppingListSync = new ShoppingListSync(spySyncOptions, mockShoppingListService, + mock(CustomerService.class), mock(TypeService.class)); + + final ShoppingListDraft shoppingListDraft = + ShoppingListDraftBuilder + .of(LocalizedString.ofEnglish("shoppingListName")) + .key("shoppingListKey") + .description(mockShoppingList.getDescription()) + .slug(mockShoppingList.getSlug()) + .anonymousId(mockShoppingList.getAnonymousId()) + .deleteDaysAfterLastModification(mockShoppingList.getDeleteDaysAfterLastModification()) + .build(); + + //test + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(shoppingListDraft)) + .toCompletableFuture() + .join(); + + // assertions + AssertionsForStatistics.assertThat(shoppingListSyncStatistics).hasValues(1, 0, 0, 0); + + verify(spySyncOptions).applyBeforeUpdateCallback(emptyList(), shoppingListDraft, mockShoppingList); + verify(spySyncOptions, never()).applyBeforeCreateCallback(shoppingListDraft); + } + + @Test + void sync_WithBadRequestException_ShouldFailToUpdateAndIncreaseFailedCounter() { + // preparation + final ShoppingListService mockShoppingListService = mock(ShoppingListService.class); + final ShoppingList mockShoppingList = mock(ShoppingList.class); + when(mockShoppingList.getKey()).thenReturn("shoppingListKey"); + + when(mockShoppingListService.fetchMatchingShoppingListsByKeys(anySet())) + .thenReturn(completedFuture(singleton(mockShoppingList))); + + when(mockShoppingListService.updateShoppingList(any(), anyList())) + .thenReturn(exceptionallyCompletedFuture(new BadRequestException("Invalid request"))); + + final ShoppingListSyncOptions spySyncOptions = spy(syncOptions); + final ShoppingListSync shoppingListSync = new ShoppingListSync(spySyncOptions, mockShoppingListService, + mock(CustomerService.class), mock(TypeService.class)); + + final ShoppingListDraft shoppingListDraft = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("shoppingListName")) + .key("shoppingListKey") + .build(); + + //test + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(shoppingListDraft)) + .toCompletableFuture() + .join(); + + // assertions + AssertionsForStatistics.assertThat(shoppingListSyncStatistics).hasValues(1, 0, 0, 1); + + assertThat(errorMessages) + .hasSize(1) + .singleElement(as(STRING)) + .contains("Invalid request"); + + assertThat(exceptions) + .hasSize(1) + .singleElement(as(THROWABLE)) + .isExactlyInstanceOf(SyncException.class) + .hasRootCauseExactlyInstanceOf(BadRequestException.class); + } + + @Test + void sync_WithConcurrentModificationException_ShouldRetryToUpdateNewCustomerWithSuccess() { + // preparation + final ShoppingListService mockShoppingListService = mock(ShoppingListService.class); + final ShoppingList mockShoppingList = mock(ShoppingList.class); + when(mockShoppingList.getName()).thenReturn(LocalizedString.ofEnglish("shoppingListName")); + when(mockShoppingList.getKey()).thenReturn("shoppingListKey"); + when(mockShoppingList.getDescription()).thenReturn(LocalizedString.ofEnglish("shoppingListDesc")); + when(mockShoppingList.getSlug()).thenReturn(LocalizedString.ofEnglish("shoppingListSlug")); + when(mockShoppingList.getAnonymousId()).thenReturn("shoppingListAnonymousId"); + when(mockShoppingList.getDeleteDaysAfterLastModification()).thenReturn(360); + + when(mockShoppingListService.fetchMatchingShoppingListsByKeys(anySet())) + .thenReturn(completedFuture(singleton(mockShoppingList))); + + when(mockShoppingListService.updateShoppingList(any(), anyList())) + .thenReturn(exceptionallyCompletedFuture(new SphereException(new ConcurrentModificationException()))) + .thenReturn(completedFuture(mockShoppingList)); + + when(mockShoppingListService.fetchShoppingList("shoppingListKey")) + .thenReturn(completedFuture(Optional.of(mockShoppingList))); + + final ShoppingListSyncOptions spySyncOptions = spy(syncOptions); + final ShoppingListSync shoppingListSync = new ShoppingListSync(spySyncOptions, mockShoppingListService, + mock(CustomerService.class), mock(TypeService.class)); + + final ShoppingListDraft shoppingListDraft = + ShoppingListDraftBuilder + .of(LocalizedString.ofEnglish("shoppingListName")) + .key("shoppingListKey") + .description(LocalizedString.ofEnglish("newShoppingListDesc")) + .slug(mockShoppingList.getSlug()) + .anonymousId(mockShoppingList.getAnonymousId()) + .deleteDaysAfterLastModification(mockShoppingList.getDeleteDaysAfterLastModification()) + .build(); + + //test + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(shoppingListDraft)) + .toCompletableFuture() + .join(); + + // assertions + AssertionsForStatistics.assertThat(shoppingListSyncStatistics).hasValues(1, 0, 1, 0); + } + + @Test + void sync_WithConcurrentModificationExceptionAndFailedFetch_ShouldFailToReFetchAndUpdate() { + // preparation + final ShoppingListService mockShoppingListService = mock(ShoppingListService.class); + final ShoppingList mockShoppingList = mock(ShoppingList.class); + when(mockShoppingList.getKey()).thenReturn("shoppingListKey"); + + when(mockShoppingListService.fetchMatchingShoppingListsByKeys(anySet())) + .thenReturn(completedFuture(singleton(mockShoppingList))); + + when(mockShoppingListService.updateShoppingList(any(), anyList())) + .thenReturn(exceptionallyCompletedFuture(new SphereException(new ConcurrentModificationException()))) + .thenReturn(completedFuture(mockShoppingList)); + + when(mockShoppingListService.fetchShoppingList("shoppingListKey")) + .thenReturn(exceptionallyCompletedFuture(new SphereException())); + + final ShoppingListSyncOptions spySyncOptions = spy(syncOptions); + final ShoppingListSync shoppingListSync = new ShoppingListSync(spySyncOptions, mockShoppingListService, + mock(CustomerService.class), mock(TypeService.class)); + + final ShoppingListDraft shoppingListDraft = + ShoppingListDraftBuilder + .of(LocalizedString.ofEnglish("shoppingListName")) + .key("shoppingListKey") + .build(); + + //test + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(shoppingListDraft)) + .toCompletableFuture() + .join(); + + // assertions + AssertionsForStatistics.assertThat(shoppingListSyncStatistics).hasValues(1, 0, 0, 1); + + assertThat(errorMessages) + .hasSize(1) + .singleElement(as(STRING)) + .contains("Failed to fetch from CTP while retrying after concurrency modification."); + + assertThat(exceptions) + .hasSize(1) + .singleElement(as(THROWABLE)) + .isExactlyInstanceOf(SyncException.class) + .hasRootCauseExactlyInstanceOf(SphereException.class); + } + + @Test + void sync_WithConcurrentModificationExceptionAndUnexpectedDelete_ShouldFailToReFetchAndUpdate() { + // preparation + final ShoppingListService mockShoppingListService = mock(ShoppingListService.class); + final ShoppingList mockShoppingList = mock(ShoppingList.class); + when(mockShoppingList.getKey()).thenReturn("shoppingListKey"); + + when(mockShoppingListService.fetchMatchingShoppingListsByKeys(anySet())) + .thenReturn(completedFuture(singleton(mockShoppingList))); + + when(mockShoppingListService.updateShoppingList(any(), anyList())) + .thenReturn(exceptionallyCompletedFuture(new SphereException(new ConcurrentModificationException()))) + .thenReturn(completedFuture(mockShoppingList)); + + when(mockShoppingListService.fetchShoppingList("shoppingListKey")) + .thenReturn(completedFuture(Optional.empty())); + + final ShoppingListSyncOptions spySyncOptions = spy(syncOptions); + final ShoppingListSync shoppingListSync = new ShoppingListSync(spySyncOptions, mockShoppingListService, + mock(CustomerService.class), mock(TypeService.class)); + + final ShoppingListDraft shoppingListDraft = + ShoppingListDraftBuilder + .of(LocalizedString.ofEnglish("shoppingListName")) + .key("shoppingListKey") + .build(); + + //test + final ShoppingListSyncStatistics shoppingListSyncStatistics = shoppingListSync + .sync(singletonList(shoppingListDraft)) + .toCompletableFuture() + .join(); + + // assertions + AssertionsForStatistics.assertThat(shoppingListSyncStatistics).hasValues(1, 0, 0, 1); + + assertThat(errorMessages) + .hasSize(1) + .singleElement(as(STRING)) + .contains("Not found when attempting to fetch while retrying after concurrency modification."); + + assertThat(exceptions) + .hasSize(1) + .singleElement(as(THROWABLE)) + .isExactlyInstanceOf(SyncException.class) + .hasNoCause(); + } +} From a834e7761369a49ede68f5f5fae6abdb9ac344fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahmet=20=C3=96z?= Date: Tue, 17 Nov 2020 17:24:50 +0100 Subject: [PATCH 05/12] Concept of the shopping lists' line item update actions. (#614) * introduce adr Co-authored-by: King-Hin Leung <35692276+leungkinghin@users.noreply.github.com> Co-authored-by: Lam Tran --- .adr-dir | 1 + .../adr/0001-record-architecture-decisions.md | 19 + ...ineitem-and-textlineitem-update-actions.md | 687 ++++++++++++++++++ 3 files changed, 707 insertions(+) create mode 100644 .adr-dir create mode 100644 docs/adr/0001-record-architecture-decisions.md create mode 100644 docs/adr/0002-shopping-lists-lineitem-and-textlineitem-update-actions.md diff --git a/.adr-dir b/.adr-dir new file mode 100644 index 0000000000..c73b64aed2 --- /dev/null +++ b/.adr-dir @@ -0,0 +1 @@ +docs/adr diff --git a/docs/adr/0001-record-architecture-decisions.md b/docs/adr/0001-record-architecture-decisions.md new file mode 100644 index 0000000000..9d0acfd52c --- /dev/null +++ b/docs/adr/0001-record-architecture-decisions.md @@ -0,0 +1,19 @@ +# 1. Record architecture decisions + +Date: 2020-11-04 + +## Status + +Accepted + +## Context + +We need to record the architectural decisions made on this project. + +## Decision + +We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). + +## Consequences + +See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). diff --git a/docs/adr/0002-shopping-lists-lineitem-and-textlineitem-update-actions.md b/docs/adr/0002-shopping-lists-lineitem-and-textlineitem-update-actions.md new file mode 100644 index 0000000000..efa77efd3c --- /dev/null +++ b/docs/adr/0002-shopping-lists-lineitem-and-textlineitem-update-actions.md @@ -0,0 +1,687 @@ +# 2. LineItem and TextLineItem update actions of the ShoppingLists. + +Date: 2020-11-04 + +## Status + +[Approved](https://github.com/commercetools/commercetools-sync-java/pull/614) + +## Context + + + +In a commerce application, a shopping list is a personal wishlist of a customer, (i.e. ingredients for a recipe, birthday wishes). +Shopping lists hold line items of products in the platform or any other items that can be described as text line items. + +We have challenges to build update actions of the `LineItem` and `TextLineItem` because of the nature of the synchronization, +so in this document, we will describe the reasons and constraints, mostly related to order of the items: +- LineItem orders might be important, if the customer has a front end that sorts the line items with their order could mean sorting by importance. + +## LineItems + +### How to ensure line item order? + + + + + + + + + + + + + + + + + + + + + + +
LineItemDraftsLineItems
+{
+  "lineItems": [
+    {
+      "sku": "SKU-1",
+      "quantity": 1,
+      "addedAt": "2020-11-04T09:38:35.571Z",
+      "custom": {
+        "type": {
+          "key": "custom-type-for-shoppinglists"
+        },
+        "fields": {
+          "textField": "text-1"
+        }
+      }
+    },
+    {
+      "sku": "SKU-2",
+      "quantity": 2,
+      "addedAt": "2020-11-04T09:40:12.341Z",
+      "custom": {
+        "type": {
+          "key": "custom-type-for-shoppinglists"
+        },
+        "fields": {
+          "textField": "text-2"
+        }
+      }
+    }
+  ]
+}
+
+
+{
+  "lineItems": [
+    {
+      "id": "24de3821-e27d-4ddb-bd0b-ecc99365285f",
+      "variant": {
+        "sku": "SKU-2"
+      },
+      "quantity": 2,
+      "custom": {
+        "type": {
+          "id": "4796e155-f5a4-403a-ae1a-04b10c9dfc54",
+          "obj": {
+            "key": "custom-type-for-shoppinglists"
+          }
+        },
+        "fields": {
+          "textField": "text-2"
+        }
+      },
+     "addedAt": "2020-11-04T09:38:35.571Z"
+    }
+  ]
+}
+
+
Analysis
+

Draft has line items with SKU-1 and SKU-2. In the target project line item with + SKU-2 exists, so SKU-1 is a new line item.

+

So we need to create an AddLineItem action + and a Change LineItems Order + of the line items SKU-1 and SKU-2, because when we add line item with SKU-1 + the order will be SKU-2 and SKU-1.

+

The challenge in here is, those actions can not be added in one request because we don't know the + line item id of the new line item + with SKU-1, so we need to find another way to create a new line item with the right order.

+
Proposed solution
+

+ Normally, for a difference, we might do a set intersection and then calculate action for differences, + but that does not make sense because we are not aware of the order from the draft. + So in this case the one request could be created but we might need to remove line item with SKU-2 + and line items in the draft with the given order with the line items SKU-1 and SKU-2. +

+
+{
+  "version": 1,
+  "actions": [
+    {
+      "action": "removeLineItem",
+      "lineItemId": "24de3821-e27d-4ddb-bd0b-ecc99365285f"
+    },
+    {
+      "action": "addLineItem",
+      "sku": "SKU-1",
+      "quantity": 1,
+      "addedAt": "2020-11-04T09:38:35.571Z",
+      "custom": {
+        "type": {
+          "key": "custom-type-for-shoppinglists"
+        },
+        "fields": {
+          "textField": "text-1"
+        }
+      }
+    },
+    {
+      "action": "addLineItem",
+      "sku": "SKU-2",
+      "quantity": 2,
+      "addedAt": "2020-11-04T09:40:12.341Z",
+      "custom": {
+        "type": {
+          "key": "custom-type-for-shoppinglists"
+        },
+        "fields": {
+          "textField": "text-2"
+        }
+      }
+    }
+  ]
+}
+
+
+ +### Do we need to remove all line items when the order changes ? + + + + + + + + + + + + + + + + + + + + + + +
LineItemDraftsLineItems
+{
+  "lineItems": [
+    {
+      "sku": "SKU-1",
+      "quantity": 1,
+      "addedAt": "2020-11-04T09:38:35.571Z",
+      "custom": {
+        "type": {
+          "key": "custom-type-for-shoppinglists"
+        },
+        "fields": {
+          "textField": "text-1"
+        }
+      }
+    },
+    {
+      "sku": "SKU-3",
+      "quantity": 3,
+      "addedAt": "2020-11-05T10:00:10.101Z",
+      "custom": {
+        "type": {
+          "key": "custom-type-for-shoppinglists"
+        },
+        "fields": {
+          "textField": "text-3"
+        }
+      }
+    },
+    {
+      "sku": "SKU-2",
+      "quantity": 2,
+      "addedAt": "2020-11-04T09:40:12.341Z",
+      "custom": {
+        "type": {
+          "key": "custom-type-for-shoppinglists"
+        },
+        "fields": {
+          "textField": "text-2"
+        }
+      }
+    }        
+  ]
+}
+
+
+{
+  "lineItems": [
+    {
+      "id": "1c38d582-2e65-43f8-85db-4d34e6cff57a",
+      "variant": {
+        "sku": "SKU-1"
+      },
+      "quantity": 1,
+      "custom": {
+        "type": {
+          "id": "4796e155-f5a4-403a-ae1a-04b10c9dfc54",
+          "obj": {
+            "key": "custom-type-for-shoppinglists"
+          }
+        },
+        "fields": {
+          "textField": "text-1"
+        }
+      },
+     "addedAt": "2020-11-04T09:38:35.571Z"
+    },
+    {
+      "id": "24de3821-e27d-4ddb-bd0b-ecc99365285f",
+      "variant": {
+        "sku": "SKU-2"
+      },
+      "quantity": 2,
+      "custom": {
+        "type": {
+          "id": "4796e155-f5a4-403a-ae1a-04b10c9dfc54",
+          "obj": {
+            "key": "custom-type-for-shoppinglists"
+          }
+        },
+        "fields": {
+          "textField": "text-2"
+        }
+      },
+     "addedAt": "2020-11-04T09:40:12.341Z"
+    }
+  ]
+}
+
+
Analysis
+

Draft has line items with SKU-1, SKU-3 and SKU-2 also in target project line item with + SKU-1 exists in the same order, SKU-3 is a new line item, and SKU-2 needs to be in last order.

+

So we need to create an AddLineItem action and + a Change LineItems Order + of the line items SKU-2 and SKU-3, because when we add line item with SKU-3 + the order will be SKU-1, SKU-2 and SKU-3.

+

The challenge in here is, those actions can not be added in one request because we don't know the + line item id of the new line item + with SKU-3, so we need to find another way to create a new line item with the right order.

+

Also another challenge in here is about the line item with SKU-1, as the order and data is + exactly same, we need to find a better way to avoid creating unnecessary actions. +

+
Proposed solution
+

+ The solution idea about the new line item and changed order is still same like in the case-1. Do we + need to remove and add line item with SKU-1? No, it is not needed and we could start the removing + and adding from the first order change. +

+
+{
+  "version": 1,
+  "actions": [
+    {
+      "action": "removeLineItem",
+      "lineItemId": "24de3821-e27d-4ddb-bd0b-ecc99365285f"
+    },
+    {
+      "action": "addLineItem",
+      "sku": "SKU-3",
+      "quantity": 3,
+      "addedAt": "2020-11-05T10:00:10.101Z",
+      "custom": {
+        "type": {
+          "key": "custom-type-for-shoppinglists"
+        },
+        "fields": {
+          "textField": "text-3"
+        }
+      }
+    },
+    {
+      "action": "addLineItem",
+      "sku": "SKU-2",
+      "quantity": 2,
+      "addedAt": "2020-11-04T09:40:12.341Z",
+      "custom": {
+        "type": {
+          "key": "custom-type-for-shoppinglists"
+        },
+        "fields": {
+          "textField": "text-2"
+        }
+      }
+    }
+  ]
+}
+
+
+ +### Do we need to remove and add all line items when no new line item is added or removed, just order is different ? + + + + + + + + + + + + + + + + + + + + + + +
LineItemDraftsLineItems
+{
+  "lineItems": [
+    {
+      "sku": "SKU-2",
+      "quantity": 2,
+      "addedAt": "2020-11-05T10:00:10.101Z",
+      "custom": {
+        "type": {
+          "key": "custom-type-for-shoppinglists"
+        },
+        "fields": {
+          "textField": "text-2"
+        }
+      }
+    },
+    {
+      "sku": "SKU-1",
+      "quantity": 1,
+      "addedAt": "2020-11-04T09:38:35.571Z",
+      "custom": {
+        "type": {
+          "key": "custom-type-for-shoppinglists"
+        },
+        "fields": {
+          "textField": "text-1"
+        }
+      }
+    }
+  ]
+}
+
+
+{
+  "lineItems": [
+    {
+      "id": "24de3821-e27d-4ddb-bd0b-ecc99365285f",
+      "variant": {
+        "sku": "SKU-1"
+      },
+      "quantity": 1,
+      "custom": {
+        "type": {
+          "id": "4796e155-f5a4-403a-ae1a-04b10c9dfc54",
+          "obj": {
+            "key": "custom-type-for-shoppinglists"
+          }
+        },
+        "fields": {
+          "textField": "text-1"
+        }
+      },
+     "addedAt": "2020-11-04T09:38:35.571Z"
+    },
+    {
+      "id": "24de3821-e27d-4ddb-bd0b-ecc99365285f",
+      "variant": {
+        "sku": "SKU-2"
+      },
+      "quantity": 2,
+      "custom": {
+        "type": {
+          "id": "4796e155-f5a4-403a-ae1a-04b10c9dfc54",
+          "obj": {
+            "key": "custom-type-for-shoppinglists"
+          }
+        },
+        "fields": {
+          "textField": "text-2"
+        }
+      },
+     "addedAt": "2020-11-04T09:40:12.341Z"
+    }
+  ]
+}
+
+
Analysis
+

The Draft has line items with SKU-2 and SKU-1 also in target project line item + with SKU-2 and SKU-1 exists but in a different order.

+

So we need + a Change + LineItems Order + of the line items with order SKU-2 and SKU-1.

+

The challenge here is about the line item order and no new line item is added or removed, + just order is different, so we need to find a better way to avoid creating unnecessary actions like + removing and adding back, is this possible ? +

+
Proposed solution
+

+ The solution idea for the changing order with removing and adding back looks like an overhead. We know + all line item ids, so change order action could be created. However, the challenge is + finding an algorithm to compare and find the line item ids, and then prepare an order. +

+

+ The example above seems reasonable but how you would sync a case like: + [SKU-1, SKU-2, SKU-3] to [SKU-3, SKU-1, SKU-4, SKU-2], so with a different algorithm it might + be done with change order [line-item-id-3, line-item-id-1, line-item-id-2] then removeLineItem + SKU-2, add back addLineItem SKU-4, in total 3 actions. Even for this we need to remove and add back. +

+

+ It looks like there are more different cases, when we dig in. That's why we decided to keep the idea + of removing and adding back to not have a more complex algorithm. +

+
+ +## TextLineItems + +### How to ensure text line item order? + + + + + + + + + + + + + + + + + + + + + + +
TextLineItemDraftsTextLineItems
+{
+  "textLineItems": [
+    {
+     "name": {
+        "de": "name1-DE",
+        "en": "name1-EN"
+      },
+      "description": {
+        "de": "desc1-DE",
+        "en": "desc1-EN"
+      },
+      "quantity": 1,
+      "addedAt": "2020-11-04T09:38:35.571Z",
+      "custom": {
+        "type": {
+          "key": "custom-type-for-shoppinglists"
+        },
+        "fields": {
+          "textField": "text-1"
+        }
+      }
+    },
+    {
+      "name": {
+        "de": "name2-DE",
+        "en": "name2-EN"
+      },
+      "description": {
+        "de": "desc2-DE",
+        "en": "desc2-EN"
+      },
+      "quantity": 2,
+      "addedAt": "2020-11-04T09:40:12.341Z",
+      "custom": {
+        "type": {
+          "key": "custom-type-for-shoppinglists"
+        },
+        "fields": {
+          "textField": "text-2"
+        }
+      }
+    }
+  ]
+}
+
+
+{
+  "textLineItems": [
+    {
+      "id": "24de3821-e27d-4ddb-bd0b-ecc99365285f",
+      "name": {
+        "de": "name2-DE",
+        "en": "name2-EN"
+      },
+      "description": {
+        "de": "desc2-DE",
+        "en": "desc2-EN"
+      },
+      "quantity": 2,
+      "custom": {
+        "type": {
+          "id": "4796e155-f5a4-403a-ae1a-04b10c9dfc54",
+          "obj": {
+            "key": "custom-type-for-shoppinglists"
+          }
+        },
+        "fields": {
+          "textField": "text-2"
+        }
+      },
+     "addedAt": "2020-11-04T09:38:35.571Z"
+    }
+  ]
+}
+
+
Analysis
+

Draft has text line items with name-1 and name-2. In the target project text line item with + name-2 exists, so name-1 is a new text line item.

+

So we need to create an AddTextLineItem + action + and a Change + TextLineItems Order + of the text line items name-1 and name-2, because when we add text line item with name-1 + the order will be name-2 and name-1.

+

The challenge in here is, those actions cannot be added in one request because we don't know the + text line item id of the new text line item + with name-1. We need to find another way to create a new text line item with the right order. +

+
Proposed solution
+

+ Normally, for a difference, we do a set intersection and then calculate action for differences, + but that does not make sense because we are not aware of the order from the draft. +

+

+ Before that, we need to analyse the AddTextLineItem + action, because the platform is not checking if the data exist. An API user could add the + exact same data multiple times. So it's impossible to know the order by + just checking the differences between the resource and draft object. Also, the name of the text line item + does not need to be unique as line item does. Each line item is identified by its product variant and + custom fields. Luckily the platform supports changing all field (except addedAt) of the text line + items, so when an order change is needed we update the + fields of the text line items. Which will look like: +

+
+{
+  "version": 1,
+  "actions": [
+    {
+      "action" : "changeTextLineItemName",
+      "textLineItemId" : "24de3821-e27d-4ddb-bd0b-ecc99365285f",
+      "name": {
+        "de": "name1-DE",
+        "en": "name1-EN"
+      }
+    },
+    {
+      "action" : "changeTextLineItemQuantity",
+      "textLineItemId" : "24de3821-e27d-4ddb-bd0b-ecc99365285f",
+      "quantity" : 1
+    },
+    {
+      "action" : "setTextLineItemDescription",
+      "textLineItemId" : "24de3821-e27d-4ddb-bd0b-ecc99365285f",
+      "description": {
+        "de": "desc1-DE",
+        "en": "desc1-EN"
+      }
+    },
+    {
+      "action" : "setTextLineItemCustomField",
+      "textLineItemId" : "24de3821-e27d-4ddb-bd0b-ecc99365285f",
+      "name" : "textField",
+      "value" : "text-1"
+    },
+    {
+      "action" : "addTextLineItem",
+      "name": {
+        "de": "name2-DE",
+        "en": "name2-EN"
+      },
+      "description": {
+        "de": "desc2-DE",
+        "en": "desc2-EN"
+      },
+      "quantity": 2,
+      "addedAt": "2020-11-04T09:38:35.571Z",
+      "custom": {
+        "type": {
+          "key": "custom-type-for-shoppinglists"
+        },
+        "fields": {
+          "textField": "text-2"
+        }
+      }
+    }
+  ]
+}
+
+
+ +## Common + +### How addedAt will be compared? + +In commercetools shopping lists API, there is no [update action](https://docs.commercetools.com/api/projects/shoppingLists#update-actions) +to change the `addedAt` field of the `LineItem` and `TextLineItem`, also in API it has a default value described as `Defaults to the current date and time.`, +when it's not set in the draft, so how to compare and update this field? + +**Proposed solution:** + +The `addedAt` field will be synced only if the value provided in the line item draft, otherwise, the `addedAt` value will be omitted. +To be able to sync it we need to remove and add this line item back with the up-to-date value. +After some discussions in pull requests, we decided to not change this field. + +## Decision + + + +- In commercetools API, the product variant to be selected in the LineItemDraft can be specified either by its product ID plus variant ID or by its SKU. +For the sync library, product variant will be matched by its SKU, if the SKU not set for a LineItemDraft, the draft will not be synced and an error callback will be triggered. +Check [LineItemDraft Product Variant Selection](https://docs.commercetools.com/api/projects/shoppingLists#lineitemdraft-product-variant-selection) for more details. + +- When a [Change LineItems Order](https://docs.commercetools.com/api/projects/shoppingLists#change-lineitems-order) action is needed, +the line items will be removed and added back with the order provided in the `ShoppingListDraft`. + +- When a [Change TextLineItems Order](https://docs.commercetools.com/api/projects/shoppingLists#change-textlineitems-order) action is needed, +the text line items will be updated with using update actions with the order provided in the `ShoppingListDraft`. + +- In commercetools shopping lists API, there is no [update action](https://docs.commercetools.com/api/projects/shoppingLists#update-actions) +to change the `addedAt` field of the `LineItem` and `TextLineItem`, hereby we will not update the `addedAt` value. + +## Consequences + + + +- To ensure the order of the line items, we need to remove and add line items. That means a bigger payload and a performance overhead. + +- To ensure the order of text line items, we need to calculate and update more than expected. That means a bigger payload and a performance overhead. + +- **Caveat**: `addedAt` values not synced. From 44643e73da9bcf912fb004618a1d28ed173b5229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahmet=20=C3=96z?= Date: Tue, 17 Nov 2020 17:27:48 +0100 Subject: [PATCH 06/12] Implement LineItem actions (#618) --- .../updateactions/AddLineItemWithSku.java | 58 ++ .../helpers/ShoppingListBatchValidator.java | 21 +- .../utils/LineItemCustomActionBuilder.java | 46 ++ .../utils/LineItemUpdateActionUtils.java | 259 ++++++++ .../ShoppingListReferenceResolutionUtils.java | 48 +- .../utils/ShoppingListSyncUtils.java | 36 +- ...oppingListCustomUpdateActionUtilsTest.java | 59 +- .../shoppinglists/ShoppingListSyncTest.java | 20 +- .../LineItemListUpdateActionUtilsTest.java | 559 ++++++++++++++++++ .../utils/LineItemUpdateActionUtilsTest.java | 445 ++++++++++++++ .../utils/ShoppingListSyncUtilsTest.java | 2 +- ...ppinglist-with-lineitems-not-expanded.json | 157 +++++ .../shoppinglist-with-lineitems-sku-12.json | 111 ++++ ...t-with-lineitems-sku-123-with-changes.json | 162 +++++ ...eitems-sku-123-with-different-addedAt.json | 183 ++++++ ...ith-lineitems-sku-123-without-addedAt.json | 180 ++++++ .../shoppinglist-with-lineitems-sku-123.json | 183 ++++++ .../shoppinglist-with-lineitems-sku-1234.json | 213 +++++++ .../shoppinglist-with-lineitems-sku-124.json | 162 +++++ ...t-with-lineitems-sku-132-with-changes.json | 183 ++++++ .../shoppinglist-with-lineitems-sku-1324.json | 213 +++++++ .../shoppinglist-with-lineitems-sku-1423.json | 213 +++++++ .../shoppinglist-with-lineitems-sku-312.json | 162 +++++ .../shoppinglist-with-lineitems-sku-32.json | 111 ++++ .../shoppinglist-with-lineitems-sku-324.json | 162 +++++ 25 files changed, 3894 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/commands/updateactions/AddLineItemWithSku.java create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/utils/LineItemCustomActionBuilder.java create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/utils/LineItemUpdateActionUtils.java create mode 100644 src/test/java/com/commercetools/sync/shoppinglists/utils/LineItemListUpdateActionUtilsTest.java create mode 100644 src/test/java/com/commercetools/sync/shoppinglists/utils/LineItemUpdateActionUtilsTest.java create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-not-expanded.json create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-12.json create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-with-changes.json create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-with-different-addedAt.json create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-without-addedAt.json create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123.json create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-1234.json create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-124.json create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-132-with-changes.json create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-1324.json create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-1423.json create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-312.json create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-32.json create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-324.json diff --git a/src/main/java/com/commercetools/sync/shoppinglists/commands/updateactions/AddLineItemWithSku.java b/src/main/java/com/commercetools/sync/shoppinglists/commands/updateactions/AddLineItemWithSku.java new file mode 100644 index 0000000000..5554054672 --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/commands/updateactions/AddLineItemWithSku.java @@ -0,0 +1,58 @@ +package com.commercetools.sync.shoppinglists.commands.updateactions; + +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.commands.UpdateActionImpl; +import io.sphere.sdk.shoppinglists.LineItemDraft; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.types.CustomFieldsDraft; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.time.ZonedDateTime; + +/** + * TODO (JVM-SDK): https://github.com/commercetools/commercetools-jvm-sdk/issues/2079 + * ShoppingList#AddLineItem action does not support product variant selection by SKU, + * so we needed to add this custom action as a workaround. + */ +public final class AddLineItemWithSku extends UpdateActionImpl { + + private final String sku; + private final Long quantity; + private final ZonedDateTime addedAt; + private final CustomFieldsDraft custom; + + private AddLineItemWithSku( + @Nullable final String sku, + @Nullable final Long quantity, + @Nullable final ZonedDateTime addedAt, + @Nullable final CustomFieldsDraft custom) { + + super("addLineItem"); + + this.sku = sku; + this.quantity = quantity; + this.addedAt = addedAt; + this.custom = custom; + } + + /** + * Creates an update action "addLineItem" which adds a line item to a shopping list. + * + * @param lineItemDraft Line item draft template to map update action's fields. + * @return an update action "addLineItem" which adds a line item to a shopping list. + */ + @Nonnull + public static UpdateAction of(@Nonnull final LineItemDraft lineItemDraft) { + + return new AddLineItemWithSku(lineItemDraft.getSku(), + lineItemDraft.getQuantity(), + lineItemDraft.getAddedAt(), + lineItemDraft.getCustom()); + } + + public String getSku() { + return sku; + } +} + diff --git a/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidator.java b/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidator.java index 09cd67426e..acc6c8a3b8 100644 --- a/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidator.java +++ b/src/main/java/com/commercetools/sync/shoppinglists/helpers/ShoppingListBatchValidator.java @@ -16,7 +16,6 @@ import java.util.Set; import static java.lang.String.format; -import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toSet; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -96,7 +95,7 @@ private boolean isValidShoppingListDraft( } else { final List draftErrors = getErrorsInAllLineItemsAndTextLineItems(shoppingListDraft); if (!draftErrors.isEmpty()) { - final String concatenatedErrors = draftErrors.stream().collect(joining(",")); + final String concatenatedErrors = String.join(",", draftErrors); this.handleError(concatenatedErrors); } else { return true; @@ -160,7 +159,7 @@ private List getTextLineItemDraftErrorsInAllTextLineItems( return errorMessages; } - private boolean isNullOrEmptyLocalizedString(@Nonnull final LocalizedString localizedString) { + private boolean isNullOrEmptyLocalizedString(@Nullable final LocalizedString localizedString) { return localizedString == null || localizedString.getLocales().isEmpty(); } @@ -186,11 +185,9 @@ private void collectReferencedKeysInLineItems( shoppingListDraft .getLineItems() - .stream() - .forEach(lineItemDraft -> { - collectReferencedKeyFromCustomFieldsDraft(lineItemDraft.getCustom(), - referencedKeys.typeKeys::add); - }); + .forEach(lineItemDraft -> + collectReferencedKeyFromCustomFieldsDraft( + lineItemDraft.getCustom(), referencedKeys.typeKeys::add)); } private void collectReferencedKeysInTextLineItems( @@ -203,11 +200,9 @@ private void collectReferencedKeysInTextLineItems( shoppingListDraft .getTextLineItems() - .stream() - .forEach(textLineItemDraft -> { - collectReferencedKeyFromCustomFieldsDraft(textLineItemDraft.getCustom(), - referencedKeys.typeKeys::add); - }); + .forEach(textLineItemDraft -> + collectReferencedKeyFromCustomFieldsDraft( + textLineItemDraft.getCustom(), referencedKeys.typeKeys::add)); } public static class ReferencedKeys { diff --git a/src/main/java/com/commercetools/sync/shoppinglists/utils/LineItemCustomActionBuilder.java b/src/main/java/com/commercetools/sync/shoppinglists/utils/LineItemCustomActionBuilder.java new file mode 100644 index 0000000000..c0c3ca5345 --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/utils/LineItemCustomActionBuilder.java @@ -0,0 +1,46 @@ +package com.commercetools.sync.shoppinglists.utils; + +import com.commercetools.sync.commons.helpers.GenericCustomActionBuilder; +import com.fasterxml.jackson.databind.JsonNode; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetLineItemCustomField; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetLineItemCustomType; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Map; + +public final class LineItemCustomActionBuilder implements GenericCustomActionBuilder { + + @Nonnull + @Override + public UpdateAction buildRemoveCustomTypeAction( + @Nullable final Integer variantId, + @Nullable final String lineItemId) { + + return SetLineItemCustomType.ofRemoveType(lineItemId); + } + + @Nonnull + @Override + public UpdateAction buildSetCustomTypeAction( + @Nullable final Integer variantId, + @Nullable final String lineItemId, + @Nonnull final String customTypeId, + @Nullable final Map customFieldsJsonMap) { + + return SetLineItemCustomType.ofTypeIdAndJson(customTypeId, customFieldsJsonMap, lineItemId); + } + + @Nonnull + @Override + public UpdateAction buildSetCustomFieldAction( + @Nullable final Integer variantId, + @Nullable final String lineItemId, + @Nullable final String customFieldName, + @Nullable final JsonNode customFieldValue) { + + return SetLineItemCustomField.ofJson(customFieldName, customFieldValue, lineItemId); + } +} diff --git a/src/main/java/com/commercetools/sync/shoppinglists/utils/LineItemUpdateActionUtils.java b/src/main/java/com/commercetools/sync/shoppinglists/utils/LineItemUpdateActionUtils.java new file mode 100644 index 0000000000..eddf6830b0 --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/utils/LineItemUpdateActionUtils.java @@ -0,0 +1,259 @@ +package com.commercetools.sync.shoppinglists.utils; + +import com.commercetools.sync.commons.utils.CustomUpdateActionUtils; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import com.commercetools.sync.shoppinglists.commands.updateactions.AddLineItemWithSku; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.shoppinglists.LineItem; +import io.sphere.sdk.shoppinglists.LineItemDraft; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeLineItemQuantity; +import io.sphere.sdk.shoppinglists.commands.updateactions.RemoveLineItem; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static com.commercetools.sync.commons.utils.CommonTypeUpdateActionUtils.buildUpdateAction; +import static com.commercetools.sync.commons.utils.OptionalUtils.filterEmptyOptionals; +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; + +public final class LineItemUpdateActionUtils { + + /** + * Compares a list of {@link LineItem}s with a list of {@link LineItemDraft}s. + * The method takes in functions for building the required update actions (AddLineItem, RemoveLineItem and + * 1-1 update actions on line items (e.g. changeLineItemQuantity, setLineItemCustomType, etc..). + * + *

If the list of new {@link LineItemDraft}s is {@code null}, then remove actions are built for every existing + * line item. + * + * @param oldShoppingList shopping list resource, whose line item should be updated. + * @param newShoppingList new shopping list draft, which contains the line item to update. + * @param syncOptions responsible for supplying the sync options to the sync utility method. It is used for + * triggering the error callback within the utility, in case of errors. + * @return a list of line item update actions on the resource of shopping lists, if the list of line items are not + * identical. Otherwise, if the line items are identical, an empty list is returned. + */ + @Nonnull + public static List> buildLineItemsUpdateActions( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingList, + @Nonnull final ShoppingListSyncOptions syncOptions) { + + final boolean hasOldLineItems = oldShoppingList.getLineItems() != null + && !oldShoppingList.getLineItems().isEmpty(); + final boolean hasNewLineItems = newShoppingList.getLineItems() != null + && !newShoppingList.getLineItems().isEmpty() + && newShoppingList.getLineItems().stream().anyMatch(Objects::nonNull); + + if (hasOldLineItems && !hasNewLineItems) { + + return oldShoppingList.getLineItems() + .stream() + .map(RemoveLineItem::of) + .collect(toList()); + + } else if (!hasOldLineItems) { + + if (!hasNewLineItems) { + return emptyList(); + } + + return newShoppingList.getLineItems() + .stream() + .filter(Objects::nonNull) + .map(AddLineItemWithSku::of) + .collect(toList()); + } + + final List oldLineItems = oldShoppingList.getLineItems(); + final List newlineItems = newShoppingList.getLineItems() + .stream() + .filter(Objects::nonNull) + .collect(toList()); + + return buildUpdateActions(oldShoppingList, newShoppingList, oldLineItems, newlineItems, syncOptions); + } + + + /** + * The decisions in the calculating update actions are documented on the + * `docs/adr/0002-shopping-lists-lineitem-and-textlineitem-update-actions.md` + */ + @Nonnull + private static List> buildUpdateActions( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingList, + @Nonnull final List oldLineItems, + @Nonnull final List newlineItems, + @Nonnull final ShoppingListSyncOptions syncOptions) { + + final List> updateActions = new ArrayList<>(); + + final int minSize = Math.min(oldLineItems.size(), newlineItems.size()); + int indexOfFirstDifference = minSize; + for (int i = 0; i < minSize; i++) { + + final LineItem oldLineItem = oldLineItems.get(i); + final LineItemDraft newLineItem = newlineItems.get(i); + + if (oldLineItem.getVariant() == null || StringUtils.isBlank(oldLineItem.getVariant().getSku())) { + + throw new IllegalArgumentException( + format("LineItem at position '%d' of the ShoppingList with key '%s' has no SKU set. " + + "Please make sure all line items have SKUs", i, oldShoppingList.getKey())); + + } else if (StringUtils.isBlank(newLineItem.getSku())) { + + throw new IllegalArgumentException( + format("LineItemDraft at position '%d' of the ShoppingListDraft with key '%s' has no SKU set. " + + "Please make sure all line items have SKUs", i, newShoppingList.getKey())); + + } + + if (oldLineItem.getVariant().getSku().equals(newLineItem.getSku()) + && hasIdenticalAddedAtValues(oldLineItem, newLineItem)) { + + updateActions.addAll(buildLineItemUpdateActions( + oldShoppingList, newShoppingList, oldLineItem, newLineItem, syncOptions)); + } else { + // different sku or addedAt means the order is different. + // To be able to ensure the order, we need to remove and add this line item back + // with the up to date values. + indexOfFirstDifference = i; + break; + } + } + + // for example: + // old: li-1, li-2 + // new: li-1, li-3, li-2 + // indexOfFirstDifference: 1 (li-2 vs li-3) + // expected: remove from old li-2, add from draft li-3, li-2 starting from the index. + for (int i = indexOfFirstDifference; i < oldLineItems.size(); i++) { + updateActions.add(RemoveLineItem.of(oldLineItems.get(i).getId())); + } + + for (int i = indexOfFirstDifference; i < newlineItems.size(); i++) { + updateActions.add(AddLineItemWithSku.of(newlineItems.get(i))); + } + + return updateActions; + } + + private static boolean hasIdenticalAddedAtValues( + @Nonnull final LineItem oldLineItem, + @Nonnull final LineItemDraft newLineItem) { + + if (newLineItem.getAddedAt() == null) { + return true; // omit, if not set in draft. + } + + return oldLineItem.getAddedAt().equals(newLineItem.getAddedAt()); + } + + /** + * Compares all the fields of a {@link LineItem} and a {@link LineItemDraft} and returns a list of + * {@link UpdateAction}<{@link ShoppingList}> as a result. If both the {@link LineItem} and + * the {@link LineItemDraft} have identical fields, then no update action is needed and hence an empty {@link List} + * is returned. + * + * @param oldShoppingList shopping list resource, whose line item should be updated. + * @param newShoppingList new shopping list draft, which contains the line item to update. + * @param oldLineItem the line item which should be updated. + * @param newLineItem the line item draft where we get the new fields (i.e. quantity, custom fields and types). + * @param syncOptions responsible for supplying the sync options to the sync utility method. It is used for + * triggering the error callback within the utility, in case of errors. + * @return A list with the update actions or an empty list if the line item fields are identical. + */ + @Nonnull + public static List> buildLineItemUpdateActions( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingList, + @Nonnull final LineItem oldLineItem, + @Nonnull final LineItemDraft newLineItem, + @Nonnull final ShoppingListSyncOptions syncOptions) { + + final List> updateActions = filterEmptyOptionals( + buildChangeLineItemQuantityUpdateAction(oldLineItem, newLineItem) + ); + + updateActions.addAll( + buildLineItemCustomUpdateActions(oldShoppingList, newShoppingList, oldLineItem, newLineItem, syncOptions)); + + return updateActions; + } + + /** + * Compares the {@code quantity} values of a {@link LineItem} and a {@link LineItemDraft} + * and returns an {@link Optional} of update action, which would contain the {@code "changeLineItemQuantity"} + * {@link UpdateAction}. If both {@link LineItem} and {@link LineItemDraft} have the same + * {@code quantity} values, then no update action is needed and empty optional will be returned. + * + *

Note: If {@code quantity} from the {@code newLineItem} is {@code null}, the new {@code quantity} + * will be set to default value {@code 1L}. If {@code quantity} from the {@code newLineItem} is {@code 0}, then it + * means removing the line item. + * + * @param oldLineItem the line item which should be updated. + * @param newLineItem the line item draft where we get the new quantity. + * @return A filled optional with the update action or an empty optional if the quantities are identical. + */ + @Nonnull + public static Optional> buildChangeLineItemQuantityUpdateAction( + @Nonnull final LineItem oldLineItem, + @Nonnull final LineItemDraft newLineItem) { + + final Long newLineItemQuantity = newLineItem.getQuantity() == null + ? NumberUtils.LONG_ONE : newLineItem.getQuantity(); + + return buildUpdateAction(oldLineItem.getQuantity(), newLineItemQuantity, + () -> ChangeLineItemQuantity.of(oldLineItem.getId(), newLineItemQuantity)); + } + + /** + * Compares the custom fields and custom types of a {@link LineItem} and a {@link LineItemDraft} and returns a + * list of {@link UpdateAction}<{@link ShoppingList}> as a result. If both the {@link LineItem} and the + * {@link LineItemDraft} have identical custom fields and types, then no update action is needed and hence an empty + * {@link List} is returned. + * + * @param oldShoppingList shopping list resource, whose line item should be updated. + * @param newShoppingList new shopping list draft, which contains the line item to update. + * @param oldLineItem the line item which should be updated. + * @param newLineItem the line item draft where we get the new custom fields and types. + * @param syncOptions responsible for supplying the sync options to the sync utility method. It is used for + * triggering the error callback within the utility, in case of errors. + * @return A list with the custom field/type update actions or an empty list if the custom fields/types are + * identical. + */ + @Nonnull + public static List> buildLineItemCustomUpdateActions( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingList, + @Nonnull final LineItem oldLineItem, + @Nonnull final LineItemDraft newLineItem, + @Nonnull final ShoppingListSyncOptions syncOptions) { + + return CustomUpdateActionUtils.buildCustomUpdateActions( + oldShoppingList, + newShoppingList, + oldLineItem::getCustom, + newLineItem::getCustom, + new LineItemCustomActionBuilder(), + null, // not used by util. + t -> oldLineItem.getId(), + lineItem -> LineItem.resourceTypeId(), + t -> oldLineItem.getId(), + syncOptions); + } + + private LineItemUpdateActionUtils() { + } +} diff --git a/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListReferenceResolutionUtils.java b/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListReferenceResolutionUtils.java index d2ae666c24..cd86f92035 100644 --- a/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListReferenceResolutionUtils.java +++ b/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListReferenceResolutionUtils.java @@ -34,7 +34,7 @@ public final class ShoppingListReferenceResolutionUtils { /** - * Returns an {@link List}<{@link ShoppingListDraft}> consisting of the results of applying the + * Returns a {@link List}<{@link ShoppingListDraft}> consisting of the results of applying the * mapping from {@link ShoppingList} to {@link ShoppingListDraft} with considering reference resolution. * * @@ -88,8 +88,52 @@ public static List mapToShoppingListDrafts( .collect(toList()); } + /** + * Returns a @link ShoppingListDraft} consisting of the result of applying the + * mapping from {@link ShoppingList} to {@link ShoppingListDraft} with considering reference resolution. + * + *
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Reference fieldfromto
customer{@link Reference}<{@link Customer}>{@link ResourceIdentifier}<{@link Customer}>
custom.type{@link Reference}<{@link Type}>{@link ResourceIdentifier}<{@link Type}>
lineItems.custom.type{@link Set}<{@link Reference}<{@link Type}>>{@link Set}<{@link ResourceIdentifier}<{@link Type}>>
textLineItems.custom.type{@link Set}<{@link Reference}<{@link Type}>>{@link Set}<{@link ResourceIdentifier}<{@link Type}>>
+ * + *

Note: The aforementioned references should be expanded with a key. + * Any reference that is not expanded will have its id in place and not replaced by the key will be + * considered as existing resources on the target commercetools project and + * the library will issues an update/create API request without reference resolution. + * + * @param shoppingList the shopping list with expanded references. + * @return a {@link ShoppingListDraft} built from the supplied {@link ShoppingList}. + */ @Nonnull - private static ShoppingListDraft mapToShoppingListDraft(@Nonnull final ShoppingList shoppingList) { + public static ShoppingListDraft mapToShoppingListDraft(@Nonnull final ShoppingList shoppingList) { return ShoppingListDraftBuilder .of(shoppingList.getName()) diff --git a/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtils.java b/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtils.java index f080a027f8..4182e2af7f 100644 --- a/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtils.java +++ b/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtils.java @@ -4,15 +4,13 @@ import io.sphere.sdk.commands.UpdateAction; import io.sphere.sdk.shoppinglists.ShoppingList; import io.sphere.sdk.shoppinglists.ShoppingListDraft; -import io.sphere.sdk.types.CustomDraft; -import io.sphere.sdk.types.CustomFieldsDraft; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import java.util.List; import static com.commercetools.sync.commons.utils.CustomUpdateActionUtils.buildPrimaryResourceCustomUpdateActions; import static com.commercetools.sync.commons.utils.OptionalUtils.filterEmptyOptionals; +import static com.commercetools.sync.shoppinglists.utils.LineItemUpdateActionUtils.buildLineItemsUpdateActions; import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildChangeNameUpdateAction; import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildSetAnonymousIdUpdateAction; import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildSetCustomerUpdateAction; @@ -54,38 +52,16 @@ public static List> buildActions( buildSetDeleteDaysAfterLastModificationUpdateAction(oldShoppingList, newShoppingList) ); - final List> shoppingListCustomUpdateActions = - buildPrimaryResourceCustomUpdateActions(oldShoppingList, - new CustomShoppingListDraft(newShoppingList), - shoppingListCustomActionBuilder, - syncOptions); + updateActions.addAll(buildPrimaryResourceCustomUpdateActions(oldShoppingList, + newShoppingList::getCustom, + shoppingListCustomActionBuilder, + syncOptions)); - updateActions.addAll(shoppingListCustomUpdateActions); + updateActions.addAll(buildLineItemsUpdateActions(oldShoppingList, newShoppingList, syncOptions)); return updateActions; } - /** - * The class is needed by `buildPrimaryResourceCustomUpdateActions` generic utility method, - * because required generic type `S` is based on the CustomDraft interface (S extends CustomDraft). - * - *

TODO (JVM-SDK): Missing the interface CustomDraft. - * See for more details: https://github.com/commercetools/commercetools-jvm-sdk/issues/2073 - */ - private static class CustomShoppingListDraft implements CustomDraft { - private final ShoppingListDraft shoppingListDraft; - - public CustomShoppingListDraft(@Nonnull final ShoppingListDraft shoppingListDraft) { - this.shoppingListDraft = shoppingListDraft; - } - - @Nullable - @Override - public CustomFieldsDraft getCustom() { - return shoppingListDraft.getCustom(); - } - } - private ShoppingListSyncUtils() { } } diff --git a/src/test/java/com/commercetools/sync/commons/utils/ShoppingListCustomUpdateActionUtilsTest.java b/src/test/java/com/commercetools/sync/commons/utils/ShoppingListCustomUpdateActionUtilsTest.java index 98b1bb8731..f49d5d197f 100644 --- a/src/test/java/com/commercetools/sync/commons/utils/ShoppingListCustomUpdateActionUtilsTest.java +++ b/src/test/java/com/commercetools/sync/commons/utils/ShoppingListCustomUpdateActionUtilsTest.java @@ -1,14 +1,18 @@ package com.commercetools.sync.commons.utils; import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; +import com.commercetools.sync.shoppinglists.utils.LineItemCustomActionBuilder; import com.commercetools.sync.shoppinglists.utils.ShoppingListCustomActionBuilder; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import io.sphere.sdk.client.SphereClient; import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.shoppinglists.LineItem; import io.sphere.sdk.shoppinglists.ShoppingList; import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomField; import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomType; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetLineItemCustomField; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetLineItemCustomType; import org.junit.jupiter.api.Test; import java.util.HashMap; @@ -19,11 +23,12 @@ import static java.util.Collections.emptyMap; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class ShoppingListCustomUpdateActionUtilsTest { @Test - void buildTypedSetCustomTypeUpdateAction_WithCustomerResource_ShouldBuildCustomerUpdateAction() { + void buildTypedSetCustomTypeUpdateAction_WithShoppingListResource_ShouldBuildShoppingListUpdateAction() { final String newCustomTypeId = UUID.randomUUID().toString(); final UpdateAction updateAction = @@ -51,9 +56,59 @@ void buildSetCustomFieldAction_WithShoppingListResource_ShouldBuildShoppingListU final String customFieldName = "name"; final UpdateAction updateAction = ShoppingListCustomActionBuilder.of() - .buildSetCustomFieldAction(null, null, customFieldName, customFieldValue); + .buildSetCustomFieldAction(null, + null, customFieldName, + customFieldValue); assertThat(updateAction).isInstanceOf(SetCustomField.class); assertThat((SetCustomField) updateAction).hasValues("setCustomField", customFieldName, customFieldValue); } + + @Test + void buildTypedSetLineItemCustomTypeUpdateAction_WithLineItemResource_ShouldBuildShoppingListUpdateAction() { + final LineItem lineItem = mock(LineItem.class); + when(lineItem.getId()).thenReturn("line_item_id"); + + final String newCustomTypeId = UUID.randomUUID().toString(); + + final UpdateAction updateAction = + GenericUpdateActionUtils.buildTypedSetCustomTypeUpdateAction(newCustomTypeId, new HashMap<>(), + lineItem::getCustom, + new LineItemCustomActionBuilder(), -1, t -> lineItem.getId(), + lineItemResource -> LineItem.resourceTypeId(), t -> lineItem.getId(), + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)).build()).orElse(null); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction).isInstanceOf(SetLineItemCustomType.class); + assertThat((SetLineItemCustomType) updateAction) + .hasValues("setLineItemCustomType", emptyMap(), ofId(newCustomTypeId)); + assertThat(((SetLineItemCustomType) updateAction).getLineItemId()).isEqualTo("line_item_id"); + } + + @Test + void buildRemoveLineItemCustomTypeAction_WithLineItemResource_ShouldBuildShoppingListUpdateAction() { + final UpdateAction updateAction = + new LineItemCustomActionBuilder().buildRemoveCustomTypeAction(-1, "line_item_id"); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction).isInstanceOf(SetLineItemCustomType.class); + assertThat((SetLineItemCustomType) updateAction) + .hasValues("setLineItemCustomType", null, ofId(null)); + assertThat(((SetLineItemCustomType) updateAction).getLineItemId()).isEqualTo("line_item_id"); + } + + @Test + void buildSetLineItemCustomFieldAction_WithLineItemResource_ShouldBuildShoppingListUpdateAction() { + final JsonNode customFieldValue = JsonNodeFactory.instance.textNode("foo"); + final String customFieldName = "name"; + + final UpdateAction updateAction = new LineItemCustomActionBuilder() + .buildSetCustomFieldAction(-1, "line_item_id", customFieldName, customFieldValue); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction).isInstanceOf(SetLineItemCustomField.class); + assertThat((SetLineItemCustomField) updateAction) + .hasValues("setLineItemCustomField", customFieldName, customFieldValue); + assertThat(((SetLineItemCustomField) updateAction).getLineItemId()).isEqualTo("line_item_id"); + } } diff --git a/src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncTest.java b/src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncTest.java index df5f2300be..02e76412d9 100644 --- a/src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncTest.java +++ b/src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncTest.java @@ -12,6 +12,7 @@ import io.sphere.sdk.models.LocalizedString; import io.sphere.sdk.models.ResourceIdentifier; import io.sphere.sdk.models.SphereException; +import io.sphere.sdk.products.ProductVariant; import io.sphere.sdk.shoppinglists.LineItem; import io.sphere.sdk.shoppinglists.LineItemDraft; import io.sphere.sdk.shoppinglists.LineItemDraftBuilder; @@ -22,14 +23,15 @@ import io.sphere.sdk.shoppinglists.TextLineItemDraft; import io.sphere.sdk.shoppinglists.TextLineItemDraftBuilder; import io.sphere.sdk.types.CustomFieldsDraft; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletionException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; import static com.commercetools.sync.shoppinglists.helpers.ShoppingListBatchValidator.SHOPPING_LIST_DRAFT_IS_NULL; import static com.commercetools.sync.shoppinglists.helpers.ShoppingListBatchValidator.SHOPPING_LIST_DRAFT_KEY_NOT_SET; @@ -358,21 +360,27 @@ void sync_WithOnlyDraftsToUpdate_ShouldOnlyCallBeforeUpdateCallback() { verify(spySyncOptions, never()).applyBeforeCreateCallback(shoppingListDraft); } - // TODO - Enable the test case when LineItem is available in UpdateActionUtils - @Disabled @Test void sync_WithUnchangedShoppingListDraftAndUpdatedLineItemDraft_ShouldIncrementUpdated() { // preparation final ShoppingListService mockShoppingListService = mock(ShoppingListService.class); final ShoppingList mockShoppingList = mock(ShoppingList.class); + final LineItem mockLineItem = mock(LineItem.class); when(mockShoppingList.getKey()).thenReturn("shoppingListKey"); when(mockShoppingList.getName()).thenReturn(LocalizedString.ofEnglish("shoppingListName")); - when(mockLineItem.getVariant().getSku()).thenReturn("dummy-sku"); + + final ProductVariant mockProductVariant = mock(ProductVariant.class); + when(mockProductVariant.getSku()).thenReturn("dummy-sku"); + when(mockLineItem.getVariant()).thenReturn(mockProductVariant); when(mockLineItem.getQuantity()).thenReturn(10L); + when(mockShoppingListService.fetchMatchingShoppingListsByKeys(anySet())) .thenReturn(completedFuture(singleton(mockShoppingList))); + when(mockShoppingListService.updateShoppingList(any(), anyList())) + .thenReturn(completedFuture(mockShoppingList)); + final ShoppingListSyncOptions spySyncOptions = spy(syncOptions); final ShoppingListSync shoppingListSync = new ShoppingListSync(spySyncOptions, mockShoppingListService, mock(CustomerService.class), mock(TypeService.class)); diff --git a/src/test/java/com/commercetools/sync/shoppinglists/utils/LineItemListUpdateActionUtilsTest.java b/src/test/java/com/commercetools/sync/shoppinglists/utils/LineItemListUpdateActionUtilsTest.java new file mode 100644 index 0000000000..468dc91e8a --- /dev/null +++ b/src/test/java/com/commercetools/sync/shoppinglists/utils/LineItemListUpdateActionUtilsTest.java @@ -0,0 +1,559 @@ +package com.commercetools.sync.shoppinglists.utils; + +import com.commercetools.sync.services.TypeService; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; +import com.commercetools.sync.shoppinglists.commands.updateactions.AddLineItemWithSku; +import com.commercetools.sync.shoppinglists.helpers.LineItemReferenceResolver; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.customers.Customer; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.models.Reference; +import io.sphere.sdk.shoppinglists.LineItemDraft; +import io.sphere.sdk.shoppinglists.LineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.ShoppingListDraftBuilder; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeLineItemQuantity; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeName; +import io.sphere.sdk.shoppinglists.commands.updateactions.RemoveLineItem; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetAnonymousId; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomField; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomer; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetDeleteDaysAfterLastModification; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetDescription; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetLineItemCustomField; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetSlug; +import io.sphere.sdk.types.CustomFieldsDraft; +import org.junit.jupiter.api.Test; + +import javax.annotation.Nonnull; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static com.commercetools.sync.commons.utils.CompletableFutureUtils.mapValuesToFutureOfCompletedValues; +import static com.commercetools.sync.shoppinglists.utils.LineItemUpdateActionUtils.buildLineItemsUpdateActions; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListSyncUtils.buildActions; +import static io.sphere.sdk.json.SphereJsonUtils.readObjectFromResource; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class LineItemListUpdateActionUtilsTest { + + private static final String RES_ROOT = "com/commercetools/sync/shoppinglists/utils/lineitems/"; + private static final String SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123 = + RES_ROOT + "shoppinglist-with-lineitems-sku-123.json"; + private static final String SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123_WITH_CHANGES = + RES_ROOT + "shoppinglist-with-lineitems-sku-123-with-changes.json"; + private static final String SHOPPING_LIST_WITH_LINE_ITEMS_SKU_12 = + RES_ROOT + "shoppinglist-with-lineitems-sku-12.json"; + private static final String SHOPPING_LIST_WITH_LINE_ITEMS_SKU_1234 = + RES_ROOT + "shoppinglist-with-lineitems-sku-1234.json"; + private static final String SHOPPING_LIST_WITH_LINE_ITEMS_SKU_124 = + RES_ROOT + "shoppinglist-with-lineitems-sku-124.json"; + private static final String SHOPPING_LIST_WITH_LINE_ITEMS_SKU_312 = + RES_ROOT + "shoppinglist-with-lineitems-sku-312.json"; + private static final String SHOPPING_LIST_WITH_LINE_ITEMS_SKU_32 = + RES_ROOT + "shoppinglist-with-lineitems-sku-32.json"; + private static final String SHOPPING_LIST_WITH_LINE_ITEMS_SKU_1324 = + RES_ROOT + "shoppinglist-with-lineitems-sku-1324.json"; + private static final String SHOPPING_LIST_WITH_LINE_ITEMS_SKU_1423 = + RES_ROOT + "shoppinglist-with-lineitems-sku-1423.json"; + private static final String SHOPPING_LIST_WITH_LINE_ITEMS_SKU_324 = + RES_ROOT + "shoppinglist-with-lineitems-sku-324.json"; + private static final String SHOPPING_LIST_WITH_LINE_ITEMS_SKU_132_WITH_CHANGES = + RES_ROOT + "shoppinglist-with-lineitems-sku-132-with-changes.json"; + private static final String SHOPPING_LIST_WITH_LINE_ITEMS_NOT_EXPANDED = + RES_ROOT + "shoppinglist-with-lineitems-not-expanded.json"; + private static final String SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123_WITH_DIFFERENT_ADDED_AT = + RES_ROOT + "shoppinglist-with-lineitems-sku-123-with-different-addedAt.json"; + private static final String SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123_WITHOUT_ADDED_AT = + RES_ROOT + "shoppinglist-with-lineitems-sku-123-without-addedAt.json"; + + private static final ShoppingListSyncOptions SYNC_OPTIONS = + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)).build(); + + private static final LineItemReferenceResolver lineItemReferenceResolver = + new LineItemReferenceResolver(SYNC_OPTIONS, getMockTypeService()); + + @Test + void buildLineItemsUpdateActions_WithoutNewAndWithNullOldLineItems_ShouldNotBuildActions() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getLineItems()).thenReturn(emptyList()); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("name")) + .build(); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildLineItemsUpdateActions_WithNullNewAndNullOldLineItems_ShouldNotBuildActions() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getLineItems()).thenReturn(null); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("name")) + .build(); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildLineItemsUpdateActions_WithNullOldAndEmptyNewLineItems_ShouldNotBuildActions() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getLineItems()).thenReturn(null); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("name")) + .lineItems(emptyList()) + .build(); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildLineItemsUpdateActions_WithNullOldAndNewLineItemsWithNullElement_ShouldNotBuildActions() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getLineItems()).thenReturn(null); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("name")) + .lineItems(singletonList(null)) + .build(); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildLineItemsUpdateActions_WithNullNewLineItemsAndExistingLineItems_ShouldBuild3RemoveActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("name")) + .build(); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + RemoveLineItem.of("line_item_id_1"), + RemoveLineItem.of("line_item_id_2"), + RemoveLineItem.of("line_item_id_3")); + } + + @Test + void buildLineItemsUpdateActions_WithNewLineItemsAndNoOldLineItems_ShouldBuild3AddActions() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getLineItems()).thenReturn(null); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(newShoppingList.getLineItems()).isNotNull(); + + assertThat(updateActions) + .hasSize(3) + .extracting("sku") + .asString() + .isEqualTo("[SKU-1, SKU-2, SKU-3]"); + + assertThat(updateActions).containsExactly( + AddLineItemWithSku.of(newShoppingList.getLineItems().get(0)), + AddLineItemWithSku.of(newShoppingList.getLineItems().get(1)), + AddLineItemWithSku.of(newShoppingList.getLineItems().get(2)) + ); + } + + @Test + void buildLineItemsUpdateActions_WithIdenticalLineItems_ShouldNotBuildUpdateActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildLineItemsUpdateActions_WithSameLineItemPositionButChangesWithin_ShouldBuildUpdateActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123_WITH_CHANGES); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + ChangeLineItemQuantity.of("line_item_id_1", 2L), + SetLineItemCustomField.ofJson("textField", + JsonNodeFactory.instance.textNode("newText1"), "line_item_id_1"), + + ChangeLineItemQuantity.of("line_item_id_2", 4L), + SetLineItemCustomField.ofJson("textField", + JsonNodeFactory.instance.textNode("newText2"), "line_item_id_2"), + + ChangeLineItemQuantity.of("line_item_id_3", 6L), + SetLineItemCustomField.ofJson("textField", + JsonNodeFactory.instance.textNode("newText3"), "line_item_id_3") + ); + } + + @Test + void buildLineItemsUpdateActions_WithOneMissingLineItem_ShouldBuildOneRemoveLineItemAction() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_12); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + RemoveLineItem.of("line_item_id_3") + ); + } + + @Test + void buildLineItemsUpdateActions_WithOneExtraLineItem_ShouldBuildAddLineItemAction() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_1234); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + final LineItemDraft expectedLineItemDraft = + LineItemDraftBuilder.ofSku("SKU-4", 4L) + .custom(CustomFieldsDraft.ofTypeIdAndJson("custom_type_id", + singletonMap("textField", + JsonNodeFactory.instance.textNode("text4")))) + .addedAt(ZonedDateTime.parse("2020-11-05T10:00:00.000Z")) + .build(); + + assertThat(updateActions).containsExactly( + AddLineItemWithSku.of(expectedLineItemDraft) + ); + } + + @Test + void buildLineItemsUpdateAction_WithOneLineItemSwitch_ShouldBuildRemoveAndAddLineItemActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_124); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + final LineItemDraft expectedLineItemDraft = + LineItemDraftBuilder.ofSku("SKU-4", 4L) + .custom(CustomFieldsDraft.ofTypeIdAndJson("custom_type_id", + singletonMap("textField", + JsonNodeFactory.instance.textNode("text4")))) + .addedAt(ZonedDateTime.parse("2020-11-05T10:00:00.000Z")) + .build(); + + assertThat(updateActions).containsExactly( + RemoveLineItem.of("line_item_id_3"), + AddLineItemWithSku.of(expectedLineItemDraft) + ); + } + + @Test + void buildLineItemsUpdateAction_WithDifferentOrder_ShouldBuildRemoveAndAddLineItemActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_312); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + RemoveLineItem.of("line_item_id_1"), + RemoveLineItem.of("line_item_id_2"), + RemoveLineItem.of("line_item_id_3"), + AddLineItemWithSku.of(newShoppingList.getLineItems().get(0)), // SKU-3 + AddLineItemWithSku.of(newShoppingList.getLineItems().get(1)), // SKU-1 + AddLineItemWithSku.of(newShoppingList.getLineItems().get(2)) // SKU-2 + ); + } + + @Test + void buildLineItemsUpdateAction_WithRemovedAndDifferentOrder_ShouldBuildRemoveAndAddLineItemActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_32); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + RemoveLineItem.of("line_item_id_1"), + RemoveLineItem.of("line_item_id_2"), + RemoveLineItem.of("line_item_id_3"), + AddLineItemWithSku.of(newShoppingList.getLineItems().get(0)), // SKU-3 + AddLineItemWithSku.of(newShoppingList.getLineItems().get(1)) // SKU-2 + ); + } + + @Test + void buildLineItemsUpdateAction_WithAddedAndDifferentOrder_ShouldBuildRemoveAndAddLineItemActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_1324); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + RemoveLineItem.of("line_item_id_2"), + RemoveLineItem.of("line_item_id_3"), + AddLineItemWithSku.of(newShoppingList.getLineItems().get(1)), // SKU-3 + AddLineItemWithSku.of(newShoppingList.getLineItems().get(2)), // SKU-2 + AddLineItemWithSku.of(newShoppingList.getLineItems().get(3)) // SKU-4 + ); + } + + @Test + void buildLineItemsUpdateAction_WithAddedLineItemInBetween_ShouldBuildRemoveAndAddLineItemActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_1423); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + RemoveLineItem.of("line_item_id_2"), + RemoveLineItem.of("line_item_id_3"), + AddLineItemWithSku.of(newShoppingList.getLineItems().get(1)), // SKU-4 + AddLineItemWithSku.of(newShoppingList.getLineItems().get(2)), // SKU-2 + AddLineItemWithSku.of(newShoppingList.getLineItems().get(3)) // SKU-3 + ); + } + + @Test + void buildLineItemsUpdateAction_WithAddedRemovedAndDifOrder_ShouldBuildRemoveAndAddLineItemActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_324); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + RemoveLineItem.of("line_item_id_1"), + RemoveLineItem.of("line_item_id_2"), + RemoveLineItem.of("line_item_id_3"), + AddLineItemWithSku.of(newShoppingList.getLineItems().get(0)), // SKU-3 + AddLineItemWithSku.of(newShoppingList.getLineItems().get(1)), // SKU-2 + AddLineItemWithSku.of(newShoppingList.getLineItems().get(2)) // SKU-4 + ); + } + + @Test + void buildLineItemsUpdateAction_WithAddedRemovedAndDifOrderAndChangedLineItem_ShouldBuildAllDiffLineItemActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_132_WITH_CHANGES); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + ChangeLineItemQuantity.of("line_item_id_1", 2L), + SetLineItemCustomField.ofJson("textField", + JsonNodeFactory.instance.textNode("newText1"), "line_item_id_1"), + RemoveLineItem.of("line_item_id_2"), + RemoveLineItem.of("line_item_id_3"), + AddLineItemWithSku.of(newShoppingList.getLineItems().get(1)), // SKU-3 + AddLineItemWithSku.of(newShoppingList.getLineItems().get(2)) // SKU-2 + ); + } + + @Test + void buildLineItemsUpdateAction_WithNotExpandedLineItem_ShouldThrowIllegalArgumentException() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_NOT_EXPANDED, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_132_WITH_CHANGES); + + assertThatThrownBy(() -> buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("LineItem at position '1' of the ShoppingList with key " + + "'shoppinglist-with-lineitems-not-expanded' has no SKU set. " + + "Please make sure all line items have SKUs"); + } + + @Test + void buildLineItemsUpdateAction_WithLineItemWithoutSku_ShouldThrowIllegalArgumentException() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_132_WITH_CHANGES, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_NOT_EXPANDED); + + assertThatThrownBy(() -> buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS)) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("LineItemDraft at position '1' of the ShoppingListDraft with key " + + "'shoppinglist-with-lineitems-not-expanded' has no SKU set. " + + "Please make sure all line items have SKUs"); + } + + @Test + void buildLineItemsUpdateActions_WithIdenticalLineItemDraftsWithUpdatedAddedAt_ShouldBuildRemoveAndAddActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = mapToShoppingListDraftWithResolvedLineItemReferences( + SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123_WITH_DIFFERENT_ADDED_AT); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + RemoveLineItem.of("line_item_id_1"), + RemoveLineItem.of("line_item_id_2"), + RemoveLineItem.of("line_item_id_3"), + AddLineItemWithSku.of(newShoppingList.getLineItems().get(0)), // SKU-1 + AddLineItemWithSku.of(newShoppingList.getLineItems().get(1)), // SKU-2 + AddLineItemWithSku.of(newShoppingList.getLineItems().get(2)) // SKU-3 + ); + } + + @Test + void buildLineItemsUpdateActions_WithIdenticalLineItemDraftsWithoutAddedAtValues_ShouldNotBuildActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = mapToShoppingListDraftWithResolvedLineItemReferences( + SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123_WITHOUT_ADDED_AT); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildActions_WithDifferentValuesWithLineItems_ShouldReturnActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_132_WITH_CHANGES); + + + final List> updateActions = + buildActions(oldShoppingList, newShoppingList, mock(ShoppingListSyncOptions.class)); + + assertThat(updateActions).containsExactly( + SetSlug.of(LocalizedString.ofEnglish("newSlug")), + ChangeName.of(LocalizedString.ofEnglish("newName")), + SetDescription.of(LocalizedString.ofEnglish("newDescription")), + SetCustomer.of(Reference.of(Customer.referenceTypeId(), "customer_id_2")), + SetAnonymousId.of("newAnonymousId"), + SetDeleteDaysAfterLastModification.of(45), + SetCustomField.ofJson("textField", JsonNodeFactory.instance.textNode("newTextValue")), + ChangeLineItemQuantity.of("line_item_id_1", 2L), + SetLineItemCustomField.ofJson("textField", + JsonNodeFactory.instance.textNode("newText1"), "line_item_id_1"), + + RemoveLineItem.of("line_item_id_2"), + RemoveLineItem.of("line_item_id_3"), + AddLineItemWithSku.of(LineItemDraftBuilder + .ofSku("SKU-3", 6L) + .custom(CustomFieldsDraft.ofTypeIdAndJson("custom_type_id", + singletonMap("textField", + JsonNodeFactory.instance.textNode("newText3")))) + .addedAt(ZonedDateTime.parse("2020-11-04T10:00:00.000Z")) + .build()), + AddLineItemWithSku.of(LineItemDraftBuilder + .ofSku("SKU-2", 4L) + .custom(CustomFieldsDraft.ofTypeIdAndJson("custom_type_id", + singletonMap("textField", + JsonNodeFactory.instance.textNode("newText2")))) + .addedAt(ZonedDateTime.parse("2020-11-03T10:00:00.000Z")) + .build()) + ); + } + + @Nonnull + private static ShoppingListDraft mapToShoppingListDraftWithResolvedLineItemReferences( + @Nonnull final String resourcePath) { + + final ShoppingListDraft template = + ShoppingListReferenceResolutionUtils.mapToShoppingListDraft( + readObjectFromResource(resourcePath, ShoppingList.class)); + + final ShoppingListDraftBuilder builder = ShoppingListDraftBuilder.of(template); + + mapValuesToFutureOfCompletedValues( + Objects.requireNonNull(builder.getLineItems()), lineItemReferenceResolver::resolveReferences, toList()) + .thenApply(builder::lineItems) + .join(); + + return builder.build(); + } + + @Nonnull + private static TypeService getMockTypeService() { + final TypeService typeService = mock(TypeService.class); + when(typeService.fetchCachedTypeId(anyString())) + .thenReturn(completedFuture(Optional.of("custom_type_id"))); + return typeService; + } +} diff --git a/src/test/java/com/commercetools/sync/shoppinglists/utils/LineItemUpdateActionUtilsTest.java b/src/test/java/com/commercetools/sync/shoppinglists/utils/LineItemUpdateActionUtilsTest.java new file mode 100644 index 0000000000..dd26aad8e5 --- /dev/null +++ b/src/test/java/com/commercetools/sync/shoppinglists/utils/LineItemUpdateActionUtilsTest.java @@ -0,0 +1,445 @@ +package com.commercetools.sync.shoppinglists.utils; + +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.shoppinglists.LineItem; +import io.sphere.sdk.shoppinglists.LineItemDraft; +import io.sphere.sdk.shoppinglists.LineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeLineItemQuantity; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetLineItemCustomField; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetLineItemCustomType; +import io.sphere.sdk.types.CustomFields; +import io.sphere.sdk.types.CustomFieldsDraft; +import io.sphere.sdk.types.Type; +import org.junit.jupiter.api.Test; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static com.commercetools.sync.shoppinglists.utils.LineItemUpdateActionUtils.buildChangeLineItemQuantityUpdateAction; +import static com.commercetools.sync.shoppinglists.utils.LineItemUpdateActionUtils.buildLineItemCustomUpdateActions; +import static com.commercetools.sync.shoppinglists.utils.LineItemUpdateActionUtils.buildLineItemUpdateActions; +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class LineItemUpdateActionUtilsTest { + + private static final ShoppingListSyncOptions SYNC_OPTIONS = + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)).build(); + + final ShoppingList oldShoppingList = mock(ShoppingList.class); + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + + @Test + void buildLineItemCustomUpdateActions_WithSameValues_ShouldNotBuildUpdateAction() { + final Map oldCustomFieldsMap = new HashMap<>(); + oldCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(true)); + oldCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("de", "val")); + + final CustomFields oldCustomFields = mock(CustomFields.class); + when(oldCustomFields.getType()).thenReturn(Type.referenceOfId("1")); + when(oldCustomFields.getFieldsJsonMap()).thenReturn(oldCustomFieldsMap); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson("1", oldCustomFieldsMap); + + final LineItem oldLineItem = mock(LineItem.class); + when(oldLineItem.getId()).thenReturn("line_item_id"); + when(oldLineItem.getCustom()).thenReturn(oldCustomFields); + + final LineItemDraft newLineItem = + LineItemDraftBuilder.ofSku("sku", 1L) + .addedAt(ZonedDateTime.now()) + .custom(newCustomFieldsDraft) + .build(); + + final List> updateActions = + buildLineItemCustomUpdateActions(oldShoppingList, newShoppingList, oldLineItem, newLineItem, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildLineItemCustomUpdateActions_WithDifferentValues_ShouldBuildUpdateAction() { + final Map oldCustomFieldsMap = new HashMap<>(); + oldCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(true)); + oldCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("de", "val1")); + + final Map newCustomFieldsMap = new HashMap<>(); + newCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(false)); + newCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("es", "val2")); + + final CustomFields oldCustomFields = mock(CustomFields.class); + when(oldCustomFields.getType()).thenReturn(Type.referenceOfId("1")); + when(oldCustomFields.getFieldsJsonMap()).thenReturn(oldCustomFieldsMap); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson("1", newCustomFieldsMap); + + final LineItem oldLineItem = mock(LineItem.class); + when(oldLineItem.getId()).thenReturn("line_item_id"); + when(oldLineItem.getCustom()).thenReturn(oldCustomFields); + + final LineItemDraft newLineItem = + LineItemDraftBuilder.ofSku("sku", 1L) + .addedAt(ZonedDateTime.now()) + .custom(newCustomFieldsDraft) + .build(); + + final List> updateActions = + buildLineItemCustomUpdateActions(oldShoppingList, newShoppingList, oldLineItem, newLineItem, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + SetLineItemCustomField.ofJson("field1", + JsonNodeFactory.instance.booleanNode(false), "line_item_id"), + SetLineItemCustomField.ofJson("field2", + JsonNodeFactory.instance.objectNode().put("es", "val2"), "line_item_id") + ); + } + + @Test + void buildLineItemCustomUpdateActions_WithNullOldValues_ShouldBuildUpdateAction() { + final Map newCustomFieldsMap = new HashMap<>(); + newCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(false)); + newCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("es", "val")); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson("1", newCustomFieldsMap); + + final LineItem oldLineItem = mock(LineItem.class); + when(oldLineItem.getId()).thenReturn("line_item_id"); + when(oldLineItem.getCustom()).thenReturn(null); + + final LineItemDraft newLineItem = + LineItemDraftBuilder.ofSku("sku", 1L) + .addedAt(ZonedDateTime.now()) + .custom(newCustomFieldsDraft) + .build(); + + final List> updateActions = + buildLineItemCustomUpdateActions(oldShoppingList, newShoppingList, oldLineItem, newLineItem, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + SetLineItemCustomType.ofTypeIdAndJson("1", newCustomFieldsMap, "line_item_id") + ); + } + + @Test + void buildLineItemCustomUpdateActions_WithBadCustomFieldData_ShouldNotBuildUpdateActionAndTriggerErrorCallback() { + final Map oldCustomFieldsMap = new HashMap<>(); + oldCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(true)); + oldCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("de", "val1")); + + final Map newCustomFieldsMap = new HashMap<>(); + newCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(false)); + newCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("es", "val2")); + + + final CustomFields oldCustomFields = mock(CustomFields.class); + when(oldCustomFields.getType()).thenReturn(Type.referenceOfId("")); + when(oldCustomFields.getFieldsJsonMap()).thenReturn(oldCustomFieldsMap); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson("", newCustomFieldsMap); + + final LineItem oldLineItem = mock(LineItem.class); + when(oldLineItem.getId()).thenReturn("line_item_id"); + when(oldLineItem.getCustom()).thenReturn(oldCustomFields); + + final LineItemDraft newLineItem = + LineItemDraftBuilder.ofSku("sku", 1L) + .addedAt(ZonedDateTime.now()) + .custom(newCustomFieldsDraft) + .build(); + + final List errors = new ArrayList<>(); + + final ShoppingListSyncOptions syncOptions = + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)) + .errorCallback((exception, oldResource, newResource, updateActions) -> + errors.add(exception.getMessage())) + .build(); + + final List> updateActions = + buildLineItemCustomUpdateActions(oldShoppingList, newShoppingList, oldLineItem, newLineItem, syncOptions); + + assertThat(updateActions).isEmpty(); + assertThat(errors).hasSize(1); + assertThat(errors.get(0)) + .isEqualTo(format("Failed to build custom fields update actions on the line-item with id '%s'." + + " Reason: Custom type ids are not set for both the old and new line-item.", oldLineItem.getId())); + } + + @Test + void buildLineItemCustomUpdateActions_WithNullValue_ShouldCorrectlyBuildAction() { + final Map oldCustomFieldsMap = new HashMap<>(); + oldCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(true)); + oldCustomFieldsMap.put("field2", JsonNodeFactory + .instance + .arrayNode() + .add(JsonNodeFactory.instance.booleanNode(false))); + + final Map newCustomFieldsMap = new HashMap<>(); + newCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(true)); + newCustomFieldsMap.put("field2", null); + + + final CustomFields oldCustomFields = mock(CustomFields.class); + final String typeId = UUID.randomUUID().toString(); + when(oldCustomFields.getType()).thenReturn(Type.referenceOfId(typeId)); + when(oldCustomFields.getFieldsJsonMap()).thenReturn(oldCustomFieldsMap); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson(typeId, newCustomFieldsMap); + + final LineItem oldLineItem = mock(LineItem.class); + when(oldLineItem.getId()).thenReturn("line_item_id"); + when(oldLineItem.getCustom()).thenReturn(oldCustomFields); + + final LineItemDraft newLineItem = + LineItemDraftBuilder.ofSku("sku", 1L) + .addedAt(ZonedDateTime.now()) + .custom(newCustomFieldsDraft) + .build(); + + final List errors = new ArrayList<>(); + + final ShoppingListSyncOptions syncOptions = + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)) + .errorCallback((exception, oldResource, newResource, updateActions) -> + errors.add(exception.getMessage())) + .build(); + + final List> updateActions = + buildLineItemCustomUpdateActions(oldShoppingList, newShoppingList, oldLineItem, newLineItem, syncOptions); + + assertThat(errors).isEmpty(); + assertThat(updateActions) + .containsExactly(SetLineItemCustomField.ofJson("field2", null, "line_item_id")); + } + + @Test + void buildLineItemCustomUpdateActions_WithNullJsonNodeValue_ShouldCorrectlyBuildAction() { + final Map oldCustomFieldsMap = new HashMap<>(); + oldCustomFieldsMap.put("field", JsonNodeFactory + .instance + .arrayNode() + .add(JsonNodeFactory.instance.booleanNode(false))); + + final Map newCustomFieldsMap = new HashMap<>(); + newCustomFieldsMap.put("field", JsonNodeFactory.instance.nullNode()); + + + final CustomFields oldCustomFields = mock(CustomFields.class); + final String typeId = UUID.randomUUID().toString(); + when(oldCustomFields.getType()).thenReturn(Type.referenceOfId(typeId)); + when(oldCustomFields.getFieldsJsonMap()).thenReturn(oldCustomFieldsMap); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson(typeId, newCustomFieldsMap); + + final LineItem oldLineItem = mock(LineItem.class); + when(oldLineItem.getId()).thenReturn("line_item_id"); + when(oldLineItem.getCustom()).thenReturn(oldCustomFields); + + final LineItemDraft newLineItem = + LineItemDraftBuilder.ofSku("sku", 1L) + .addedAt(ZonedDateTime.now()) + .custom(newCustomFieldsDraft) + .build(); + + final List errors = new ArrayList<>(); + + final ShoppingListSyncOptions syncOptions = + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)) + .errorCallback((exception, oldResource, newResource, updateActions) -> + errors.add(exception.getMessage())) + .build(); + + final List> updateActions = + buildLineItemCustomUpdateActions(oldShoppingList, newShoppingList, oldLineItem, newLineItem, syncOptions); + + assertThat(errors).isEmpty(); + assertThat(updateActions) + .containsExactly(SetLineItemCustomField.ofJson("field", null, "line_item_id")); + } + + @Test + void buildChangeLineItemQuantityUpdateAction_WithSameValues_ShouldNotBuildUpdateAction() { + final LineItem oldLineItem = mock(LineItem.class); + when(oldLineItem.getId()).thenReturn("line_item_id"); + when(oldLineItem.getQuantity()).thenReturn(2L); + + final LineItemDraft newLineItem = + LineItemDraftBuilder.ofSku("sku", 2L) + .addedAt(ZonedDateTime.now()) + .build(); + + final Optional> updateAction = + buildChangeLineItemQuantityUpdateAction(oldLineItem, newLineItem); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction).isNotPresent(); + } + + @Test + void buildChangeLineItemQuantityUpdateAction_WithDifferentValues_ShouldBuildUpdateAction() { + final LineItem oldLineItem = mock(LineItem.class); + when(oldLineItem.getId()).thenReturn("line_item_id"); + when(oldLineItem.getQuantity()).thenReturn(2L); + + final LineItemDraft newLineItem = + LineItemDraftBuilder.ofSku("sku", 4L) + .addedAt(ZonedDateTime.now()) + .build(); + + final UpdateAction updateAction = + buildChangeLineItemQuantityUpdateAction(oldLineItem, newLineItem).orElse(null); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction.getAction()).isEqualTo("changeLineItemQuantity"); + assertThat((ChangeLineItemQuantity) updateAction) + .isEqualTo(ChangeLineItemQuantity.of("line_item_id", 4L)); + } + + @Test + void buildChangeLineItemQuantityUpdateAction_WithNewNullValue_ShouldBuildUpdateAction() { + final LineItem oldLineItem = mock(LineItem.class); + when(oldLineItem.getId()).thenReturn("line_item_id"); + when(oldLineItem.getQuantity()).thenReturn(2L); + + final LineItemDraft newLineItem = + LineItemDraftBuilder.ofSku("sku", null) + .addedAt(ZonedDateTime.now()) + .build(); + + final UpdateAction updateAction = + buildChangeLineItemQuantityUpdateAction(oldLineItem, newLineItem).orElse(null); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction.getAction()).isEqualTo("changeLineItemQuantity"); + assertThat((ChangeLineItemQuantity) updateAction) + .isEqualTo(ChangeLineItemQuantity.of("line_item_id", 1L)); + } + + @Test + void buildChangeLineItemQuantityUpdateAction_WithNewZeroValue_ShouldBuildUpdateAction() { + final LineItem oldLineItem = mock(LineItem.class); + when(oldLineItem.getId()).thenReturn("line_item_id"); + when(oldLineItem.getQuantity()).thenReturn(2L); + + final LineItemDraft newLineItem = + LineItemDraftBuilder.ofSku("sku", null) + .quantity(0L) + .addedAt(ZonedDateTime.now()) + .build(); + + final UpdateAction updateAction = + buildChangeLineItemQuantityUpdateAction(oldLineItem, newLineItem).orElse(null); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction.getAction()).isEqualTo("changeLineItemQuantity"); + assertThat((ChangeLineItemQuantity) updateAction) + .isEqualTo(ChangeLineItemQuantity.of("line_item_id", 0L)); + } + + @Test + void buildChangeLineItemQuantityUpdateAction_WithNewNullValueAndOldDefaultValue_ShouldNotBuildAction() { + final LineItem oldLineItem = mock(LineItem.class); + when(oldLineItem.getId()).thenReturn("line_item_id"); + when(oldLineItem.getQuantity()).thenReturn(1L); + + final LineItemDraft newLineItem = + LineItemDraftBuilder.ofSku("sku", null) + .addedAt(ZonedDateTime.now()) + .build(); + + final Optional> updateAction = + buildChangeLineItemQuantityUpdateAction(oldLineItem, newLineItem); + + assertThat(updateAction).isNotPresent(); + } + + @Test + void buildLineItemUpdateActions_WithSameValues_ShouldNotBuildUpdateAction() { + final Map oldCustomFieldsMap = new HashMap<>(); + oldCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(true)); + oldCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("de", "val")); + + final CustomFields oldCustomFields = mock(CustomFields.class); + when(oldCustomFields.getType()).thenReturn(Type.referenceOfId("1")); + when(oldCustomFields.getFieldsJsonMap()).thenReturn(oldCustomFieldsMap); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson("1", oldCustomFieldsMap); + + final LineItem oldLineItem = mock(LineItem.class); + when(oldLineItem.getId()).thenReturn("line_item_id"); + when(oldLineItem.getQuantity()).thenReturn(1L); + when(oldLineItem.getCustom()).thenReturn(oldCustomFields); + + final LineItemDraft newLineItem = + LineItemDraftBuilder.ofSku("sku", 1L) + .addedAt(ZonedDateTime.now()) + .custom(newCustomFieldsDraft) + .build(); + + final List> updateActions = + buildLineItemUpdateActions(oldShoppingList, newShoppingList, oldLineItem, newLineItem, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildLineItemUpdateActions_WithDifferentValues_ShouldBuildUpdateAction() { + final Map oldCustomFieldsMap = new HashMap<>(); + oldCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(true)); + oldCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("de", "val1")); + + final Map newCustomFieldsMap = new HashMap<>(); + newCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(false)); + newCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("es", "val2")); + + final CustomFields oldCustomFields = mock(CustomFields.class); + when(oldCustomFields.getType()).thenReturn(Type.referenceOfId("1")); + when(oldCustomFields.getFieldsJsonMap()).thenReturn(oldCustomFieldsMap); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson("1", newCustomFieldsMap); + + final LineItem oldLineItem = mock(LineItem.class); + when(oldLineItem.getId()).thenReturn("line_item_id"); + when(oldLineItem.getQuantity()).thenReturn(2L); + when(oldLineItem.getCustom()).thenReturn(oldCustomFields); + + final LineItemDraft newLineItem = + LineItemDraftBuilder.ofSku("sku", 4L) + .addedAt(ZonedDateTime.now()) + .custom(newCustomFieldsDraft) + .build(); + + final List> updateActions = + buildLineItemUpdateActions(oldShoppingList, newShoppingList, oldLineItem, newLineItem, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + ChangeLineItemQuantity.of("line_item_id", 4L), + SetLineItemCustomField.ofJson("field1", + JsonNodeFactory.instance.booleanNode(false), "line_item_id"), + SetLineItemCustomField.ofJson("field2", + JsonNodeFactory.instance.objectNode().put("es", "val2"), "line_item_id") + ); + } +} diff --git a/src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtilsTest.java b/src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtilsTest.java index b3ee626468..389764707b 100644 --- a/src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtilsTest.java +++ b/src/test/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtilsTest.java @@ -61,7 +61,7 @@ void setup() { } @Test - void buildActions_WithDifferentValues_ShouldReturnActions() { + void buildActions_WithDifferentValuesExceptLineItems_ShouldReturnActions() { final ShoppingList oldShoppingList = mock(ShoppingList.class); when(oldShoppingList.getSlug()).thenReturn(LocalizedString.of(LOCALE, "oldSlug")); when(oldShoppingList.getName()).thenReturn(LocalizedString.of(LOCALE, "oldName")); diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-not-expanded.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-not-expanded.json new file mode 100644 index 0000000000..e213c9f59a --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-not-expanded.json @@ -0,0 +1,157 @@ +{ + "key": "shoppinglist-with-lineitems-not-expanded", + "name": { + "en": "name" + }, + "lineItems": [ + { + "id": "line_item_id_1", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 1, + "quantity": 1, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text1" + } + }, + "addedAt": "2020-11-02T10:00:00.000Z", + "variant": { + "id": 1, + "sku": "SKU-1" + } + }, + { + "id": "line_item_id_2", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 2, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text2" + } + }, + "addedAt": "2020-11-03T10:00:00.000Z" + }, + { + "id": "line_item_id_3", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 3, + "quantity": 3, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text3" + } + }, + "addedAt": "2020-11-04T10:00:00.000Z", + "variant": { + "id": 3 + } + } + ], + "textLineItems": [] +} diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-12.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-12.json new file mode 100644 index 0000000000..4954543d0b --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-12.json @@ -0,0 +1,111 @@ +{ + "key": "shoppinglist-with-lineitems-sku-12", + "name": { + "en": "name" + }, + "lineItems": [ + { + "id": "line_item_id_1", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 1, + "quantity": 1, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text1" + } + }, + "addedAt": "2020-11-02T10:00:00.000Z", + "variant": { + "id": 1, + "sku": "SKU-1" + } + }, + { + "id": "line_item_id_2", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 2, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text2" + } + }, + "addedAt": "2020-11-03T10:00:00.000Z", + "variant": { + "id": 2, + "sku": "SKU-2" + } + } + ], + "textLineItems": [] +} diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-with-changes.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-with-changes.json new file mode 100644 index 0000000000..54e57db09c --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-with-changes.json @@ -0,0 +1,162 @@ +{ + "key": "shoppinglist-with-lineitems-sku-123-with-changes", + "name": { + "en": "name" + }, + "lineItems": [ + { + "id": "line_item_id_1", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 1, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "newText1" + } + }, + "addedAt": "2020-11-02T10:00:00.000Z", + "variant": { + "id": 1, + "sku": "SKU-1" + } + }, + { + "id": "line_item_id_2", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 2, + "quantity": 4, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "newText2" + } + }, + "addedAt": "2020-11-03T10:00:00.000Z", + "variant": { + "id": 2, + "sku": "SKU-2" + } + }, + { + "id": "line_item_id_3", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 3, + "quantity": 6, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "newText3" + } + }, + "addedAt": "2020-11-04T10:00:00.000Z", + "variant": { + "id": 3, + "sku": "SKU-3" + } + } + ], + "textLineItems": [] +} diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-with-different-addedAt.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-with-different-addedAt.json new file mode 100644 index 0000000000..e2ff548440 --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-with-different-addedAt.json @@ -0,0 +1,183 @@ +{ + "key": "shoppinglist-with-lineitems-sku-123-with-different-addedAt", + "name": { + "en": "name" + }, + "slug": { + "en": "slug" + }, + "description": { + "en": "description" + }, + "anonymousId": "anonymousId", + "lineItems": [ + { + "id": "line_item_id_1", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 1, + "quantity": 1, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text1" + } + }, + "addedAt": "2020-12-02T10:00:00.000Z", + "variant": { + "id": 1, + "sku": "SKU-1" + } + }, + { + "id": "line_item_id_2", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 2, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text2" + } + }, + "addedAt": "2020-12-03T10:00:00.000Z", + "variant": { + "id": 2, + "sku": "SKU-2" + } + }, + { + "id": "line_item_id_3", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 3, + "quantity": 3, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text3" + } + }, + "addedAt": "2020-12-04T10:00:00.000Z", + "variant": { + "id": 3, + "sku": "SKU-3" + } + } + ], + "textLineItems": [], + "customer": { + "typeId": "customer", + "id": "customer_id_1" + }, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id" + }, + "fields": { + "textField": "textValue" + } + }, + "deleteDaysAfterLastModification": 30 +} diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-without-addedAt.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-without-addedAt.json new file mode 100644 index 0000000000..ad0940a82b --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-without-addedAt.json @@ -0,0 +1,180 @@ +{ + "key": "shoppinglist-with-lineitems-sku-123-without-addedAt", + "name": { + "en": "name" + }, + "slug": { + "en": "slug" + }, + "description": { + "en": "description" + }, + "anonymousId": "anonymousId", + "lineItems": [ + { + "id": "line_item_id_1", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 1, + "quantity": 1, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text1" + } + }, + "variant": { + "id": 1, + "sku": "SKU-1" + } + }, + { + "id": "line_item_id_2", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 2, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text2" + } + }, + "variant": { + "id": 2, + "sku": "SKU-2" + } + }, + { + "id": "line_item_id_3", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 3, + "quantity": 3, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text3" + } + }, + "variant": { + "id": 3, + "sku": "SKU-3" + } + } + ], + "textLineItems": [], + "customer": { + "typeId": "customer", + "id": "customer_id_1" + }, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id" + }, + "fields": { + "textField": "textValue" + } + }, + "deleteDaysAfterLastModification": 30 +} diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123.json new file mode 100644 index 0000000000..ef4d76d405 --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123.json @@ -0,0 +1,183 @@ +{ + "key": "shoppinglist-with-lineitems-sku-123", + "name": { + "en": "name" + }, + "slug": { + "en": "slug" + }, + "description": { + "en": "description" + }, + "anonymousId": "anonymousId", + "lineItems": [ + { + "id": "line_item_id_1", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 1, + "quantity": 1, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text1" + } + }, + "addedAt": "2020-11-02T10:00:00.000Z", + "variant": { + "id": 1, + "sku": "SKU-1" + } + }, + { + "id": "line_item_id_2", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 2, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text2" + } + }, + "addedAt": "2020-11-03T10:00:00.000Z", + "variant": { + "id": 2, + "sku": "SKU-2" + } + }, + { + "id": "line_item_id_3", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 3, + "quantity": 3, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text3" + } + }, + "addedAt": "2020-11-04T10:00:00.000Z", + "variant": { + "id": 3, + "sku": "SKU-3" + } + } + ], + "textLineItems": [], + "customer": { + "typeId": "customer", + "id": "customer_id_1" + }, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id" + }, + "fields": { + "textField": "textValue" + } + }, + "deleteDaysAfterLastModification": 30 +} diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-1234.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-1234.json new file mode 100644 index 0000000000..876f3e65e5 --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-1234.json @@ -0,0 +1,213 @@ +{ + "key": "shoppinglist-with-lineitems-sku-1234", + "name": { + "en": "name" + }, + "lineItems": [ + { + "id": "line_item_id_1", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 1, + "quantity": 1, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text1" + } + }, + "addedAt": "2020-11-02T10:00:00.000Z", + "variant": { + "id": 1, + "sku": "SKU-1" + } + }, + { + "id": "line_item_id_2", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 2, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text2" + } + }, + "addedAt": "2020-11-03T10:00:00.000Z", + "variant": { + "id": 2, + "sku": "SKU-2" + } + }, + { + "id": "line_item_id_3", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 3, + "quantity": 3, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text3" + } + }, + "addedAt": "2020-11-04T10:00:00.000Z", + "variant": { + "id": 3, + "sku": "SKU-3" + } + }, + { + "id": "line_item_id_4", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 4, + "quantity": 4, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text4" + } + }, + "addedAt": "2020-11-05T10:00:00.000Z", + "variant": { + "id": 4, + "sku": "SKU-4" + } + } + ], + "textLineItems": [] +} diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-124.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-124.json new file mode 100644 index 0000000000..199f5d445f --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-124.json @@ -0,0 +1,162 @@ +{ + "key": "shoppinglist-with-lineitems-sku-124", + "name": { + "en": "name" + }, + "lineItems": [ + { + "id": "line_item_id_1", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 1, + "quantity": 1, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text1" + } + }, + "addedAt": "2020-11-02T10:00:00.000Z", + "variant": { + "id": 1, + "sku": "SKU-1" + } + }, + { + "id": "line_item_id_2", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 2, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text2" + } + }, + "addedAt": "2020-11-03T10:00:00.000Z", + "variant": { + "id": 2, + "sku": "SKU-2" + } + }, + { + "id": "line_item_id_4", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 4, + "quantity": 4, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text4" + } + }, + "addedAt": "2020-11-05T10:00:00.000Z", + "variant": { + "id": 4, + "sku": "SKU-4" + } + } + ], + "textLineItems": [] +} diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-132-with-changes.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-132-with-changes.json new file mode 100644 index 0000000000..4c02bb05f5 --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-132-with-changes.json @@ -0,0 +1,183 @@ +{ + "key": "shoppinglist-with-lineitems-sku-132-with-changes", + "name": { + "en": "newName" + }, + "slug": { + "en": "newSlug" + }, + "description": { + "en": "newDescription" + }, + "anonymousId": "newAnonymousId", + "lineItems": [ + { + "id": "line_item_id_1", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 1, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "newText1" + } + }, + "addedAt": "2020-11-02T10:00:00.000Z", + "variant": { + "id": 1, + "sku": "SKU-1" + } + }, + { + "id": "line_item_id_3", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 3, + "quantity": 6, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "newText3" + } + }, + "addedAt": "2020-11-04T10:00:00.000Z", + "variant": { + "id": 3, + "sku": "SKU-3" + } + }, + { + "id": "line_item_id_2", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 2, + "quantity": 4, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "newText2" + } + }, + "addedAt": "2020-11-03T10:00:00.000Z", + "variant": { + "id": 2, + "sku": "SKU-2" + } + } + ], + "textLineItems": [], + "customer": { + "typeId": "customer", + "id": "customer_id_2" + }, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id" + }, + "fields": { + "textField": "newTextValue" + } + }, + "deleteDaysAfterLastModification": 45 +} diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-1324.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-1324.json new file mode 100644 index 0000000000..4d128d17e9 --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-1324.json @@ -0,0 +1,213 @@ +{ + "key": "shoppinglist-with-lineitems-sku-1324", + "name": { + "en": "name" + }, + "lineItems": [ + { + "id": "line_item_id_1", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 1, + "quantity": 1, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text1" + } + }, + "addedAt": "2020-11-02T10:00:00.000Z", + "variant": { + "id": 1, + "sku": "SKU-1" + } + }, + { + "id": "line_item_id_3", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 3, + "quantity": 3, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text3" + } + }, + "addedAt": "2020-11-04T10:00:00.000Z", + "variant": { + "id": 3, + "sku": "SKU-3" + } + }, + { + "id": "line_item_id_2", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 2, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text2" + } + }, + "addedAt": "2020-11-03T10:00:00.000Z", + "variant": { + "id": 2, + "sku": "SKU-2" + } + }, + { + "id": "line_item_id_4", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 4, + "quantity": 4, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text4" + } + }, + "addedAt": "2020-11-05T10:00:00.000Z", + "variant": { + "id": 4, + "sku": "SKU-4" + } + } + ], + "textLineItems": [] +} diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-1423.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-1423.json new file mode 100644 index 0000000000..ad03fcd7c9 --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-1423.json @@ -0,0 +1,213 @@ +{ + "key": "shoppinglist-with-lineitems-sku-1423", + "name": { + "en": "name" + }, + "lineItems": [ + { + "id": "line_item_id_1", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 1, + "quantity": 1, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text1" + } + }, + "addedAt": "2020-11-02T10:00:00.000Z", + "variant": { + "id": 1, + "sku": "SKU-1" + } + }, + { + "id": "line_item_id_4", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 4, + "quantity": 4, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text4" + } + }, + "addedAt": "2020-11-05T10:00:00.000Z", + "variant": { + "id": 4, + "sku": "SKU-4" + } + }, + { + "id": "line_item_id_2", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 2, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text2" + } + }, + "addedAt": "2020-11-03T10:00:00.000Z", + "variant": { + "id": 2, + "sku": "SKU-2" + } + }, + { + "id": "line_item_id_3", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 3, + "quantity": 3, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text3" + } + }, + "addedAt": "2020-11-04T10:00:00.000Z", + "variant": { + "id": 3, + "sku": "SKU-3" + } + } + ], + "textLineItems": [] +} diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-312.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-312.json new file mode 100644 index 0000000000..d2d240cce8 --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-312.json @@ -0,0 +1,162 @@ +{ + "key": "shoppinglist-with-lineitems-sku-312", + "name": { + "en": "name" + }, + "lineItems": [ + { + "id": "line_item_id_3", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 3, + "quantity": 3, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text3" + } + }, + "addedAt": "2020-11-04T10:00:00.000Z", + "variant": { + "id": 3, + "sku": "SKU-3" + } + }, + { + "id": "line_item_id_1", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 1, + "quantity": 1, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text1" + } + }, + "addedAt": "2020-11-02T10:00:00.000Z", + "variant": { + "id": 1, + "sku": "SKU-1" + } + }, + { + "id": "line_item_id_2", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 2, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text2" + } + }, + "addedAt": "2020-11-03T10:00:00.000Z", + "variant": { + "id": 2, + "sku": "SKU-2" + } + } + ], + "textLineItems": [] +} diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-32.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-32.json new file mode 100644 index 0000000000..8c05c83999 --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-32.json @@ -0,0 +1,111 @@ +{ + "key": "shoppinglist-with-lineitems-sku-32", + "name": { + "en": "name" + }, + "lineItems": [ + { + "id": "line_item_id_3", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 3, + "quantity": 3, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text3" + } + }, + "addedAt": "2020-11-04T10:00:00.000Z", + "variant": { + "id": 3, + "sku": "SKU-3" + } + }, + { + "id": "line_item_id_2", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 2, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text2" + } + }, + "addedAt": "2020-11-03T10:00:00.000Z", + "variant": { + "id": 2, + "sku": "SKU-2" + } + } + ], + "textLineItems": [] +} diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-324.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-324.json new file mode 100644 index 0000000000..51e0d71481 --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-324.json @@ -0,0 +1,162 @@ +{ + "key": "shoppinglist-with-lineitems-sku-324", + "name": { + "en": "name" + }, + "lineItems": [ + { + "id": "line_item_id_3", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 3, + "quantity": 3, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text3" + } + }, + "addedAt": "2020-11-04T10:00:00.000Z", + "variant": { + "id": 3, + "sku": "SKU-3" + } + }, + { + "id": "line_item_id_2", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 2, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text2" + } + }, + "addedAt": "2020-11-03T10:00:00.000Z", + "variant": { + "id": 2, + "sku": "SKU-2" + } + }, + { + "id": "line_item_id_4", + "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "name": { + "en": "Some Product" + }, + "variantId": 4, + "quantity": 4, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text4" + } + }, + "addedAt": "2020-11-05T10:00:00.000Z", + "variant": { + "id": 4, + "sku": "SKU-4" + } + } + ], + "textLineItems": [] +} From 7277062c8aadf6f57c8842a5b60ed397804c11b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahmet=20=C3=96z?= Date: Tue, 17 Nov 2020 17:43:51 +0100 Subject: [PATCH 07/12] Implement text line item actions (#625) --- .../AddTextLineItemWithAddedAt.java | 58 ++ .../utils/LineItemUpdateActionUtils.java | 48 +- .../utils/ShoppingListSyncUtils.java | 3 + .../TextLineItemCustomActionBuilder.java | 46 ++ .../utils/TextLineItemUpdateActionUtils.java | 290 +++++++++ ...oppingListCustomUpdateActionUtilsTest.java | 52 ++ .../shoppinglists/ShoppingListSyncTest.java | 11 +- .../LineItemListUpdateActionUtilsTest.java | 118 ++-- ...TextLineItemListUpdateActionUtilsTest.java | 470 ++++++++++++++ .../TextLineItemUpdateActionUtilsTest.java | 576 ++++++++++++++++++ ...oppinglist-with-textlineitems-name-12.json | 130 ++++ ...-textlineitems-name-123-with-changes.json} | 83 ++- ...pinglist-with-textlineitems-name-123.json} | 54 +- ...pinglist-with-textlineitems-name-1234.json | 230 +++++++ 14 files changed, 2038 insertions(+), 131 deletions(-) create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/commands/updateactions/AddTextLineItemWithAddedAt.java create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/utils/TextLineItemCustomActionBuilder.java create mode 100644 src/main/java/com/commercetools/sync/shoppinglists/utils/TextLineItemUpdateActionUtils.java create mode 100644 src/test/java/com/commercetools/sync/shoppinglists/utils/TextLineItemListUpdateActionUtilsTest.java create mode 100644 src/test/java/com/commercetools/sync/shoppinglists/utils/TextLineItemUpdateActionUtilsTest.java create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/textlineitems/shoppinglist-with-textlineitems-name-12.json rename src/test/resources/com/commercetools/sync/shoppinglists/utils/{lineitems/shoppinglist-with-lineitems-sku-123-with-different-addedAt.json => textlineitems/shoppinglist-with-textlineitems-name-123-with-changes.json} (73%) rename src/test/resources/com/commercetools/sync/shoppinglists/utils/{lineitems/shoppinglist-with-lineitems-sku-123-without-addedAt.json => textlineitems/shoppinglist-with-textlineitems-name-123.json} (82%) create mode 100644 src/test/resources/com/commercetools/sync/shoppinglists/utils/textlineitems/shoppinglist-with-textlineitems-name-1234.json diff --git a/src/main/java/com/commercetools/sync/shoppinglists/commands/updateactions/AddTextLineItemWithAddedAt.java b/src/main/java/com/commercetools/sync/shoppinglists/commands/updateactions/AddTextLineItemWithAddedAt.java new file mode 100644 index 0000000000..ae57c60d1b --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/commands/updateactions/AddTextLineItemWithAddedAt.java @@ -0,0 +1,58 @@ +package com.commercetools.sync.shoppinglists.commands.updateactions; + +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.commands.UpdateActionImpl; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.TextLineItemDraft; +import io.sphere.sdk.types.CustomFieldsDraft; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.time.ZonedDateTime; + +/** + * TODO (JVM-SDK): https://github.com/commercetools/commercetools-jvm-sdk/issues/2079 + * ShoppingList#AddTextLineItem action does not support `addedAt` value, + * so we needed to add this custom action as a workaround. + */ +public final class AddTextLineItemWithAddedAt extends UpdateActionImpl { + + private final LocalizedString name; + private final LocalizedString description; + private final Long quantity; + private final ZonedDateTime addedAt; + private final CustomFieldsDraft custom; + + private AddTextLineItemWithAddedAt( + @Nonnull final LocalizedString name, + @Nullable final LocalizedString description, + @Nullable final Long quantity, + @Nullable final ZonedDateTime addedAt, + @Nullable final CustomFieldsDraft custom) { + + super("addTextLineItem"); + this.name = name; + this.description = description; + this.quantity = quantity; + this.addedAt = addedAt; + this.custom = custom; + } + + /** + * Creates an update action "addTextLineItem" which adds a text line item to a shopping list. + * + * @param textLineItemDraft text line item draft template to map update action's fields. + * @return an update action "addTextLineItem" which adds a text line item to a shopping list. + */ + @Nonnull + public static UpdateAction of(@Nonnull final TextLineItemDraft textLineItemDraft) { + + return new AddTextLineItemWithAddedAt( + textLineItemDraft.getName(), + textLineItemDraft.getDescription(), + textLineItemDraft.getQuantity(), + textLineItemDraft.getAddedAt(), + textLineItemDraft.getCustom()); + } +} diff --git a/src/main/java/com/commercetools/sync/shoppinglists/utils/LineItemUpdateActionUtils.java b/src/main/java/com/commercetools/sync/shoppinglists/utils/LineItemUpdateActionUtils.java index eddf6830b0..fc525f8e40 100644 --- a/src/main/java/com/commercetools/sync/shoppinglists/utils/LineItemUpdateActionUtils.java +++ b/src/main/java/com/commercetools/sync/shoppinglists/utils/LineItemUpdateActionUtils.java @@ -1,5 +1,6 @@ package com.commercetools.sync.shoppinglists.utils; +import com.commercetools.sync.commons.exceptions.SyncException; import com.commercetools.sync.commons.utils.CustomUpdateActionUtils; import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; import com.commercetools.sync.shoppinglists.commands.updateactions.AddLineItemWithSku; @@ -70,6 +71,7 @@ public static List> buildLineItemsUpdateActions( return newShoppingList.getLineItems() .stream() .filter(Objects::nonNull) + .filter(LineItemUpdateActionUtils::hasQuantity) .map(AddLineItemWithSku::of) .collect(toList()); } @@ -83,6 +85,17 @@ public static List> buildLineItemsUpdateActions( return buildUpdateActions(oldShoppingList, newShoppingList, oldLineItems, newlineItems, syncOptions); } + private static boolean hasQuantity(@Nonnull final LineItemDraft lineItemDraft) { + /* + + with this check, it's avoided bad request case like below: + + "code": "InvalidField", + "message": "The value '0' is not valid for field 'quantity'. Quantity 0 is not allowed.", + + */ + return lineItemDraft.getQuantity() != null && lineItemDraft.getQuantity() > 0; + } /** * The decisions in the calculating update actions are documented on the @@ -107,25 +120,29 @@ private static List> buildUpdateActions( if (oldLineItem.getVariant() == null || StringUtils.isBlank(oldLineItem.getVariant().getSku())) { - throw new IllegalArgumentException( - format("LineItem at position '%d' of the ShoppingList with key '%s' has no SKU set. " - + "Please make sure all line items have SKUs", i, oldShoppingList.getKey())); + syncOptions.applyErrorCallback(new SyncException( + format("LineItem at position '%d' of the ShoppingList with key '%s' has no SKU set. " + + "Please make sure all line items have SKUs.", i, oldShoppingList.getKey())), + oldShoppingList, newShoppingList, updateActions); + + return emptyList(); } else if (StringUtils.isBlank(newLineItem.getSku())) { - throw new IllegalArgumentException( - format("LineItemDraft at position '%d' of the ShoppingListDraft with key '%s' has no SKU set. " - + "Please make sure all line items have SKUs", i, newShoppingList.getKey())); + syncOptions.applyErrorCallback(new SyncException( + format("LineItemDraft at position '%d' of the ShoppingListDraft with key '%s' has no SKU set. " + + "Please make sure all line items have SKUs.", i, newShoppingList.getKey())), + oldShoppingList, newShoppingList, updateActions); + return emptyList(); } - if (oldLineItem.getVariant().getSku().equals(newLineItem.getSku()) - && hasIdenticalAddedAtValues(oldLineItem, newLineItem)) { + if (oldLineItem.getVariant().getSku().equals(newLineItem.getSku())) { updateActions.addAll(buildLineItemUpdateActions( oldShoppingList, newShoppingList, oldLineItem, newLineItem, syncOptions)); } else { - // different sku or addedAt means the order is different. + // different sku means the order is different. // To be able to ensure the order, we need to remove and add this line item back // with the up to date values. indexOfFirstDifference = i; @@ -143,22 +160,15 @@ && hasIdenticalAddedAtValues(oldLineItem, newLineItem)) { } for (int i = indexOfFirstDifference; i < newlineItems.size(); i++) { - updateActions.add(AddLineItemWithSku.of(newlineItems.get(i))); + if (hasQuantity(newlineItems.get(i))) { + updateActions.add(AddLineItemWithSku.of(newlineItems.get(i))); + } } return updateActions; } - private static boolean hasIdenticalAddedAtValues( - @Nonnull final LineItem oldLineItem, - @Nonnull final LineItemDraft newLineItem) { - - if (newLineItem.getAddedAt() == null) { - return true; // omit, if not set in draft. - } - return oldLineItem.getAddedAt().equals(newLineItem.getAddedAt()); - } /** * Compares all the fields of a {@link LineItem} and a {@link LineItemDraft} and returns a list of diff --git a/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtils.java b/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtils.java index 4182e2af7f..13749e7d34 100644 --- a/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtils.java +++ b/src/main/java/com/commercetools/sync/shoppinglists/utils/ShoppingListSyncUtils.java @@ -17,6 +17,7 @@ import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildSetDeleteDaysAfterLastModificationUpdateAction; import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildSetDescriptionUpdateAction; import static com.commercetools.sync.shoppinglists.utils.ShoppingListUpdateActionUtils.buildSetSlugUpdateAction; +import static com.commercetools.sync.shoppinglists.utils.TextLineItemUpdateActionUtils.buildTextLineItemsUpdateActions; public final class ShoppingListSyncUtils { @@ -59,6 +60,8 @@ public static List> buildActions( updateActions.addAll(buildLineItemsUpdateActions(oldShoppingList, newShoppingList, syncOptions)); + updateActions.addAll(buildTextLineItemsUpdateActions(oldShoppingList, newShoppingList, syncOptions)); + return updateActions; } diff --git a/src/main/java/com/commercetools/sync/shoppinglists/utils/TextLineItemCustomActionBuilder.java b/src/main/java/com/commercetools/sync/shoppinglists/utils/TextLineItemCustomActionBuilder.java new file mode 100644 index 0000000000..2fa8d89ef2 --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/utils/TextLineItemCustomActionBuilder.java @@ -0,0 +1,46 @@ +package com.commercetools.sync.shoppinglists.utils; + +import com.commercetools.sync.commons.helpers.GenericCustomActionBuilder; +import com.fasterxml.jackson.databind.JsonNode; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetTextLineItemCustomField; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetTextLineItemCustomType; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Map; + +public final class TextLineItemCustomActionBuilder implements GenericCustomActionBuilder { + + @Nonnull + @Override + public UpdateAction buildRemoveCustomTypeAction( + @Nullable final Integer variantId, + @Nullable final String textLineItemId) { + + return SetTextLineItemCustomType.ofRemoveType(textLineItemId); + } + + @Nonnull + @Override + public UpdateAction buildSetCustomTypeAction( + @Nullable final Integer variantId, + @Nullable final String textLineItemId, + @Nonnull final String customTypeId, + @Nullable final Map customFieldsJsonMap) { + + return SetTextLineItemCustomType.ofTypeIdAndJson(customTypeId, customFieldsJsonMap, textLineItemId); + } + + @Nonnull + @Override + public UpdateAction buildSetCustomFieldAction( + @Nullable final Integer variantId, + @Nullable final String textLineItemId, + @Nullable final String customFieldName, + @Nullable final JsonNode customFieldValue) { + + return SetTextLineItemCustomField.ofJson(customFieldName, customFieldValue, textLineItemId); + } +} diff --git a/src/main/java/com/commercetools/sync/shoppinglists/utils/TextLineItemUpdateActionUtils.java b/src/main/java/com/commercetools/sync/shoppinglists/utils/TextLineItemUpdateActionUtils.java new file mode 100644 index 0000000000..4e062911bb --- /dev/null +++ b/src/main/java/com/commercetools/sync/shoppinglists/utils/TextLineItemUpdateActionUtils.java @@ -0,0 +1,290 @@ +package com.commercetools.sync.shoppinglists.utils; + +import com.commercetools.sync.commons.exceptions.SyncException; +import com.commercetools.sync.commons.utils.CustomUpdateActionUtils; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import com.commercetools.sync.shoppinglists.commands.updateactions.AddTextLineItemWithAddedAt; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.TextLineItem; +import io.sphere.sdk.shoppinglists.TextLineItemDraft; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeTextLineItemName; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeTextLineItemQuantity; +import io.sphere.sdk.shoppinglists.commands.updateactions.RemoveTextLineItem; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetTextLineItemDescription; +import org.apache.commons.lang3.math.NumberUtils; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static com.commercetools.sync.commons.utils.CommonTypeUpdateActionUtils.buildUpdateAction; +import static com.commercetools.sync.commons.utils.OptionalUtils.filterEmptyOptionals; +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; + +public final class TextLineItemUpdateActionUtils { + + /** + * Compares a list of {@link TextLineItem}s with a list of {@link TextLineItemDraft}s. + * The method takes in functions for building the required update actions (AddTextLineItem, RemoveTextLineItem and + * 1-1 update actions on text line items (e.g. changeTextLineItemQuantity, setTextLineItemCustomType, etc..). + * + *

If the list of new {@link TextLineItemDraft}s is {@code null}, then remove actions are built for every + * existing text line item. + * + * @param oldShoppingList shopping list resource, whose text line items should be updated. + * @param newShoppingList new shopping list draft, which contains the text line items to update. + * @param syncOptions responsible for supplying the sync options to the sync utility method. It is used for + * triggering the error callback within the utility, in case of errors. + * @return a list of text line item update actions on the resource of shopping lists, if the list of text line items + * are not identical. Otherwise, if the text line items are identical, an empty list is returned. + */ + @Nonnull + public static List> buildTextLineItemsUpdateActions( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingList, + @Nonnull final ShoppingListSyncOptions syncOptions) { + + final boolean hasOldTextLineItems = oldShoppingList.getTextLineItems() != null + && !oldShoppingList.getTextLineItems().isEmpty(); + final boolean hasNewTextLineItems = newShoppingList.getTextLineItems() != null + && !newShoppingList.getTextLineItems().isEmpty() + && newShoppingList.getTextLineItems().stream().anyMatch(Objects::nonNull); + + if (hasOldTextLineItems && !hasNewTextLineItems) { + + return oldShoppingList.getTextLineItems() + .stream() + .map(RemoveTextLineItem::of) + .collect(toList()); + + } else if (!hasOldTextLineItems) { + + if (!hasNewTextLineItems) { + return emptyList(); + } + + return newShoppingList.getTextLineItems() + .stream() + .filter(Objects::nonNull) + .filter(TextLineItemUpdateActionUtils::hasQuantity) + .map(AddTextLineItemWithAddedAt::of) + .collect(toList()); + } + + final List oldTextLineItems = oldShoppingList.getTextLineItems(); + final List newTextLineItems = newShoppingList.getTextLineItems() + .stream() + .filter(Objects::nonNull) + .collect(toList()); + + return buildUpdateActions(oldShoppingList, newShoppingList, oldTextLineItems, newTextLineItems, syncOptions); + } + + private static boolean hasQuantity(@Nonnull final TextLineItemDraft textLineItemDraft) { + /* + + with this check, it's avoided bad request case like below: + + "code": "InvalidField", + "message": "The value '0' is not valid for field 'quantity'. Quantity 0 is not allowed.", + + */ + return textLineItemDraft.getQuantity() != null && textLineItemDraft.getQuantity() > 0; + } + + /** + * The decisions in the calculating update actions are documented on the + * `docs/adr/0002-shopping-lists-lineitem-and-textlineitem-update-actions.md` + */ + @Nonnull + private static List> buildUpdateActions( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingList, + @Nonnull final List oldTextLineItems, + @Nonnull final List newTextLineItems, + @Nonnull final ShoppingListSyncOptions syncOptions) { + + final List> updateActions = new ArrayList<>(); + + final int minSize = Math.min(oldTextLineItems.size(), newTextLineItems.size()); + for (int i = 0; i < minSize; i++) { + + final TextLineItem oldTextLineItem = oldTextLineItems.get(i); + final TextLineItemDraft newTextLineItem = newTextLineItems.get(i); + + if (newTextLineItem.getName() == null || newTextLineItem.getName().getLocales().isEmpty()) { + /* + checking the name of the oldTextLineItem is not needed, because it's required. + with this check below, it's avoided bad request case like: + + "detailedErrorMessage": "actions -> name: Missing required value" + */ + syncOptions.applyErrorCallback(new SyncException( + format("TextLineItemDraft at position '%d' of the ShoppingListDraft with key '%s' has no name " + + "set. Please make sure all text line items have names.", i, newShoppingList.getKey())), + oldShoppingList, newShoppingList, updateActions); + + return emptyList(); + } + + updateActions.addAll(buildTextLineItemUpdateActions( + oldShoppingList, newShoppingList, oldTextLineItem, newTextLineItem, syncOptions)); + } + + for (int i = minSize; i < oldTextLineItems.size(); i++) { + updateActions.add(RemoveTextLineItem.of(oldTextLineItems.get(i).getId())); + } + + for (int i = minSize; i < newTextLineItems.size(); i++) { + if (hasQuantity(newTextLineItems.get(i))) { + updateActions.add(AddTextLineItemWithAddedAt.of(newTextLineItems.get(i))); + } + } + + return updateActions; + } + + /** + * Compares all the fields of a {@link TextLineItem} and a {@link TextLineItemDraft} and returns a list of + * {@link UpdateAction}<{@link ShoppingList}> as a result. If both the {@link TextLineItem} and + * the {@link TextLineItemDraft} have identical fields, then no update action is needed and hence an empty + * {@link List} is returned. + * + * @param oldShoppingList shopping list resource, whose line item should be updated. + * @param newShoppingList new shopping list draft, which contains the line item to update. + * @param oldTextLineItem the text line item which should be updated. + * @param newTextLineItem the text line item draft where we get the new fields (i.e. quantity, custom fields). + * @param syncOptions responsible for supplying the sync options to the sync utility method. It is used for + * triggering the error callback within the utility, in case of errors. + * @return A list with the update actions or an empty list if the text line item fields are identical. + */ + @Nonnull + public static List> buildTextLineItemUpdateActions( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingList, + @Nonnull final TextLineItem oldTextLineItem, + @Nonnull final TextLineItemDraft newTextLineItem, + @Nonnull final ShoppingListSyncOptions syncOptions) { + + final List> updateActions = filterEmptyOptionals( + buildChangeTextLineItemNameUpdateAction(oldTextLineItem, newTextLineItem), + buildSetTextLineItemDescriptionUpdateAction(oldTextLineItem, newTextLineItem), + buildChangeTextLineItemQuantityUpdateAction(oldTextLineItem, newTextLineItem) + ); + + updateActions.addAll(buildTextLineItemCustomUpdateActions( + oldShoppingList, newShoppingList, oldTextLineItem, newTextLineItem, syncOptions)); + + return updateActions; + } + + /** + * Compares the {@link LocalizedString} names of {@link TextLineItem} and a {@link TextLineItemDraft} and + * returns an {@link Optional} of update action, which would contain the {@code "changeTextLineItemName"} + * {@link UpdateAction}. If both the {@link TextLineItem} and the {@link TextLineItemDraft} have the same + * {@code description} values, then no update action is needed and hence an empty optional will be returned. + * + * @param oldTextLineItem the text line item which should be updated. + * @param newTextLineItem the text line item draft where we get the new name. + * @return A filled optional with the update action or an empty optional if the names are identical. + */ + @Nonnull + public static Optional> buildChangeTextLineItemNameUpdateAction( + @Nonnull final TextLineItem oldTextLineItem, + @Nonnull final TextLineItemDraft newTextLineItem) { + + return buildUpdateAction(oldTextLineItem.getName(), newTextLineItem.getName(), () -> + ChangeTextLineItemName.of(oldTextLineItem.getId(), newTextLineItem.getName())); + } + + /** + * Compares the {@link LocalizedString} descriptions of {@link TextLineItem} and a {@link TextLineItemDraft} and + * returns an {@link Optional} of update action, which would contain the {@code "setTextLineItemDescription"} + * {@link UpdateAction}. If both the {@link TextLineItem} and the {@link TextLineItemDraft} have the same + * {@code description} values, then no update action is needed and hence an empty optional will be returned. + * + * @param oldTextLineItem the text line item which should be updated. + * @param newTextLineItem the text line item draft where we get the new description. + * @return A filled optional with the update action or an empty optional if the descriptions are identical. + */ + @Nonnull + public static Optional> buildSetTextLineItemDescriptionUpdateAction( + @Nonnull final TextLineItem oldTextLineItem, + @Nonnull final TextLineItemDraft newTextLineItem) { + + return buildUpdateAction(oldTextLineItem.getDescription(), newTextLineItem.getDescription(), () -> + SetTextLineItemDescription.of(oldTextLineItem).withDescription(newTextLineItem.getDescription())); + } + + /** + * Compares the {@code quantity} values of a {@link TextLineItem} and a {@link TextLineItemDraft} + * and returns an {@link Optional} of update action, which would contain the {@code "changeTextLineItemQuantity"} + * {@link UpdateAction}. If both {@link TextLineItem} and {@link TextLineItemDraft} have the same + * {@code quantity} values, then no update action is needed and empty optional will be returned. + * + *

Note: If {@code quantity} from the {@code newTextLineItem} is {@code null}, the new {@code quantity} + * will be set to default value {@code 1L}. If {@code quantity} from the {@code newTextLineItem} is {@code 0}, + * then it means removing the text line item. + * + * @param oldTextLineItem the text line item which should be updated. + * @param newTextLineItem the text line item draft where we get the new quantity. + * @return A filled optional with the update action or an empty optional if the quantities are identical. + */ + @Nonnull + public static Optional> buildChangeTextLineItemQuantityUpdateAction( + @Nonnull final TextLineItem oldTextLineItem, + @Nonnull final TextLineItemDraft newTextLineItem) { + + final Long newTextLineItemQuantity = newTextLineItem.getQuantity() == null + ? NumberUtils.LONG_ONE : newTextLineItem.getQuantity(); + + return buildUpdateAction(oldTextLineItem.getQuantity(), newTextLineItemQuantity, + () -> ChangeTextLineItemQuantity.of(oldTextLineItem.getId(), newTextLineItemQuantity)); + } + + /** + * Compares the custom fields and custom types of a {@link TextLineItem} and a {@link TextLineItemDraft} and returns + * a list of {@link UpdateAction}<{@link ShoppingList}> as a result. If both the {@link TextLineItem} and the + * {@link TextLineItemDraft} have identical custom fields and types, then no update action is needed and hence an + * empty {@link List} is returned. + * + * @param oldShoppingList shopping list resource, whose text line item should be updated. + * @param newShoppingList new shopping list draft, which contains the text line item to update. + * @param oldTextLineItem the text line item which should be updated. + * @param newTextLineItem the text line item draft where we get the new custom fields and types. + * @param syncOptions responsible for supplying the sync options to the sync utility method. It is used for + * triggering the error callback within the utility, in case of errors. + * @return A list with the custom field/type update actions or an empty list if the custom fields/types are + * identical. + */ + @Nonnull + public static List> buildTextLineItemCustomUpdateActions( + @Nonnull final ShoppingList oldShoppingList, + @Nonnull final ShoppingListDraft newShoppingList, + @Nonnull final TextLineItem oldTextLineItem, + @Nonnull final TextLineItemDraft newTextLineItem, + @Nonnull final ShoppingListSyncOptions syncOptions) { + + return CustomUpdateActionUtils.buildCustomUpdateActions( + oldShoppingList, + newShoppingList, + oldTextLineItem::getCustom, + newTextLineItem::getCustom, + new TextLineItemCustomActionBuilder(), + null, // not used by util. + t -> oldTextLineItem.getId(), + textLineItem -> TextLineItem.resourceTypeId(), + t -> oldTextLineItem.getId(), + syncOptions); + } + + private TextLineItemUpdateActionUtils() { + } +} diff --git a/src/test/java/com/commercetools/sync/commons/utils/ShoppingListCustomUpdateActionUtilsTest.java b/src/test/java/com/commercetools/sync/commons/utils/ShoppingListCustomUpdateActionUtilsTest.java index f49d5d197f..2b6f3f605e 100644 --- a/src/test/java/com/commercetools/sync/commons/utils/ShoppingListCustomUpdateActionUtilsTest.java +++ b/src/test/java/com/commercetools/sync/commons/utils/ShoppingListCustomUpdateActionUtilsTest.java @@ -3,16 +3,20 @@ import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; import com.commercetools.sync.shoppinglists.utils.LineItemCustomActionBuilder; import com.commercetools.sync.shoppinglists.utils.ShoppingListCustomActionBuilder; +import com.commercetools.sync.shoppinglists.utils.TextLineItemCustomActionBuilder; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import io.sphere.sdk.client.SphereClient; import io.sphere.sdk.commands.UpdateAction; import io.sphere.sdk.shoppinglists.LineItem; import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.TextLineItem; import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomField; import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomType; import io.sphere.sdk.shoppinglists.commands.updateactions.SetLineItemCustomField; import io.sphere.sdk.shoppinglists.commands.updateactions.SetLineItemCustomType; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetTextLineItemCustomField; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetTextLineItemCustomType; import org.junit.jupiter.api.Test; import java.util.HashMap; @@ -111,4 +115,52 @@ void buildSetLineItemCustomFieldAction_WithLineItemResource_ShouldBuildShoppingL .hasValues("setLineItemCustomField", customFieldName, customFieldValue); assertThat(((SetLineItemCustomField) updateAction).getLineItemId()).isEqualTo("line_item_id"); } + + @Test + void buildTypedSetTextLineItemCustomTypeUpdateAction_WithTextLineItemRes_ShouldBuildShoppingListUpdateAction() { + final TextLineItem textLineItem = mock(TextLineItem.class); + when(textLineItem.getId()).thenReturn("text_line_item_id"); + + final String newCustomTypeId = UUID.randomUUID().toString(); + + final UpdateAction updateAction = + GenericUpdateActionUtils.buildTypedSetCustomTypeUpdateAction(newCustomTypeId, new HashMap<>(), + textLineItem::getCustom, + new TextLineItemCustomActionBuilder(), -1, t -> textLineItem.getId(), + textLineItemResource -> TextLineItem.resourceTypeId(), t -> textLineItem.getId(), + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)).build()).orElse(null); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction).isInstanceOf(SetTextLineItemCustomType.class); + assertThat((SetTextLineItemCustomType) updateAction) + .hasValues("setTextLineItemCustomType", emptyMap(), ofId(newCustomTypeId)); + assertThat(((SetTextLineItemCustomType) updateAction).getTextLineItemId()).isEqualTo("text_line_item_id"); + } + + @Test + void buildRemoveTextLineItemCustomTypeAction_WithTextLineItemResource_ShouldBuildShoppingListUpdateAction() { + final UpdateAction updateAction = new TextLineItemCustomActionBuilder() + .buildRemoveCustomTypeAction(-1, "text_line_item_id"); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction).isInstanceOf(SetTextLineItemCustomType.class); + assertThat((SetTextLineItemCustomType) updateAction) + .hasValues("setTextLineItemCustomType", null, ofId(null)); + assertThat(((SetTextLineItemCustomType) updateAction).getTextLineItemId()).isEqualTo("text_line_item_id"); + } + + @Test + void buildSetTextLineItemCustomFieldAction_WithTextLineItemResource_ShouldBuildShoppingListUpdateAction() { + final JsonNode customFieldValue = JsonNodeFactory.instance.textNode("foo"); + final String customFieldName = "name"; + + final UpdateAction updateAction = new TextLineItemCustomActionBuilder() + .buildSetCustomFieldAction(-1, "text_line_item_id", customFieldName, customFieldValue); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction).isInstanceOf(SetTextLineItemCustomField.class); + assertThat((SetTextLineItemCustomField) updateAction) + .hasValues("setTextLineItemCustomField", customFieldName, customFieldValue); + assertThat(((SetTextLineItemCustomField) updateAction).getTextLineItemId()).isEqualTo("text_line_item_id"); + } } diff --git a/src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncTest.java b/src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncTest.java index 02e76412d9..2cd9925712 100644 --- a/src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncTest.java +++ b/src/test/java/com/commercetools/sync/shoppinglists/ShoppingListSyncTest.java @@ -24,7 +24,6 @@ import io.sphere.sdk.shoppinglists.TextLineItemDraftBuilder; import io.sphere.sdk.types.CustomFieldsDraft; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.ArrayList; @@ -407,20 +406,22 @@ void sync_WithUnchangedShoppingListDraftAndUpdatedLineItemDraft_ShouldIncrementU verify(spySyncOptions, never()).applyBeforeCreateCallback(shoppingListDraft); } - // TODO - Enable the test case when TextLineItem is available in UpdateActionUtils - @Disabled @Test void sync_WithUnchangedShoppingListDraftAndUpdatedTextLineItemDraft_ShouldIncrementUpdated() { // preparation - final ShoppingListService mockShoppingListService = mock(ShoppingListService.class); final ShoppingList mockShoppingList = mock(ShoppingList.class); - final TextLineItem mockTextLineItem = mock(TextLineItem.class); when(mockShoppingList.getKey()).thenReturn("shoppingListKey"); when(mockShoppingList.getName()).thenReturn(LocalizedString.ofEnglish("shoppingListName")); + + final TextLineItem mockTextLineItem = mock(TextLineItem.class); when(mockTextLineItem.getName()).thenReturn(LocalizedString.ofEnglish("textLineItemName")); when(mockTextLineItem.getQuantity()).thenReturn(10L); + + final ShoppingListService mockShoppingListService = mock(ShoppingListService.class); when(mockShoppingListService.fetchMatchingShoppingListsByKeys(anySet())) .thenReturn(completedFuture(singleton(mockShoppingList))); + when(mockShoppingListService.updateShoppingList(any(), anyList())) + .thenReturn(completedFuture(mockShoppingList)); final ShoppingListSyncOptions spySyncOptions = spy(syncOptions); final ShoppingListSync shoppingListSync = new ShoppingListSync(spySyncOptions, mockShoppingListService, diff --git a/src/test/java/com/commercetools/sync/shoppinglists/utils/LineItemListUpdateActionUtilsTest.java b/src/test/java/com/commercetools/sync/shoppinglists/utils/LineItemListUpdateActionUtilsTest.java index 468dc91e8a..8c8e6432e8 100644 --- a/src/test/java/com/commercetools/sync/shoppinglists/utils/LineItemListUpdateActionUtilsTest.java +++ b/src/test/java/com/commercetools/sync/shoppinglists/utils/LineItemListUpdateActionUtilsTest.java @@ -31,6 +31,8 @@ import javax.annotation.Nonnull; import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -39,13 +41,13 @@ import static com.commercetools.sync.shoppinglists.utils.LineItemUpdateActionUtils.buildLineItemsUpdateActions; import static com.commercetools.sync.shoppinglists.utils.ShoppingListSyncUtils.buildActions; import static io.sphere.sdk.json.SphereJsonUtils.readObjectFromResource; +import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static java.util.concurrent.CompletableFuture.completedFuture; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -77,10 +79,6 @@ class LineItemListUpdateActionUtilsTest { RES_ROOT + "shoppinglist-with-lineitems-sku-132-with-changes.json"; private static final String SHOPPING_LIST_WITH_LINE_ITEMS_NOT_EXPANDED = RES_ROOT + "shoppinglist-with-lineitems-not-expanded.json"; - private static final String SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123_WITH_DIFFERENT_ADDED_AT = - RES_ROOT + "shoppinglist-with-lineitems-sku-123-with-different-addedAt.json"; - private static final String SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123_WITHOUT_ADDED_AT = - RES_ROOT + "shoppinglist-with-lineitems-sku-123-without-addedAt.json"; private static final ShoppingListSyncOptions SYNC_OPTIONS = ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)).build(); @@ -194,6 +192,27 @@ void buildLineItemsUpdateActions_WithNewLineItemsAndNoOldLineItems_ShouldBuild3A ); } + @Test + void buildLineItemsUpdateActions_WithOnlyNewLineItemsWithoutValidQuantity_ShouldNotBuildAddActions() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getLineItems()).thenReturn(null); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder + .of(LocalizedString.ofEnglish("name")) + .lineItems(asList( + LineItemDraftBuilder.ofSku("sku1", null).build(), + LineItemDraftBuilder.ofSku("sku2", 0L).build(), + LineItemDraftBuilder.ofSku("sku3", -1L).build() + )) + .build(); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + @Test void buildLineItemsUpdateActions_WithIdenticalLineItems_ShouldNotBuildUpdateActions() { final ShoppingList oldShoppingList = @@ -424,63 +443,89 @@ void buildLineItemsUpdateAction_WithAddedRemovedAndDifOrderAndChangedLineItem_Sh } @Test - void buildLineItemsUpdateAction_WithNotExpandedLineItem_ShouldThrowIllegalArgumentException() { + void buildLineItemsUpdateAction_WithNotExpandedLineItem_ShouldTriggerErrorCallback() { final ShoppingList oldShoppingList = readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_NOT_EXPANDED, ShoppingList.class); final ShoppingListDraft newShoppingList = mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_132_WITH_CHANGES); - assertThatThrownBy(() -> buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS)) - .isExactlyInstanceOf(IllegalArgumentException.class) - .hasMessage("LineItem at position '1' of the ShoppingList with key " + + final List errors = new ArrayList<>(); + final List> updateActionsBeforeCallback = new ArrayList<>(); + + final ShoppingListSyncOptions syncOptions = + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)) + .errorCallback((exception, oldResource, newResource, updateActions) -> { + errors.add(exception.getMessage()); + updateActionsBeforeCallback.addAll(updateActions); + }) + .build(); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, syncOptions); + + assertThat(updateActions).isEmpty(); + assertThat(updateActionsBeforeCallback).containsExactly( + ChangeLineItemQuantity.of("line_item_id_1", 2L), + SetLineItemCustomField.ofJson("textField", + JsonNodeFactory.instance.textNode("newText1"), "line_item_id_1")); + + assertThat(errors).hasSize(1); + assertThat(errors.get(0)) + .isEqualTo("LineItem at position '1' of the ShoppingList with key " + "'shoppinglist-with-lineitems-not-expanded' has no SKU set. " - + "Please make sure all line items have SKUs"); + + "Please make sure all line items have SKUs."); } @Test - void buildLineItemsUpdateAction_WithLineItemWithoutSku_ShouldThrowIllegalArgumentException() { + void buildLineItemsUpdateAction_WithLineItemWithoutSku_ShouldTriggerErrorCallback() { final ShoppingList oldShoppingList = readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_132_WITH_CHANGES, ShoppingList.class); final ShoppingListDraft newShoppingList = mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_NOT_EXPANDED); - assertThatThrownBy(() -> buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS)) - .isExactlyInstanceOf(IllegalArgumentException.class) - .hasMessage("LineItemDraft at position '1' of the ShoppingListDraft with key " - + "'shoppinglist-with-lineitems-not-expanded' has no SKU set. " - + "Please make sure all line items have SKUs"); - } - - @Test - void buildLineItemsUpdateActions_WithIdenticalLineItemDraftsWithUpdatedAddedAt_ShouldBuildRemoveAndAddActions() { - final ShoppingList oldShoppingList = - readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); + final List errors = new ArrayList<>(); + final List> updateActionsBeforeCallback = new ArrayList<>(); - final ShoppingListDraft newShoppingList = mapToShoppingListDraftWithResolvedLineItemReferences( - SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123_WITH_DIFFERENT_ADDED_AT); + final ShoppingListSyncOptions syncOptions = + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)) + .errorCallback((exception, oldResource, newResource, updateActions) -> { + errors.add(exception.getMessage()); + updateActionsBeforeCallback.addAll(updateActions); + }) + .build(); final List> updateActions = - buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, syncOptions); - assertThat(updateActions).containsExactly( - RemoveLineItem.of("line_item_id_1"), - RemoveLineItem.of("line_item_id_2"), - RemoveLineItem.of("line_item_id_3"), - AddLineItemWithSku.of(newShoppingList.getLineItems().get(0)), // SKU-1 - AddLineItemWithSku.of(newShoppingList.getLineItems().get(1)), // SKU-2 - AddLineItemWithSku.of(newShoppingList.getLineItems().get(2)) // SKU-3 - ); + assertThat(updateActions).isEmpty(); + assertThat(updateActionsBeforeCallback).containsExactly( + ChangeLineItemQuantity.of("line_item_id_1", 1L), + SetLineItemCustomField.ofJson("textField", + JsonNodeFactory.instance.textNode("text1"), "line_item_id_1")); + + assertThat(errors).hasSize(1); + assertThat(errors.get(0)) + .isEqualTo("LineItemDraft at position '1' of the ShoppingListDraft with key " + + "'shoppinglist-with-lineitems-not-expanded' has no SKU set. " + + "Please make sure all line items have SKUs."); } @Test - void buildLineItemsUpdateActions_WithIdenticalLineItemDraftsWithoutAddedAtValues_ShouldNotBuildActions() { + void buildLineItemsUpdateActions_WithNewLineItemsWithoutValidQuantity_ShouldNotBuildAddActions() { final ShoppingList oldShoppingList = readObjectFromResource(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123, ShoppingList.class); - final ShoppingListDraft newShoppingList = mapToShoppingListDraftWithResolvedLineItemReferences( - SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123_WITHOUT_ADDED_AT); + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_123); + + Objects.requireNonNull(newShoppingList.getLineItems()).addAll(Arrays.asList( + LineItemDraftBuilder.ofSku("sku1", null).build(), + LineItemDraftBuilder.ofSku("sku2", 0L).build(), + LineItemDraftBuilder.ofSku("sku3", -1L).build() + )); final List> updateActions = buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); @@ -496,7 +541,6 @@ void buildActions_WithDifferentValuesWithLineItems_ShouldReturnActions() { final ShoppingListDraft newShoppingList = mapToShoppingListDraftWithResolvedLineItemReferences(SHOPPING_LIST_WITH_LINE_ITEMS_SKU_132_WITH_CHANGES); - final List> updateActions = buildActions(oldShoppingList, newShoppingList, mock(ShoppingListSyncOptions.class)); diff --git a/src/test/java/com/commercetools/sync/shoppinglists/utils/TextLineItemListUpdateActionUtilsTest.java b/src/test/java/com/commercetools/sync/shoppinglists/utils/TextLineItemListUpdateActionUtilsTest.java new file mode 100644 index 0000000000..6649fc47fa --- /dev/null +++ b/src/test/java/com/commercetools/sync/shoppinglists/utils/TextLineItemListUpdateActionUtilsTest.java @@ -0,0 +1,470 @@ +package com.commercetools.sync.shoppinglists.utils; + +import com.commercetools.sync.services.TypeService; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; +import com.commercetools.sync.shoppinglists.commands.updateactions.AddTextLineItemWithAddedAt; +import com.commercetools.sync.shoppinglists.helpers.TextLineItemReferenceResolver; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.customers.Customer; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.models.Reference; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.ShoppingListDraftBuilder; +import io.sphere.sdk.shoppinglists.TextLineItemDraft; +import io.sphere.sdk.shoppinglists.TextLineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeName; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeTextLineItemName; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeTextLineItemQuantity; +import io.sphere.sdk.shoppinglists.commands.updateactions.RemoveTextLineItem; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetAnonymousId; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomField; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetCustomer; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetDeleteDaysAfterLastModification; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetDescription; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetSlug; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetTextLineItemCustomField; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetTextLineItemDescription; +import io.sphere.sdk.types.CustomFieldsDraft; +import org.junit.jupiter.api.Test; + +import javax.annotation.Nonnull; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; + +import static com.commercetools.sync.commons.utils.CompletableFutureUtils.mapValuesToFutureOfCompletedValues; +import static com.commercetools.sync.shoppinglists.utils.LineItemUpdateActionUtils.buildLineItemsUpdateActions; +import static com.commercetools.sync.shoppinglists.utils.ShoppingListSyncUtils.buildActions; +import static com.commercetools.sync.shoppinglists.utils.TextLineItemUpdateActionUtils.buildTextLineItemsUpdateActions; +import static io.sphere.sdk.json.SphereJsonUtils.readObjectFromResource; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TextLineItemListUpdateActionUtilsTest { + + private static final String RES_ROOT = "com/commercetools/sync/shoppinglists/utils/textlineitems/"; + private static final String SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123 = + RES_ROOT + "shoppinglist-with-textlineitems-name-123.json"; + private static final String SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123_WITH_CHANGES = + RES_ROOT + "shoppinglist-with-textlineitems-name-123-with-changes.json"; + private static final String SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_12 = + RES_ROOT + "shoppinglist-with-textlineitems-name-12.json"; + private static final String SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_1234 = + RES_ROOT + "shoppinglist-with-textlineitems-name-1234.json"; + + private static final ShoppingListSyncOptions SYNC_OPTIONS = + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)).build(); + + private static final TextLineItemReferenceResolver textLineItemReferenceResolver = + new TextLineItemReferenceResolver(SYNC_OPTIONS, getMockTypeService()); + + @Test + void buildTextLineItemsUpdateActions_WithoutNewAndWithNullOldTextLineItems_ShouldNotBuildActions() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getTextLineItems()).thenReturn(emptyList()); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("name")) + .build(); + + final List> updateActions = + buildTextLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildTextLineItemsUpdateActions_WithNullNewAndNullOldTextLineItems_ShouldNotBuildActions() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getTextLineItems()).thenReturn(null); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("name")) + .build(); + + final List> updateActions = + buildTextLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildTextLineItemsUpdateActions_WithNullOldAndEmptyNewTextLineItems_ShouldNotBuildActions() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getTextLineItems()).thenReturn(null); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("name")) + .textLineItems(emptyList()) + .build(); + + final List> updateActions = + buildTextLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildTextLineItemsUpdateActions_WithNullOldAndNewTextLineItemsWithNullElement_ShouldNotBuildActions() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getTextLineItems()).thenReturn(null); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("name")) + .textLineItems(singletonList(null)) + .build(); + + final List> updateActions = + buildTextLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildTextLineItemsUpdateActions_WithNullNewLineItemsAndExistingLineItems_ShouldBuild3RemoveActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("name")) + .build(); + + final List> updateActions = + buildTextLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + RemoveTextLineItem.of("text_line_item_id_1"), + RemoveTextLineItem.of("text_line_item_id_2"), + RemoveTextLineItem.of("text_line_item_id_3")); + } + + @Test + void buildTextLineItemsUpdateActions_WithNewTextLineItemsAndNoOldTextLineItems_ShouldBuild3AddActions() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getTextLineItems()).thenReturn(null); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedTextLineItemReferences(SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123); + + final List> updateActions = + buildTextLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(newShoppingList.getTextLineItems()).isNotNull(); + assertThat(updateActions).containsExactly( + AddTextLineItemWithAddedAt.of(newShoppingList.getTextLineItems().get(0)), + AddTextLineItemWithAddedAt.of(newShoppingList.getTextLineItems().get(1)), + AddTextLineItemWithAddedAt.of(newShoppingList.getTextLineItems().get(2)) + ); + } + + @Test + void buildTextLineItemsUpdateActions_WithOnlyNewTextLineItemsWithoutValidQuantity_ShouldNotBuildAddActions() { + final ShoppingList oldShoppingList = mock(ShoppingList.class); + when(oldShoppingList.getTextLineItems()).thenReturn(null); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder + .of(LocalizedString.ofEnglish("name")) + .textLineItems(asList( + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name1"), null).build(), + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name2"), 0L).build(), + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name3"), -1L).build() + )) + .build(); + + final List> updateActions = + buildTextLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildLineItemsUpdateActions_WithIdenticalTextLineItems_ShouldNotBuildUpdateActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedTextLineItemReferences(SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123); + + final List> updateActions = + buildLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildLineItemsUpdateActions_WithAllDifferentFields_ShouldBuildUpdateActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = mapToShoppingListDraftWithResolvedTextLineItemReferences( + SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123_WITH_CHANGES); + + final List> updateActions = + buildTextLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + ChangeTextLineItemName.of("text_line_item_id_1", + LocalizedString.ofEnglish("newName1-EN").plus(Locale.GERMAN, "newName1-DE")), + SetTextLineItemDescription.of("text_line_item_id_1").withDescription( + LocalizedString.ofEnglish("newDesc1-EN").plus(Locale.GERMAN, "newDesc1-DE")), + ChangeTextLineItemQuantity.of("text_line_item_id_1", 2L), + SetTextLineItemCustomField.ofJson("textField", + JsonNodeFactory.instance.textNode("newText1"), "text_line_item_id_1"), + + ChangeTextLineItemName.of("text_line_item_id_2", + LocalizedString.ofEnglish("newName2-EN").plus(Locale.GERMAN, "newName2-DE")), + SetTextLineItemDescription.of("text_line_item_id_2").withDescription( + LocalizedString.ofEnglish("newDesc2-EN").plus(Locale.GERMAN, "newDesc2-DE")), + ChangeTextLineItemQuantity.of("text_line_item_id_2", 4L), + SetTextLineItemCustomField.ofJson("textField", + JsonNodeFactory.instance.textNode("newText2"), "text_line_item_id_2"), + + ChangeTextLineItemName.of("text_line_item_id_3", + LocalizedString.ofEnglish("newName3-EN").plus(Locale.GERMAN, "newName3-DE")), + SetTextLineItemDescription.of("text_line_item_id_3").withDescription( + LocalizedString.ofEnglish("newDesc3-EN").plus(Locale.GERMAN, "newDesc3-DE")), + ChangeTextLineItemQuantity.of("text_line_item_id_3", 6L), + SetTextLineItemCustomField.ofJson("textField", + JsonNodeFactory.instance.textNode("newText3"), "text_line_item_id_3") + ); + } + + @Test + void buildTextLineItemsUpdateActions_WithOneMissingTextLineItem_ShouldBuildOneRemoveTextLineItemAction() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedTextLineItemReferences(SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_12); + + final List> updateActions = + buildTextLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + RemoveTextLineItem.of("text_line_item_id_3") + ); + } + + @Test + void buildTextLineItemsUpdateActions_WithOneExtraTextLineItem_ShouldBuildAddTextLineItemAction() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedTextLineItemReferences(SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_1234); + + final List> updateActions = + buildTextLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + final TextLineItemDraft expectedLineItemDraft = + TextLineItemDraftBuilder + .of(LocalizedString.ofEnglish("name4-EN").plus(Locale.GERMAN, "name4-DE"), 4L) + .description(LocalizedString.ofEnglish("desc4-EN").plus(Locale.GERMAN, "desc4-DE")) + .custom(CustomFieldsDraft.ofTypeIdAndJson("custom_type_id", + singletonMap("textField", + JsonNodeFactory.instance.textNode("text4")))) + .addedAt(ZonedDateTime.parse("2020-11-06T10:00:00.000Z")) + .build(); + + assertThat(updateActions).containsExactly( + AddTextLineItemWithAddedAt.of(expectedLineItemDraft) + ); + } + + @Test + void buildTextLineItemsUpdateAction_WithTextLineItemWithNullName_ShouldTriggerErrorCallback() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123, ShoppingList.class); + + final TextLineItemDraft updatedTextLineItemDraft1 = TextLineItemDraftBuilder + .of(LocalizedString.ofEnglish("newName1-EN").plus(Locale.GERMAN, "newName1-DE"), 2L) + .description(LocalizedString.ofEnglish("newDesc1-EN").plus(Locale.GERMAN, "newDesc1-DE")) + .custom(CustomFieldsDraft.ofTypeIdAndJson("custom_type_id", + singletonMap("textField", + JsonNodeFactory.instance.textNode("newText1")))) + .build(); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("shoppinglist")) + .key("key") + .textLineItems(asList( + updatedTextLineItemDraft1, + TextLineItemDraftBuilder.of(null, 1L).build())) + .build(); + + final List errors = new ArrayList<>(); + final List> updateActionsBeforeCallback = new ArrayList<>(); + + final ShoppingListSyncOptions syncOptions = + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)) + .errorCallback((exception, oldResource, newResource, updateActions) -> { + errors.add(exception.getMessage()); + updateActionsBeforeCallback.addAll(updateActions); + }) + .build(); + + final List> updateActions = + buildTextLineItemsUpdateActions(oldShoppingList, newShoppingList, syncOptions); + + assertThat(updateActions).isEmpty(); + assertThat(updateActionsBeforeCallback).containsExactly( + ChangeTextLineItemName.of("text_line_item_id_1", + LocalizedString.ofEnglish("newName1-EN").plus(Locale.GERMAN, "newName1-DE")), + SetTextLineItemDescription.of("text_line_item_id_1").withDescription( + LocalizedString.ofEnglish("newDesc1-EN").plus(Locale.GERMAN, "newDesc1-DE")), + ChangeTextLineItemQuantity.of("text_line_item_id_1", 2L), + SetTextLineItemCustomField.ofJson("textField", + JsonNodeFactory.instance.textNode("newText1"), "text_line_item_id_1")); + + assertThat(errors).hasSize(1); + assertThat(errors.get(0)) + .isEqualTo("TextLineItemDraft at position '1' of the ShoppingListDraft with key " + + "'key' has no name set. Please make sure all text line items have names."); + } + + @Test + void buildTextLineItemsUpdateAction_WithTextLineItemWithoutName_ShouldTriggerErrorCallback() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + ShoppingListDraftBuilder.of(LocalizedString.ofEnglish("shoppinglist")) + .key("key") + .textLineItems(singletonList( + TextLineItemDraftBuilder.of(LocalizedString.of(), 1L).build())) + .build(); + + final List errors = new ArrayList<>(); + final List> updateActionsBeforeCallback = new ArrayList<>(); + + final ShoppingListSyncOptions syncOptions = + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)) + .errorCallback((exception, oldResource, newResource, updateActions) -> { + errors.add(exception.getMessage()); + updateActionsBeforeCallback.addAll(updateActions); + }) + .build(); + + final List> updateActions = + buildTextLineItemsUpdateActions(oldShoppingList, newShoppingList, syncOptions); + + assertThat(updateActions).isEmpty(); + assertThat(updateActionsBeforeCallback).isEmpty(); + + assertThat(errors).hasSize(1); + assertThat(errors.get(0)) + .isEqualTo("TextLineItemDraft at position '0' of the ShoppingListDraft with key " + + "'key' has no name set. Please make sure all text line items have names."); + } + + @Test + void buildTextLineItemsUpdateActions_WithNewTextLineItemsWithoutValidQuantity_ShouldNotBuildAddActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedTextLineItemReferences(SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123); + + Objects.requireNonNull(newShoppingList.getTextLineItems()).addAll(Arrays.asList( + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name1"), null).build(), + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name2"), 0L).build(), + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name3"), -1L).build() + )); + + final List> updateActions = + buildTextLineItemsUpdateActions(oldShoppingList, newShoppingList, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildActions_WithDifferentValuesWithTextLineItems_ShouldReturnActions() { + final ShoppingList oldShoppingList = + readObjectFromResource(SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123, ShoppingList.class); + + final ShoppingListDraft newShoppingList = + mapToShoppingListDraftWithResolvedTextLineItemReferences( + SHOPPING_LIST_WITH_TEXT_LINE_ITEMS_NAME_123_WITH_CHANGES); + + final List> updateActions = + buildActions(oldShoppingList, newShoppingList, mock(ShoppingListSyncOptions.class)); + + assertThat(updateActions).containsExactly( + SetSlug.of(LocalizedString.ofEnglish("newSlug")), + ChangeName.of(LocalizedString.ofEnglish("newName")), + SetDescription.of(LocalizedString.ofEnglish("newDescription")), + SetCustomer.of(Reference.of(Customer.referenceTypeId(), "customer_id_2")), + SetAnonymousId.of("newAnonymousId"), + SetDeleteDaysAfterLastModification.of(45), + SetCustomField.ofJson("textField", JsonNodeFactory.instance.textNode("newTextValue")), + + ChangeTextLineItemName.of("text_line_item_id_1", + LocalizedString.ofEnglish("newName1-EN").plus(Locale.GERMAN, "newName1-DE")), + SetTextLineItemDescription.of("text_line_item_id_1").withDescription( + LocalizedString.ofEnglish("newDesc1-EN").plus(Locale.GERMAN, "newDesc1-DE")), + ChangeTextLineItemQuantity.of("text_line_item_id_1", 2L), + SetTextLineItemCustomField.ofJson("textField", + JsonNodeFactory.instance.textNode("newText1"), "text_line_item_id_1"), + + ChangeTextLineItemName.of("text_line_item_id_2", + LocalizedString.ofEnglish("newName2-EN").plus(Locale.GERMAN, "newName2-DE")), + SetTextLineItemDescription.of("text_line_item_id_2").withDescription( + LocalizedString.ofEnglish("newDesc2-EN").plus(Locale.GERMAN, "newDesc2-DE")), + ChangeTextLineItemQuantity.of("text_line_item_id_2", 4L), + SetTextLineItemCustomField.ofJson("textField", + JsonNodeFactory.instance.textNode("newText2"), "text_line_item_id_2"), + + ChangeTextLineItemName.of("text_line_item_id_3", + LocalizedString.ofEnglish("newName3-EN").plus(Locale.GERMAN, "newName3-DE")), + SetTextLineItemDescription.of("text_line_item_id_3").withDescription( + LocalizedString.ofEnglish("newDesc3-EN").plus(Locale.GERMAN, "newDesc3-DE")), + ChangeTextLineItemQuantity.of("text_line_item_id_3", 6L), + SetTextLineItemCustomField.ofJson("textField", + JsonNodeFactory.instance.textNode("newText3"), "text_line_item_id_3") + ); + } + + @Nonnull + private static ShoppingListDraft mapToShoppingListDraftWithResolvedTextLineItemReferences( + @Nonnull final String resourcePath) { + + final ShoppingListDraft template = + ShoppingListReferenceResolutionUtils.mapToShoppingListDraft( + readObjectFromResource(resourcePath, ShoppingList.class)); + + final ShoppingListDraftBuilder builder = ShoppingListDraftBuilder.of(template); + + mapValuesToFutureOfCompletedValues( + Objects.requireNonNull(builder.getTextLineItems()), + textLineItemReferenceResolver::resolveReferences, toList()) + .thenApply(builder::textLineItems) + .join(); + + return builder.build(); + } + + @Nonnull + private static TypeService getMockTypeService() { + final TypeService typeService = mock(TypeService.class); + when(typeService.fetchCachedTypeId(anyString())) + .thenReturn(completedFuture(Optional.of("custom_type_id"))); + return typeService; + } +} diff --git a/src/test/java/com/commercetools/sync/shoppinglists/utils/TextLineItemUpdateActionUtilsTest.java b/src/test/java/com/commercetools/sync/shoppinglists/utils/TextLineItemUpdateActionUtilsTest.java new file mode 100644 index 0000000000..f9fb2a4154 --- /dev/null +++ b/src/test/java/com/commercetools/sync/shoppinglists/utils/TextLineItemUpdateActionUtilsTest.java @@ -0,0 +1,576 @@ +package com.commercetools.sync.shoppinglists.utils; + +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptions; +import com.commercetools.sync.shoppinglists.ShoppingListSyncOptionsBuilder; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import io.sphere.sdk.client.SphereClient; +import io.sphere.sdk.commands.UpdateAction; +import io.sphere.sdk.models.LocalizedString; +import io.sphere.sdk.shoppinglists.ShoppingList; +import io.sphere.sdk.shoppinglists.ShoppingListDraft; +import io.sphere.sdk.shoppinglists.TextLineItem; +import io.sphere.sdk.shoppinglists.TextLineItemDraft; +import io.sphere.sdk.shoppinglists.TextLineItemDraftBuilder; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeTextLineItemName; +import io.sphere.sdk.shoppinglists.commands.updateactions.ChangeTextLineItemQuantity; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetTextLineItemCustomField; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetTextLineItemCustomType; +import io.sphere.sdk.shoppinglists.commands.updateactions.SetTextLineItemDescription; +import io.sphere.sdk.types.CustomFields; +import io.sphere.sdk.types.CustomFieldsDraft; +import io.sphere.sdk.types.Type; +import org.junit.jupiter.api.Test; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static com.commercetools.sync.shoppinglists.utils.TextLineItemUpdateActionUtils.buildChangeTextLineItemNameUpdateAction; +import static com.commercetools.sync.shoppinglists.utils.TextLineItemUpdateActionUtils.buildChangeTextLineItemQuantityUpdateAction; +import static com.commercetools.sync.shoppinglists.utils.TextLineItemUpdateActionUtils.buildSetTextLineItemDescriptionUpdateAction; +import static com.commercetools.sync.shoppinglists.utils.TextLineItemUpdateActionUtils.buildTextLineItemCustomUpdateActions; +import static com.commercetools.sync.shoppinglists.utils.TextLineItemUpdateActionUtils.buildTextLineItemUpdateActions; +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TextLineItemUpdateActionUtilsTest { + + private static final ShoppingListSyncOptions SYNC_OPTIONS = + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)).build(); + + final ShoppingList oldShoppingList = mock(ShoppingList.class); + final ShoppingListDraft newShoppingList = mock(ShoppingListDraft.class); + + @Test + void buildTextLineItemCustomUpdateActions_WithSameValues_ShouldNotBuildUpdateAction() { + final Map oldCustomFieldsMap = new HashMap<>(); + oldCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(true)); + oldCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("de", "val")); + + final CustomFields oldCustomFields = mock(CustomFields.class); + when(oldCustomFields.getType()).thenReturn(Type.referenceOfId("1")); + when(oldCustomFields.getFieldsJsonMap()).thenReturn(oldCustomFieldsMap); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson("1", oldCustomFieldsMap); + + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getCustom()).thenReturn(oldCustomFields); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name"), 1L) + .description(LocalizedString.ofEnglish("desc")) + .addedAt(ZonedDateTime.now()) + .custom(newCustomFieldsDraft) + .build(); + + final List> updateActions = buildTextLineItemCustomUpdateActions( + oldShoppingList, newShoppingList, oldTextLineItem, newTextLineItem, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildTextLineItemCustomUpdateActions_WithDifferentValues_ShouldBuildUpdateAction() { + final Map oldCustomFieldsMap = new HashMap<>(); + oldCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(true)); + oldCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("de", "val1")); + + final Map newCustomFieldsMap = new HashMap<>(); + newCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(false)); + newCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("es", "val2")); + + final CustomFields oldCustomFields = mock(CustomFields.class); + when(oldCustomFields.getType()).thenReturn(Type.referenceOfId("1")); + when(oldCustomFields.getFieldsJsonMap()).thenReturn(oldCustomFieldsMap); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson("1", newCustomFieldsMap); + + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getCustom()).thenReturn(oldCustomFields); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name"), 1L) + .description(LocalizedString.ofEnglish("desc")) + .addedAt(ZonedDateTime.now()) + .custom(newCustomFieldsDraft) + .build(); + + final List> updateActions = buildTextLineItemCustomUpdateActions( + oldShoppingList, newShoppingList, oldTextLineItem, newTextLineItem, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + SetTextLineItemCustomField.ofJson("field1", + JsonNodeFactory.instance.booleanNode(false), "text_line_item_id"), + SetTextLineItemCustomField.ofJson("field2", + JsonNodeFactory.instance.objectNode().put("es", "val2"), "text_line_item_id") + ); + } + + @Test + void buildTextLineItemCustomUpdateActions_WithNullOldValues_ShouldBuildUpdateAction() { + final Map newCustomFieldsMap = new HashMap<>(); + newCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(false)); + newCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("es", "val")); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson("1", newCustomFieldsMap); + + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getCustom()).thenReturn(null); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name"), 1L) + .description(LocalizedString.ofEnglish("desc")) + .addedAt(ZonedDateTime.now()) + .custom(newCustomFieldsDraft) + .build(); + + final List> updateActions = buildTextLineItemCustomUpdateActions( + oldShoppingList, newShoppingList, oldTextLineItem, newTextLineItem, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + SetTextLineItemCustomType.ofTypeIdAndJson("1", newCustomFieldsMap, "text_line_item_id") + ); + } + + @Test + void buildTextLineItemCustomUpdateActions_WithBadCustomFieldData_ShouldNotBuildUpdateActionAndTriggerCallback() { + final Map oldCustomFieldsMap = new HashMap<>(); + oldCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(true)); + oldCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("de", "val1")); + + final Map newCustomFieldsMap = new HashMap<>(); + newCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(false)); + newCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("es", "val2")); + + + final CustomFields oldCustomFields = mock(CustomFields.class); + when(oldCustomFields.getType()).thenReturn(Type.referenceOfId("")); + when(oldCustomFields.getFieldsJsonMap()).thenReturn(oldCustomFieldsMap); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson("", newCustomFieldsMap); + + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getCustom()).thenReturn(oldCustomFields); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name"), 1L) + .description(LocalizedString.ofEnglish("desc")) + .addedAt(ZonedDateTime.now()) + .custom(newCustomFieldsDraft) + .build(); + + final List errors = new ArrayList<>(); + + final ShoppingListSyncOptions syncOptions = + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)) + .errorCallback((exception, oldResource, newResource, updateActions) -> + errors.add(exception.getMessage())) + .build(); + + final List> updateActions = buildTextLineItemCustomUpdateActions( + oldShoppingList, newShoppingList, oldTextLineItem, newTextLineItem, syncOptions); + + assertThat(updateActions).isEmpty(); + assertThat(errors).hasSize(1); + assertThat(errors.get(0)) + .isEqualTo(format("Failed to build custom fields update actions on the shopping-list-text-line-item with " + + "id '%s'. Reason: Custom type ids are not set for both the old " + + "and new shopping-list-text-line-item.", oldTextLineItem.getId())); + } + + @Test + void buildTextLineItemCustomUpdateActions_WithNullValue_ShouldCorrectlyBuildAction() { + final Map oldCustomFieldsMap = new HashMap<>(); + oldCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(true)); + oldCustomFieldsMap.put("field2", JsonNodeFactory + .instance + .arrayNode() + .add(JsonNodeFactory.instance.booleanNode(false))); + + final Map newCustomFieldsMap = new HashMap<>(); + newCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(true)); + newCustomFieldsMap.put("field2", null); + + + final CustomFields oldCustomFields = mock(CustomFields.class); + final String typeId = UUID.randomUUID().toString(); + when(oldCustomFields.getType()).thenReturn(Type.referenceOfId(typeId)); + when(oldCustomFields.getFieldsJsonMap()).thenReturn(oldCustomFieldsMap); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson(typeId, newCustomFieldsMap); + + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getCustom()).thenReturn(oldCustomFields); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name"), 1L) + .description(LocalizedString.ofEnglish("desc")) + .addedAt(ZonedDateTime.now()) + .custom(newCustomFieldsDraft) + .build(); + + final List errors = new ArrayList<>(); + + final ShoppingListSyncOptions syncOptions = + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)) + .errorCallback((exception, oldResource, newResource, updateActions) -> + errors.add(exception.getMessage())) + .build(); + + final List> updateActions = buildTextLineItemCustomUpdateActions( + oldShoppingList, newShoppingList, oldTextLineItem, newTextLineItem, syncOptions); + + assertThat(errors).isEmpty(); + assertThat(updateActions) + .containsExactly(SetTextLineItemCustomField.ofJson("field2", null, "text_line_item_id")); + } + + @Test + void buildTextLineItemCustomUpdateActions_WithNullJsonNodeValue_ShouldCorrectlyBuildAction() { + final Map oldCustomFieldsMap = new HashMap<>(); + oldCustomFieldsMap.put("field", JsonNodeFactory + .instance + .arrayNode() + .add(JsonNodeFactory.instance.booleanNode(false))); + + final Map newCustomFieldsMap = new HashMap<>(); + newCustomFieldsMap.put("field", JsonNodeFactory.instance.nullNode()); + + + final CustomFields oldCustomFields = mock(CustomFields.class); + final String typeId = UUID.randomUUID().toString(); + when(oldCustomFields.getType()).thenReturn(Type.referenceOfId(typeId)); + when(oldCustomFields.getFieldsJsonMap()).thenReturn(oldCustomFieldsMap); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson(typeId, newCustomFieldsMap); + + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getCustom()).thenReturn(oldCustomFields); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name"), 1L) + .description(LocalizedString.ofEnglish("desc")) + .addedAt(ZonedDateTime.now()) + .custom(newCustomFieldsDraft) + .build(); + + final List errors = new ArrayList<>(); + + final ShoppingListSyncOptions syncOptions = + ShoppingListSyncOptionsBuilder.of(mock(SphereClient.class)) + .errorCallback((exception, oldResource, newResource, updateActions) -> + errors.add(exception.getMessage())) + .build(); + + final List> updateActions = buildTextLineItemCustomUpdateActions( + oldShoppingList, newShoppingList, oldTextLineItem, newTextLineItem, syncOptions); + + assertThat(errors).isEmpty(); + assertThat(updateActions) + .containsExactly(SetTextLineItemCustomField.ofJson("field", null, "text_line_item_id")); + } + + @Test + void buildChangeTextLineItemQuantityUpdateAction_WithSameValues_ShouldNotBuildUpdateAction() { + final TextLineItem oldLineItem = mock(TextLineItem.class); + when(oldLineItem.getId()).thenReturn("text_line_item_id"); + when(oldLineItem.getQuantity()).thenReturn(2L); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name"), 2L) + .addedAt(ZonedDateTime.now()) + .build(); + + final Optional> updateAction = + buildChangeTextLineItemQuantityUpdateAction(oldLineItem, newTextLineItem); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction).isNotPresent(); + } + + @Test + void buildChangeTextLineItemQuantityUpdateAction_WithDifferentValues_ShouldBuildUpdateAction() { + final TextLineItem oldLineItem = mock(TextLineItem.class); + when(oldLineItem.getId()).thenReturn("text_line_item_id"); + when(oldLineItem.getQuantity()).thenReturn(2L); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name"), 4L) + .addedAt(ZonedDateTime.now()) + .build(); + + final UpdateAction updateAction = + buildChangeTextLineItemQuantityUpdateAction(oldLineItem, newTextLineItem).orElse(null); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction.getAction()).isEqualTo("changeTextLineItemQuantity"); + assertThat((ChangeTextLineItemQuantity) updateAction) + .isEqualTo(ChangeTextLineItemQuantity.of("text_line_item_id", 4L)); + } + + @Test + void buildChangeTextLineItemQuantityUpdateAction_WithNewNullValue_ShouldBuildUpdateAction() { + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getQuantity()).thenReturn(2L); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name"), null) + .addedAt(ZonedDateTime.now()) + .build(); + + final UpdateAction updateAction = + buildChangeTextLineItemQuantityUpdateAction(oldTextLineItem, newTextLineItem).orElse(null); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction.getAction()).isEqualTo("changeTextLineItemQuantity"); + assertThat((ChangeTextLineItemQuantity) updateAction) + .isEqualTo(ChangeTextLineItemQuantity.of("text_line_item_id", 1L)); + } + + @Test + void buildChangeTextLineItemQuantityUpdateAction_WithNewZeroValue_ShouldBuildUpdateAction() { + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getQuantity()).thenReturn(2L); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name"), 0L) + .addedAt(ZonedDateTime.now()) + .build(); + + final UpdateAction updateAction = + buildChangeTextLineItemQuantityUpdateAction(oldTextLineItem, newTextLineItem).orElse(null); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction.getAction()).isEqualTo("changeTextLineItemQuantity"); + assertThat((ChangeTextLineItemQuantity) updateAction) + .isEqualTo(ChangeTextLineItemQuantity.of("text_line_item_id", 0L)); + } + + @Test + void buildChangeTextLineItemQuantityUpdateAction_WithNewNullValueAndOldDefaultValue_ShouldNotBuildAction() { + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getQuantity()).thenReturn(1L); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name"), null) + .addedAt(ZonedDateTime.now()) + .build(); + + final Optional> updateAction = + buildChangeTextLineItemQuantityUpdateAction(oldTextLineItem, newTextLineItem); + + assertThat(updateAction).isNotPresent(); + } + + @Test + void buildSetTextLineItemDescriptionUpdateAction_WithDifferentValues_ShouldBuildUpdateAction() { + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getDescription()).thenReturn(LocalizedString.ofEnglish("oldDescription")); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name"), 1L) + .description(LocalizedString.ofEnglish("newDescription")) + .build(); + + final UpdateAction updateAction = + buildSetTextLineItemDescriptionUpdateAction(oldTextLineItem, newTextLineItem).orElse(null); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction.getAction()).isEqualTo("setTextLineItemDescription"); + assertThat(((SetTextLineItemDescription) updateAction)) + .isEqualTo(SetTextLineItemDescription.of("text_line_item_id") + .withDescription(LocalizedString.ofEnglish("newDescription"))); + } + + @Test + void buildSetTextLineItemDescriptionUpdateAction_WithNullNewValue_ShouldBuildUpdateAction() { + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getDescription()).thenReturn(LocalizedString.ofEnglish("oldDescription")); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(null, 1L) + .build(); + + final UpdateAction updateAction = + buildSetTextLineItemDescriptionUpdateAction(oldTextLineItem, newTextLineItem).orElse(null); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction.getAction()).isEqualTo("setTextLineItemDescription"); + assertThat(((SetTextLineItemDescription) updateAction)) + .isEqualTo(SetTextLineItemDescription.of("text_line_item_id") + .withDescription(null)); + } + + @Test + void buildSetTextLineItemDescriptionUpdateAction_WithSameValues_ShouldNotBuildUpdateAction() { + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getDescription()).thenReturn(LocalizedString.ofEnglish("oldDescription")); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name"), 1L) + .description(LocalizedString.ofEnglish("oldDescription")) + .build(); + + final Optional> updateAction = + buildSetTextLineItemDescriptionUpdateAction(oldTextLineItem, newTextLineItem); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction).isNotPresent(); + } + + @Test + void buildChangeTextLineItemNameUpdateAction_WithDifferentValues_ShouldBuildUpdateAction() { + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getName()).thenReturn(LocalizedString.ofEnglish("oldName")); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("newName"), 1L) + .build(); + + final UpdateAction updateAction = + buildChangeTextLineItemNameUpdateAction(oldTextLineItem, newTextLineItem).orElse(null); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction.getAction()).isEqualTo("changeTextLineItemName"); + assertThat(((ChangeTextLineItemName) updateAction)).isEqualTo( + ChangeTextLineItemName.of("text_line_item_id", LocalizedString.ofEnglish("newName"))); + } + + @Test + void buildChangeTextLineItemNameUpdateAction_WithNullNewValue_ShouldBuildUpdateAction() { + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getName()).thenReturn(LocalizedString.ofEnglish("oldName")); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(null, 1L) + .build(); + + final UpdateAction updateAction = + buildChangeTextLineItemNameUpdateAction(oldTextLineItem, newTextLineItem).orElse(null); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction.getAction()).isEqualTo("changeTextLineItemName"); + assertThat(((ChangeTextLineItemName) updateAction)) + .isEqualTo(ChangeTextLineItemName.of("text_line_item_id", null)); + } + + @Test + void buildChangeTextLineItemNameUpdateAction_WithSameValues_ShouldNotBuildUpdateAction() { + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getName()).thenReturn(LocalizedString.ofEnglish("oldName")); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("oldName"), 1L) + .build(); + + final Optional> updateAction = + buildChangeTextLineItemNameUpdateAction(oldTextLineItem, newTextLineItem); + + assertThat(updateAction).isNotNull(); + assertThat(updateAction).isNotPresent(); + } + + @Test + void buildTextLineItemUpdateActions_WithSameValues_ShouldNotBuildUpdateAction() { + final Map oldCustomFieldsMap = new HashMap<>(); + oldCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(true)); + oldCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("de", "val")); + + final CustomFields oldCustomFields = mock(CustomFields.class); + when(oldCustomFields.getType()).thenReturn(Type.referenceOfId("1")); + when(oldCustomFields.getFieldsJsonMap()).thenReturn(oldCustomFieldsMap); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson("1", oldCustomFieldsMap); + + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getName()).thenReturn(LocalizedString.ofEnglish("name")); + when(oldTextLineItem.getDescription()).thenReturn(LocalizedString.ofEnglish("desc")); + when(oldTextLineItem.getQuantity()).thenReturn(1L); + when(oldTextLineItem.getCustom()).thenReturn(oldCustomFields); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("name"), 1L) + .description(LocalizedString.ofEnglish("desc")) + .custom(newCustomFieldsDraft) + .build(); + + final List> updateActions = buildTextLineItemUpdateActions( + oldShoppingList, newShoppingList, oldTextLineItem, newTextLineItem, SYNC_OPTIONS); + + assertThat(updateActions).isEmpty(); + } + + @Test + void buildTextLineItemUpdateActions_WithDifferentValues_ShouldBuildUpdateAction() { + final Map oldCustomFieldsMap = new HashMap<>(); + oldCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(true)); + oldCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("de", "val1")); + + final Map newCustomFieldsMap = new HashMap<>(); + newCustomFieldsMap.put("field1", JsonNodeFactory.instance.booleanNode(false)); + newCustomFieldsMap.put("field2", JsonNodeFactory.instance.objectNode().put("es", "val2")); + + final CustomFields oldCustomFields = mock(CustomFields.class); + when(oldCustomFields.getType()).thenReturn(Type.referenceOfId("1")); + when(oldCustomFields.getFieldsJsonMap()).thenReturn(oldCustomFieldsMap); + + final CustomFieldsDraft newCustomFieldsDraft = + CustomFieldsDraft.ofTypeIdAndJson("1", newCustomFieldsMap); + + final TextLineItem oldTextLineItem = mock(TextLineItem.class); + when(oldTextLineItem.getId()).thenReturn("text_line_item_id"); + when(oldTextLineItem.getName()).thenReturn(LocalizedString.ofEnglish("name")); + when(oldTextLineItem.getDescription()).thenReturn(LocalizedString.ofEnglish("desc")); + when(oldTextLineItem.getQuantity()).thenReturn(1L); + when(oldTextLineItem.getCustom()).thenReturn(oldCustomFields); + + final TextLineItemDraft newTextLineItem = + TextLineItemDraftBuilder.of(LocalizedString.ofEnglish("newName"), 2L) + .description(LocalizedString.ofEnglish("newDesc")) + .custom(newCustomFieldsDraft) + .build(); + + final List> updateActions = buildTextLineItemUpdateActions( + oldShoppingList, newShoppingList, oldTextLineItem, newTextLineItem, SYNC_OPTIONS); + + assertThat(updateActions).containsExactly( + ChangeTextLineItemName.of("text_line_item_id", LocalizedString.ofEnglish("newName")), + SetTextLineItemDescription.of("text_line_item_id").withDescription(LocalizedString.ofEnglish("newDesc")), + ChangeTextLineItemQuantity.of("text_line_item_id", 2L), + SetTextLineItemCustomField.ofJson("field1", + JsonNodeFactory.instance.booleanNode(false), "text_line_item_id"), + SetTextLineItemCustomField.ofJson("field2", + JsonNodeFactory.instance.objectNode().put("es", "val2"), "text_line_item_id") + ); + } +} diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/textlineitems/shoppinglist-with-textlineitems-name-12.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/textlineitems/shoppinglist-with-textlineitems-name-12.json new file mode 100644 index 0000000000..7cd56631fd --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/textlineitems/shoppinglist-with-textlineitems-name-12.json @@ -0,0 +1,130 @@ +{ + "key": "shoppinglist-with-textlineitems-name-12", + "name": { + "en": "name" + }, + "slug": { + "en": "slug" + }, + "description": { + "en": "description" + }, + "anonymousId": "anonymousId", + "lineItems": [], + "textLineItems": [ + { + "id": "text_line_item_id_1", + "name": { + "de": "name1-DE", + "en": "name1-EN" + }, + "description": { + "de": "desc1-DE", + "en": "desc1-EN" + }, + "quantity": 1, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text1" + } + }, + "addedAt": "2020-11-03T10:00:00.000Z" + }, + { + "id": "text_line_item_id_2", + "name": { + "de": "name2-DE", + "en": "name2-EN" + }, + "description": { + "de": "desc2-DE", + "en": "desc2-EN" + }, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text2" + } + }, + "addedAt": "2020-11-04T10:00:00.000Z" + } + ], + "customer": { + "typeId": "customer", + "id": "customer_id_1" + }, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id" + }, + "fields": { + "textField": "textValue" + } + }, + "deleteDaysAfterLastModification": 30 +} diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-with-different-addedAt.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/textlineitems/shoppinglist-with-textlineitems-name-123-with-changes.json similarity index 73% rename from src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-with-different-addedAt.json rename to src/test/resources/com/commercetools/sync/shoppinglists/utils/textlineitems/shoppinglist-with-textlineitems-name-123-with-changes.json index e2ff548440..679371e5b1 100644 --- a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-with-different-addedAt.json +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/textlineitems/shoppinglist-with-textlineitems-name-123-with-changes.json @@ -1,24 +1,28 @@ { - "key": "shoppinglist-with-lineitems-sku-123-with-different-addedAt", + "key": "shoppinglist-with-textlineitems-name-123-with-changes", "name": { - "en": "name" + "en": "newName" }, "slug": { - "en": "slug" + "en": "newSlug" }, "description": { - "en": "description" + "en": "newDescription" }, - "anonymousId": "anonymousId", - "lineItems": [ + "anonymousId": "newAnonymousId", + "lineItems": [], + "textLineItems": [ { - "id": "line_item_id_1", - "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "id": "text_line_item_id_1", "name": { - "en": "Some Product" + "de": "newName1-DE", + "en": "newName1-EN" }, - "variantId": 1, - "quantity": 1, + "description": { + "de": "newDesc1-DE", + "en": "newDesc1-EN" + }, + "quantity": 2, "custom": { "type": { "typeId": "type", @@ -53,23 +57,22 @@ } }, "fields": { - "textField": "text1" + "textField": "newText1" } }, - "addedAt": "2020-12-02T10:00:00.000Z", - "variant": { - "id": 1, - "sku": "SKU-1" - } + "addedAt": "2020-12-03T10:00:00.000Z" }, { - "id": "line_item_id_2", - "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "id": "text_line_item_id_2", "name": { - "en": "Some Product" + "de": "newName2-DE", + "en": "newName2-EN" }, - "variantId": 2, - "quantity": 2, + "description": { + "de": "newDesc2-DE", + "en": "newDesc2-EN" + }, + "quantity": 4, "custom": { "type": { "typeId": "type", @@ -104,23 +107,22 @@ } }, "fields": { - "textField": "text2" + "textField": "newText2" } }, - "addedAt": "2020-12-03T10:00:00.000Z", - "variant": { - "id": 2, - "sku": "SKU-2" - } + "addedAt": "2020-12-04T10:00:00.000Z" }, { - "id": "line_item_id_3", - "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "id": "text_line_item_id_3", "name": { - "en": "Some Product" + "de": "newName3-DE", + "en": "newName3-EN" + }, + "description": { + "de": "newDesc3-DE", + "en": "newDesc3-EN" }, - "variantId": 3, - "quantity": 3, + "quantity": 6, "custom": { "type": { "typeId": "type", @@ -155,20 +157,15 @@ } }, "fields": { - "textField": "text3" + "textField": "newText3" } }, - "addedAt": "2020-12-04T10:00:00.000Z", - "variant": { - "id": 3, - "sku": "SKU-3" - } + "addedAt": "2020-12-05T10:00:00.000Z" } ], - "textLineItems": [], "customer": { "typeId": "customer", - "id": "customer_id_1" + "id": "customer_id_2" }, "custom": { "type": { @@ -176,8 +173,8 @@ "id": "custom_type_id" }, "fields": { - "textField": "textValue" + "textField": "newTextValue" } }, - "deleteDaysAfterLastModification": 30 + "deleteDaysAfterLastModification": 45 } diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-without-addedAt.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/textlineitems/shoppinglist-with-textlineitems-name-123.json similarity index 82% rename from src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-without-addedAt.json rename to src/test/resources/com/commercetools/sync/shoppinglists/utils/textlineitems/shoppinglist-with-textlineitems-name-123.json index ad0940a82b..ea19934f7b 100644 --- a/src/test/resources/com/commercetools/sync/shoppinglists/utils/lineitems/shoppinglist-with-lineitems-sku-123-without-addedAt.json +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/textlineitems/shoppinglist-with-textlineitems-name-123.json @@ -1,5 +1,5 @@ { - "key": "shoppinglist-with-lineitems-sku-123-without-addedAt", + "key": "shoppinglist-with-textlineitems-name-123", "name": { "en": "name" }, @@ -10,14 +10,18 @@ "en": "description" }, "anonymousId": "anonymousId", - "lineItems": [ + "lineItems": [], + "textLineItems": [ { - "id": "line_item_id_1", - "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "id": "text_line_item_id_1", "name": { - "en": "Some Product" + "de": "name1-DE", + "en": "name1-EN" + }, + "description": { + "de": "desc1-DE", + "en": "desc1-EN" }, - "variantId": 1, "quantity": 1, "custom": { "type": { @@ -56,18 +60,18 @@ "textField": "text1" } }, - "variant": { - "id": 1, - "sku": "SKU-1" - } + "addedAt": "2020-11-03T10:00:00.000Z" }, { - "id": "line_item_id_2", - "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "id": "text_line_item_id_2", "name": { - "en": "Some Product" + "de": "name2-DE", + "en": "name2-EN" + }, + "description": { + "de": "desc2-DE", + "en": "desc2-EN" }, - "variantId": 2, "quantity": 2, "custom": { "type": { @@ -106,18 +110,18 @@ "textField": "text2" } }, - "variant": { - "id": 2, - "sku": "SKU-2" - } + "addedAt": "2020-11-04T10:00:00.000Z" }, { - "id": "line_item_id_3", - "productId": "130a7d5c-55ea-4888-abbe-f76d474de351", + "id": "text_line_item_id_3", "name": { - "en": "Some Product" + "de": "name3-DE", + "en": "name3-EN" + }, + "description": { + "de": "desc3-DE", + "en": "desc3-EN" }, - "variantId": 3, "quantity": 3, "custom": { "type": { @@ -156,13 +160,9 @@ "textField": "text3" } }, - "variant": { - "id": 3, - "sku": "SKU-3" - } + "addedAt": "2020-11-05T10:00:00.000Z" } ], - "textLineItems": [], "customer": { "typeId": "customer", "id": "customer_id_1" diff --git a/src/test/resources/com/commercetools/sync/shoppinglists/utils/textlineitems/shoppinglist-with-textlineitems-name-1234.json b/src/test/resources/com/commercetools/sync/shoppinglists/utils/textlineitems/shoppinglist-with-textlineitems-name-1234.json new file mode 100644 index 0000000000..1d3ef4aafa --- /dev/null +++ b/src/test/resources/com/commercetools/sync/shoppinglists/utils/textlineitems/shoppinglist-with-textlineitems-name-1234.json @@ -0,0 +1,230 @@ +{ + "key": "shoppinglist-with-textlineitems-name-1234", + "name": { + "en": "name" + }, + "slug": { + "en": "slug" + }, + "description": { + "en": "description" + }, + "anonymousId": "anonymousId", + "lineItems": [], + "textLineItems": [ + { + "id": "text_line_item_id_1", + "name": { + "de": "name1-DE", + "en": "name1-EN" + }, + "description": { + "de": "desc1-DE", + "en": "desc1-EN" + }, + "quantity": 1, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text1" + } + }, + "addedAt": "2020-11-03T10:00:00.000Z" + }, + { + "id": "text_line_item_id_2", + "name": { + "de": "name2-DE", + "en": "name2-EN" + }, + "description": { + "de": "desc2-DE", + "en": "desc2-EN" + }, + "quantity": 2, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text2" + } + }, + "addedAt": "2020-11-04T10:00:00.000Z" + }, + { + "id": "text_line_item_id_3", + "name": { + "de": "name3-DE", + "en": "name3-EN" + }, + "description": { + "de": "desc3-DE", + "en": "desc3-EN" + }, + "quantity": 3, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text3" + } + }, + "addedAt": "2020-11-05T10:00:00.000Z" + }, + { + "id": "text_line_item_id_4", + "name": { + "de": "name4-DE", + "en": "name4-EN" + }, + "description": { + "de": "desc4-DE", + "en": "desc4-EN" + }, + "quantity": 4, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id", + "obj": { + "id": "custom_type_id", + "key": "custom-type-for-shoppinglists", + "name": { + "en": "custom-type-for-shoppinglists" + }, + "description": { + "en": "custom-type-for-shoppinglists" + }, + "resourceTypeIds": [ + "line-item", + "shopping-list", + "shopping-list-text-line-item" + ], + "fieldDefinitions": [ + { + "name": "textField", + "label": { + "en": "textField" + }, + "required": false, + "type": { + "name": "String" + }, + "inputHint": "SingleLine" + } + ] + } + }, + "fields": { + "textField": "text4" + } + }, + "addedAt": "2020-11-06T10:00:00.000Z" + } + ], + "customer": { + "typeId": "customer", + "id": "customer_id_1" + }, + "custom": { + "type": { + "typeId": "type", + "id": "custom_type_id" + }, + "fields": { + "textField": "textValue" + } + }, + "deleteDaysAfterLastModification": 30 +} From c08441a8efded6e91c49e7e3486ab0ddde211daa Mon Sep 17 00:00:00 2001 From: Jude Niroshan Date: Tue, 17 Nov 2020 17:49:26 +0100 Subject: [PATCH 08/12] wrapped

  • elements in