How to sync prices of base product to its variants

Sometimes simple tasks like “On base product price change also change price for all its sub products” can lead to long nights of debugging and seeking of workarounds. Sounds like this task should be easy for implementation. Create PrepareInterceptor, check if price was changed with InterceptorContext.isModified() method, iterate all sub products and put the new price.

But real life doesn’t meet our expectations. Firstly isModified method will surprised you, because it will always return false, even when price was changed. The root of such behaviour is that prices are collectiontype attribute and to deal with that can be used workaround with DefaultModelServiceInterceptorContext.getInitialElements() method, which return list of changed related objects, so you will be able to calculate if price was changed. The same workaround can be used to receive on which exact price it was changed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
        boolean changedPrice = false;
        if (ctx instanceof DefaultModelServiceInterceptorContext) {
            Set<Object> changedTypes = ((DefaultModelServiceInterceptorContext) ctx).getInitialElements().unmodifiableSet();
            for (Object changedObject : changedTypes) {
                if (changedObject instanceof PriceRowModel) {
                    changedPrice = true;
                    break;
                }
            }
        }

Second surprise us that you can’t use the same PriceRowModel for all sub products, because prices are also a partOf attribute of a product, so you need to clone each price for each variant product.

Another interesting thing is related to ProductPriceGroups. All prices added by ProductPriceGroup will be visible as a regular prices via getEurope1Prices method and you need to skip them while cloning prices to sub products.

So the final implementation can look like:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package com.blog.core.interceptors.impl;

import com.blog.core.model.ApparelProductModel;
import com.blog.core.product.BlogProductService;
import de.hybris.platform.core.model.product.ProductModel;
import de.hybris.platform.europe1.enums.ProductPriceGroup;
import de.hybris.platform.europe1.enums.ProductTaxGroup;
import de.hybris.platform.europe1.model.PriceRowModel;
import de.hybris.platform.servicelayer.interceptor.InterceptorContext;
import de.hybris.platform.servicelayer.interceptor.PrepareInterceptor;
import de.hybris.platform.servicelayer.internal.model.impl.DefaultModelServiceInterceptorContext;
import de.hybris.platform.servicelayer.model.ModelService;
import de.hybris.platform.variants.model.VariantProductModel;
import org.apache.commons.collections.CollectionUtils;

import javax.annotation.Resource;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class BlogProductChangePriceInterceptor implements PrepareInterceptor {

    @Resource
    private BlogProductService productService;

    @Resource
    private ModelService modelService;

    @Override
    public void onPrepare(Object model, InterceptorContext ctx) {
        if (model instanceof ApparelProductModel) {
            ApparelProductModel apparelProductModel = (ApparelProductModel) model;

            // If productModel is updating and price was modified
            if (!ctx.isNew(apparelProductModel) && isPriceModified(ctx, apparelProductModel)) {

                ProductPriceGroup priceGroup = apparelProductModel.getEurope1PriceFactory_PPG();
                ProductTaxGroup taxGroup = apparelProductModel.getEurope1PriceFactory_PTG();

                // Hash set is used to avoid price duplication
                final Collection<PriceRowModel> prices = new HashSet<>(apparelProductModel.getEurope1Prices());
                prices.addAll(getModifiedPrices(ctx));

                List<VariantProductModel> productVariants = productService.getAllProductVariants(apparelProductModel.getCode());
                assignPricesToVariantProduct(productVariants, priceGroup, taxGroup, prices, ctx);
            }
        }
    }

    private void assignPricesToVariantProduct(List<? extends VariantProductModel> products,
                                              ProductPriceGroup priceGroup,
                                              ProductTaxGroup taxGroup,
                                              final Collection<PriceRowModel> prices,
                                              InterceptorContext context) {
        if (CollectionUtils.isNotEmpty(products)) {
            products.forEach(product -> {
                product.setEurope1PriceFactory_PPG(priceGroup);
                product.setEurope1Prices(copyNonePGPrices(prices, product));
                product.setEurope1PriceFactory_PTG(taxGroup);
                context.registerElement(product);
            });
        }
    }

    // Due to price is partOf for product, we need to create new DB instances
    private Collection<PriceRowModel> copyNonePGPrices(final Collection<PriceRowModel> prices, ProductModel product) {
        Collection<PriceRowModel> copiedPrices = new HashSet<>();
        for (PriceRowModel price : prices) {
            // Clone only prices, which don`t belong to price group, beccause they will be added dynamically on 
            // product price group assigning 
            if (price.getPg() == null) {
                PriceRowModel clonedPrice = modelService.clone(price);
                clonedPrice.setProduct(product);
                copiedPrices.add(clonedPrice);
            }
        }
        return copiedPrices;
    }


    private boolean isPriceModified(InterceptorContext ctx, ApparelProductModel apparelProductModel) {
        boolean changedPrice = ctx.isModified(apparelProductModel, ApparelProductModel.EUROPE1PRICES) ||
                ctx.isModified(apparelProductModel, ApparelProductModel.OWNEUROPE1PRICES) ||
                ctx.isModified(apparelProductModel, ApparelProductModel.EUROPE1PRICEFACTORY_PPG);

        // Due to europe1price is collectiontype interceptor context can not identify that field was changed
        // Method below is a workaround to identify change of europe1price field
        if (!changedPrice && ctx instanceof DefaultModelServiceInterceptorContext) {
            Set<Object> changedTypes = ((DefaultModelServiceInterceptorContext) ctx).getInitialElements().unmodifiableSet();
            for (Object changedObject : changedTypes) {
                if (changedObject instanceof PriceRowModel) {
                    changedPrice = true;
                    break;
                }
            }
        }

        // Check that all variant has prices to set them prices of base product
        // We need this check when new product variant was added to product via hotfolder/impex
        if (!changedPrice) {
            // check that all variant has prices
            List<ProductModel> productWithVariants = productService.getAllProductVariants(apparelProductModel);
            long numberOfProductsWithoutPrices = productWithVariants.stream()
                    .filter(product -> CollectionUtils.isEmpty(product.getEurope1Prices()))
                    .count();
            // if some variant do not have price, than we should remap all prices
            if (numberOfProductsWithoutPrices > 0) {
                changedPrice = true;
            }
        }
        return changedPrice;

    }

    private Collection<PriceRowModel> getModifiedPrices(InterceptorContext ctx) {
        Collection<PriceRowModel> changedPrices = new HashSet<>();
        // Due to europe1price is collectiontype interceptor context can not identify that field was changed
        // Method below is a workaround to identify change of europe1price field
        if (ctx instanceof DefaultModelServiceInterceptorContext) {
            Set<Object> changedTypes = ((DefaultModelServiceInterceptorContext) ctx).getInitialElements().unmodifiableSet();
            for (Object changedObject : changedTypes) {
                if (changedObject instanceof PriceRowModel) {
                    changedPrices.add((PriceRowModel) changedObject);
                }
            }
        }

        return changedPrices;
    }
}

P.S. Keep in mind that to reuse code above you need to implement method getAllProductVariants, which returns list of VariantProducts for price changing.

comments powered by Disqus