Price optimizing: to be or not to be

It can be quite confusing when you first edit prices in Episerver Commerce. To your surprises, some of the prices you imported or edited might disappear, or change, without your consent! What happened?

To answer that question, it’s essential to know there are two pricing system in Commerce: IPriceService and IPriceDetailService.

They have some different characteristics, and one of them is very important: the default implementation of IPriceDetailService saves prices as-is, while the default implementation of IPriceService does not: it optimizes prices before saving. Prices which are best for customers will be favored over the “less good” ones.

So here’s an example of how it optimizes prices: Assuming you input prices like this:

• Unit Price 100$, Valid From: 1/1/2017 to 12/31/2017
• Unit Price 80$, Valid From: 4/1/2017 to 6/30/2017
• Unit Price 60$, Valid Form: 5/1/2017 to 5/31/2017

The saved price will be:

• Unit Price 100$, Valid From: 1/1/2017 to 3/31/2017
• Unit Price 80$, Valid From: 4/1/2017 to 4/30/2017

• Unit Price 60$, Valid Form: 5/1/2017 to 5/31/2017
• Unit Price 80$, Valid From: 6/1/2017 to 6/30/2017
• Unit Price 100$, Valid From: 7/1/2017 to 12/31/2017

While the behavior is reasonable (I definitely like it, as a customer!), it might not be what you want. You might want to keep only the highest prices (Nooo!), or you want to keep all prices – and leave it for some logic to decide which price to pick. You can do the latter by using IPriceDetailService, but that is not without drawback: the implementation of IPriceDetailService does not have cache, so it can have performance implication as it will hit database every time. Also, it might no work in certain areas, because those use IPriceService internally.

The only way to do it is to implement your own IPriceService. While the concept is simple (implement all the methods & properties, then register it in  one of your IConfigurableModule), it’s usually easier said than done. IPriceService is a quite big interface – but you can probably rely on the default implementation (PriceServiceDatabase), and only implement SetCatalogEntryPrices(IEnumerable<CatalogKey> catalogKeys, IEnumerable<IPriceValue> priceValues) yourself.

It’s worth noting that it’s risky to rely on a concrete implementation, such as PriceServiceDatabase, as it can be changed to be internal in future releases.

However, it’s still not a trivial or easy task. You will have to optimize the prices the way you like, save it to database (you can call the existing stored procedure), and have to validate the cache for updated prices. Too much issues for a seemingly small problem – and you are having to do stuffs that you should not have to.

Worry no more. Commerce 11 comes with a new interface that allow you to control the optimization of prices:

namespace Mediachase.Commerce.Pricing
{
    /// <summary>
    /// Optimizes prices on certain criterias.
    /// </summary>
    public interface IPriceOptimizer
    {
        /// <summary>
        /// Optimizes a collection of prices, remove duplicated and unwanted prices.
        /// </summary>
        /// <param name="prices">The prices to optimized</param>
        /// <returns>A list of optimized prices.</returns>
        IEnumerable<IOptimizedPriceValue> OptimizePrices(IEnumerable<IPriceValue> prices);
    }
}

All you need to do now is to create an implementation of this interface, and register it in one of your IConfigurableModule , and you’re good to go.

Here’s an example of keeping highest price:

    public class HighestPriceOptimizer : IPriceOptimizer
    {
        public IEnumerable<IOptimizedPriceValue> OptimizePrices(IEnumerable<IPriceValue> prices)
        {
            return prices.GroupBy(p => new { p.CatalogKey, p.MinQuantity, p.MarketId, p.ValidFrom, p.CustomerPricing, p.UnitPrice.Currency })
                .Select(g => g.OrderByDescending(c => c.UnitPrice.Amount).First()).Select(p => new OptimizedPriceValue(p, null));
        }
    }

Note this is a simplified implementation and I ignore complicated cases, like when prices have overlapped time – but with some magic LINQ you can make that happens.

This still comes with a limitation: With same MarketId, CustomerPricing and Currency, the price must be unique for MinQuantity and ValidFrom – so you can’t just save everything like you do with IPriceDetailService . Again IPriceService is optimized for reading, so you want to avoid such duplication.

 

Leave a Reply

Your email address will not be published. Required fields are marked *