Skip to content

Commit 5675424

Browse files
authored
Merge pull request #1119 from commercetools/DEVX-227_Sync-products-referencing-itself
Sync products with self-reference in attribute
2 parents 83a34dd + 3bee96f commit 5675424

File tree

7 files changed

+419
-6
lines changed

7 files changed

+419
-6
lines changed

src/integration-test/java/com/commercetools/sync/integration/externalsource/products/ProductSyncWithNestedReferencedProductsIT.java

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,104 @@ void sync_withNestedProductReferenceAsAttribute_shouldCreateProductReferencingEx
317317
});
318318
}
319319

320+
@Test
321+
void sync_withNestedProductReferencingItselfAsAttribute_shouldCreateProductReferencingItself() {
322+
// preparation
323+
final String sameNewProductKey = "new-product";
324+
final Attribute nestedProductReferenceAttribute =
325+
createNestedAttributeValueReferences(
326+
"product-reference",
327+
createReferenceObjectJson(sameNewProductKey, ProductReference.PRODUCT));
328+
final Attribute nestedProductReferenceSetAttribute =
329+
createNestedAttributeValueSetOfReferences(
330+
"product-reference-set",
331+
createReferenceObject(sameNewProductKey, ProductReference.PRODUCT),
332+
createReferenceObject(testProduct1.getKey(), ProductReference.PRODUCT));
333+
334+
final Attribute productReferenceAttribute =
335+
AttributeBuilder.of()
336+
.name("nestedAttribute")
337+
.value(List.of(nestedProductReferenceAttribute, nestedProductReferenceSetAttribute))
338+
.build();
339+
340+
final ProductVariantDraft masterVariant =
341+
ProductVariantDraftBuilder.of()
342+
.sku("sku")
343+
.key("new-product-master-variant")
344+
.attributes(productReferenceAttribute)
345+
.build();
346+
347+
final ProductDraft productDraftWithProductReference =
348+
ProductDraftBuilder.of()
349+
.productType(productType.toResourceIdentifier())
350+
.name(ofEnglish("productName"))
351+
.slug(ofEnglish("productSlug"))
352+
.masterVariant(masterVariant)
353+
.key("new-product")
354+
.build();
355+
356+
// test
357+
final ProductSync productSync = new ProductSync(syncOptions);
358+
final ProductSyncStatistics syncStatistics =
359+
productSync
360+
.sync(singletonList(productDraftWithProductReference))
361+
.toCompletableFuture()
362+
.join();
363+
364+
// assertion
365+
assertThat(syncStatistics).hasValues(1, 1, 1, 0, 0);
366+
assertThat(errorCallBackExceptions).isEmpty();
367+
assertThat(errorCallBackMessages).isEmpty();
368+
assertThat(warningCallBackMessages).isEmpty();
369+
assertThat(actions).hasSize(1);
370+
assertThat(actions.get(0))
371+
.satisfies(
372+
productUpdateAction -> {
373+
assertThat(productUpdateAction).isInstanceOf(ProductSetAttributeAction.class);
374+
ProductSetAttributeAction castedAction =
375+
(ProductSetAttributeAction) productUpdateAction;
376+
assertThat(castedAction.getName()).isEqualTo("nestedAttribute");
377+
assertThat(castedAction.getStaged()).isTrue();
378+
assertThat(castedAction.getValue()).isNotNull();
379+
});
380+
381+
final Product createdProduct =
382+
CTP_TARGET_CLIENT
383+
.products()
384+
.withKey(productDraftWithProductReference.getKey())
385+
.get()
386+
.execute()
387+
.toCompletableFuture()
388+
.join()
389+
.getBody();
390+
391+
final Optional<Attribute> createdProductReferenceAttribute =
392+
createdProduct
393+
.getMasterData()
394+
.getStaged()
395+
.getMasterVariant()
396+
.findAttribute(productReferenceAttribute.getName());
397+
398+
assertThat(createdProductReferenceAttribute)
399+
.hasValueSatisfying(
400+
attribute -> {
401+
assertThat(attribute.getValue()).isInstanceOf(List.class);
402+
List<Attribute> nested = AttributeAccessor.asNested(attribute);
403+
final Attribute nestedAttribute1 = nested.get(0);
404+
assertThat(nestedAttribute1.getName()).isEqualTo("product-reference");
405+
final ProductReference attrValue = (ProductReference) nestedAttribute1.getValue();
406+
assertThat(attrValue.getId()).isEqualTo(createdProduct.getId());
407+
final Attribute nestedAttribute2 = nested.get(1);
408+
assertThat(nestedAttribute2.getName()).isEqualTo("product-reference-set");
409+
final List<Reference> referenceList =
410+
AttributeAccessor.asSetReference(nestedAttribute2);
411+
assertThat(referenceList)
412+
.containsExactlyInAnyOrder(
413+
ProductReferenceBuilder.of().id(createdProduct.getId()).build(),
414+
ProductReferenceBuilder.of().id(testProduct1.getId()).build());
415+
});
416+
}
417+
320418
@Test
321419
void sync_withSameNestedProductReferenceAsAttribute_shouldNotSyncAnythingNew() {
322420
// preparation
@@ -693,6 +791,7 @@ void sync_withNonExistingNestedProductReferenceAsAttribute_ShouldFailCreatingThe
693791
createNestedAttributeValueSetOfReferences(
694792
"product-reference-set",
695793
createReferenceObject(testProduct1.getKey(), ProductReference.PRODUCT),
794+
createReferenceObject("new-product", ProductReference.PRODUCT),
696795
createReferenceObject("nonExistingKey", ProductReference.PRODUCT));
697796

698797
final Attribute productReferenceAttribute =
@@ -750,6 +849,67 @@ void sync_withNonExistingNestedProductReferenceAsAttribute_ShouldFailCreatingThe
750849
.containsExactly("nonExistingKey");
751850
return true;
752851
});
852+
853+
final Product nowExistingProduct =
854+
CTP_TARGET_CLIENT
855+
.products()
856+
.create(
857+
ProductDraftBuilder.of()
858+
.productType(productType.toResourceIdentifier())
859+
.name(ofEnglish("nonExistingName"))
860+
.slug(ofEnglish("nonExistingSlug"))
861+
.key("nonExistingKey")
862+
.build())
863+
.executeBlocking()
864+
.getBody();
865+
866+
syncOptions = buildSyncOptions();
867+
final ProductSync productSync2ndRun = new ProductSync(syncOptions);
868+
final ProductSyncStatistics syncStatistics2ndRun =
869+
productSync2ndRun
870+
.sync(singletonList(productDraftWithProductReference))
871+
.toCompletableFuture()
872+
.join();
873+
874+
// assertion
875+
assertThat(syncStatistics2ndRun).hasValues(1, 1, 1, 0, 0);
876+
assertThat(errorCallBackExceptions).isEmpty();
877+
assertThat(errorCallBackMessages).isEmpty();
878+
assertThat(warningCallBackMessages).isEmpty();
879+
assertThat(actions).isNotEmpty();
880+
881+
final Product createdProduct =
882+
CTP_TARGET_CLIENT
883+
.products()
884+
.withKey(productDraftWithProductReference.getKey())
885+
.get()
886+
.execute()
887+
.toCompletableFuture()
888+
.join()
889+
.getBody();
890+
891+
final Optional<Attribute> createdProductReferenceAttribute =
892+
createdProduct
893+
.getMasterData()
894+
.getStaged()
895+
.getMasterVariant()
896+
.findAttribute(productReferenceAttribute.getName());
897+
898+
assertThat(createdProductReferenceAttribute)
899+
.hasValueSatisfying(
900+
attribute -> {
901+
assertThat(attribute.getValue()).isInstanceOf(List.class);
902+
List<Attribute> nested = AttributeAccessor.asNested(attribute);
903+
final Attribute nestedAttribute = nested.get(0);
904+
assertThat(nestedAttribute.getName()).isEqualTo("product-reference-set");
905+
final List<Reference> referenceList =
906+
AttributeAccessor.asSetReference(nestedAttribute);
907+
assertThat(referenceList)
908+
.containsExactlyInAnyOrder(
909+
ProductReferenceBuilder.of().id(createdProduct.getId()).build(),
910+
ProductReferenceBuilder.of().id(nowExistingProduct.getId()).build(),
911+
ProductReferenceBuilder.of().id(testProduct1.getId()).build());
912+
});
753913
}
754914

755915
@Test

src/integration-test/java/com/commercetools/sync/integration/externalsource/products/ProductSyncWithReferencedProductsIT.java

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.commercetools.sync.products.ProductSyncOptionsBuilder;
2828
import com.commercetools.sync.products.helpers.ProductSyncStatistics;
2929
import com.commercetools.sync.services.impl.UnresolvedReferencesServiceImpl;
30+
import com.fasterxml.jackson.core.type.TypeReference;
3031
import com.fasterxml.jackson.databind.JsonNode;
3132
import com.fasterxml.jackson.databind.node.ObjectNode;
3233
import io.vrap.rmf.base.client.utils.json.JsonUtils;
@@ -188,6 +189,129 @@ void sync_withProductReferenceAsAttribute_shouldCreateProductReferencingExisting
188189
});
189190
}
190191

192+
@Test
193+
void sync_withProductReferencingItselfAsAttribute_shouldCreateProductReferencingItself() {
194+
// preparation
195+
final String sameNewProductKey = "new-product-key";
196+
final Attribute productReferenceAttribute =
197+
AttributeBuilder.of()
198+
.name("product-reference")
199+
.value(createReferenceObject(sameNewProductKey, ProductReference.PRODUCT))
200+
.build();
201+
final Set<Reference> references =
202+
Set.of(
203+
createReferenceObject(sameNewProductKey, ProductReference.PRODUCT),
204+
createReferenceObject(product.getKey(), ProductReference.PRODUCT));
205+
206+
final Attribute productReferenceSetAttribute =
207+
AttributeBuilder.of().name("product-reference-set").value(references).build();
208+
209+
final ProductVariantDraft masterVariant =
210+
ProductVariantDraftBuilder.of()
211+
.sku("sku")
212+
.key("new-product-master-variant")
213+
.attributes(productReferenceAttribute, productReferenceSetAttribute)
214+
.build();
215+
216+
final ProductDraft productDraftWithProductReference =
217+
ProductDraftBuilder.of()
218+
.productType(productType.toResourceIdentifier())
219+
.name(ofEnglish("productName"))
220+
.slug(ofEnglish("productSlug"))
221+
.masterVariant(masterVariant)
222+
.key(sameNewProductKey)
223+
.build();
224+
225+
// test
226+
final ProductSync productSync = new ProductSync(syncOptions);
227+
final ProductSyncStatistics syncStatistics =
228+
productSync
229+
.sync(singletonList(productDraftWithProductReference))
230+
.toCompletableFuture()
231+
.join();
232+
233+
// assertion
234+
assertThat(syncStatistics).hasValues(1, 1, 1, 0, 0);
235+
assertThat(errorCallBackExceptions).isEmpty();
236+
assertThat(errorCallBackMessages).isEmpty();
237+
assertThat(warningCallBackMessages).isEmpty();
238+
final Product createdProduct =
239+
CTP_TARGET_CLIENT
240+
.products()
241+
.withKey(productDraftWithProductReference.getKey())
242+
.get()
243+
.execute()
244+
.toCompletableFuture()
245+
.join()
246+
.getBody();
247+
248+
final JsonNode referenceObjectSameProduct =
249+
JsonUtils.toJsonNode(
250+
createReferenceObject(createdProduct.getId(), ProductReference.PRODUCT));
251+
final JsonNode referenceObjectOtherProduct =
252+
JsonUtils.toJsonNode(createReferenceObject(product.getId(), ProductReference.PRODUCT));
253+
assertThat(actions).hasSize(2);
254+
assertThat(actions.get(0))
255+
.satisfies(
256+
productUpdateAction -> {
257+
assertThat(productUpdateAction)
258+
.isInstanceOf(ProductSetAttributeInAllVariantsAction.class);
259+
ProductSetAttributeInAllVariantsAction castedAction =
260+
(ProductSetAttributeInAllVariantsAction) productUpdateAction;
261+
assertThat(castedAction.getName()).isEqualTo("product-reference");
262+
assertThat(castedAction.getStaged()).isTrue();
263+
assertThat(castedAction.getValue()).isEqualTo(referenceObjectSameProduct);
264+
});
265+
assertThat(actions.get(1))
266+
.satisfies(
267+
productUpdateAction -> {
268+
assertThat(productUpdateAction)
269+
.isInstanceOf(ProductSetAttributeInAllVariantsAction.class);
270+
ProductSetAttributeInAllVariantsAction castedAction =
271+
(ProductSetAttributeInAllVariantsAction) productUpdateAction;
272+
assertThat(castedAction.getName()).isEqualTo("product-reference-set");
273+
assertThat(castedAction.getStaged()).isTrue();
274+
List<JsonNode> valueAsList =
275+
JsonUtils.fromJsonNode(
276+
(JsonNode) castedAction.getValue(), new TypeReference<>() {});
277+
assertThat(valueAsList)
278+
.containsExactlyInAnyOrder(
279+
referenceObjectSameProduct, referenceObjectOtherProduct);
280+
});
281+
282+
final Optional<Attribute> createdProductReferenceAttribute =
283+
createdProduct
284+
.getMasterData()
285+
.getStaged()
286+
.getMasterVariant()
287+
.findAttribute(productReferenceAttribute.getName());
288+
289+
assertThat(createdProductReferenceAttribute)
290+
.hasValueSatisfying(
291+
attribute -> {
292+
final ProductReference productReference = (ProductReference) attribute.getValue();
293+
assertThat(productReference.getTypeId()).isEqualTo(ReferenceTypeId.PRODUCT);
294+
assertThat(productReference.getId()).isEqualTo(createdProduct.getId());
295+
});
296+
297+
final Optional<Attribute> createdProductReferenceSetAttribute =
298+
createdProduct
299+
.getMasterData()
300+
.getStaged()
301+
.getMasterVariant()
302+
.findAttribute(productReferenceSetAttribute.getName());
303+
304+
assertThat(createdProductReferenceSetAttribute)
305+
.hasValueSatisfying(
306+
attribute -> {
307+
final List<Reference> referenceList = AttributeAccessor.asSetReference(attribute);
308+
assertThat(referenceList)
309+
.containsExactlyInAnyOrder(
310+
ProductReferenceBuilder.of().id(createdProduct.getId()).build(),
311+
ProductReferenceBuilder.of().id(product.getId()).build());
312+
});
313+
}
314+
191315
@Test
192316
void sync_withSameProductReferenceAsAttribute_shouldNotSyncAnythingNew() {
193317
// preparation
@@ -552,6 +676,7 @@ void sync_withProductReferenceSetContainingANonExistingReference_shouldFailCreat
552676
final Set<Reference> references =
553677
Set.of(
554678
createReferenceObject("nonExistingKey", ProductReference.PRODUCT),
679+
createReferenceObject("new-product", ProductReference.PRODUCT),
555680
createReferenceObject(product2.getKey(), ProductReference.PRODUCT));
556681

557682
final Attribute productReferenceSetAttribute =
@@ -609,5 +734,61 @@ void sync_withProductReferenceSetContainingANonExistingReference_shouldFailCreat
609734
.containsExactly("nonExistingKey");
610735
return true;
611736
});
737+
738+
final Product missingProductCreated =
739+
CTP_TARGET_CLIENT
740+
.products()
741+
.create(
742+
ProductDraftBuilder.of()
743+
.productType(productType.toResourceIdentifier())
744+
.name(ofEnglish("noExistingName"))
745+
.slug(ofEnglish("noExistingSlug"))
746+
.key("nonExistingKey")
747+
.build())
748+
.executeBlocking()
749+
.getBody();
750+
// rebuild syncOptions to clear old statistics
751+
syncOptions = buildSyncOptions();
752+
final ProductSync productSync2ndRun = new ProductSync(syncOptions);
753+
final ProductSyncStatistics syncStatistics2ndRun =
754+
productSync2ndRun
755+
.sync(singletonList(productDraftWithProductReference))
756+
.toCompletableFuture()
757+
.join();
758+
759+
// assertion
760+
assertThat(syncStatistics2ndRun).hasValues(1, 1, 1, 0, 0);
761+
assertThat(errorCallBackExceptions).isEmpty();
762+
assertThat(errorCallBackMessages).isEmpty();
763+
assertThat(warningCallBackMessages).isEmpty();
764+
assertThat(actions).isNotEmpty();
765+
766+
final Product createdProduct =
767+
CTP_TARGET_CLIENT
768+
.products()
769+
.withKey(productDraftWithProductReference.getKey())
770+
.get()
771+
.execute()
772+
.toCompletableFuture()
773+
.join()
774+
.getBody();
775+
776+
final Optional<Attribute> createdProductReferenceSetAttribute =
777+
createdProduct
778+
.getMasterData()
779+
.getStaged()
780+
.getMasterVariant()
781+
.findAttribute(productReferenceSetAttribute.getName());
782+
783+
assertThat(createdProductReferenceSetAttribute)
784+
.hasValueSatisfying(
785+
attribute -> {
786+
final List<Reference> referenceList = AttributeAccessor.asSetReference(attribute);
787+
assertThat(referenceList)
788+
.containsExactlyInAnyOrder(
789+
ProductReferenceBuilder.of().id(createdProduct.getId()).build(),
790+
ProductReferenceBuilder.of().id(missingProductCreated.getId()).build(),
791+
ProductReferenceBuilder.of().id(product2.getId()).build());
792+
});
612793
}
613794
}

src/main/java/com/commercetools/sync/commons/helpers/BaseReferenceResolver.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public abstract class BaseReferenceResolver<
2828
public static final String BLANK_ID_VALUE_ON_REFERENCE =
2929
"The value of the 'id' field of the Reference"
3030
+ " is blank (null/empty). Expecting the key of the referenced resource.";
31+
public static final String SELF_REFERENCING_ID_PLACE_HOLDER = "SELF_REFERENCE_ID";
3132
protected SyncOptionsT options;
3233

3334
protected BaseReferenceResolver(@Nonnull final SyncOptionsT options) {

0 commit comments

Comments
 (0)