Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • James 7 posts 87 karma points notactivated
    1 week ago
    James
    0

    Vendr - Get Preview Price For Collection Of Products With Discount

    Hi,

    I have a store with a number of different Products.

    Each Product has a number of discount rules which act as price breaks. For example, if a user adds 5-9 of Product A to the cart, they get a slight reduction on the unit price. If they add 10+ they get a slightly bigger reduction.

    What I want to be able to do is show these price breaks upfront before they add to the cart. So for Product A I want to show a list of pack sizes:

    • "Add 5 at £x per item"
    • "Add 10 at £y per item"

    I can get the unit cost of a single item. I can then multiply this by 5 or 10 or any other amount. But what I need to do is also factor in the discount rule to the result.

    Is this possible, and if so, how is it done?

  • Matt Brailsford 2758 posts 13973 karma points MVP 7x c-trib
    1 week ago
    Matt Brailsford
    0

    Hi James,

    Not really. Discounts can only be run in the context of an order so the only way you could work that out is to create a dummy order, add the product in those quantities and then see what the price is after the discount. This is really rather intensive though.

    Generally at the moment we tend to have to say to duplicate the data, or if the discounts are percentage based, duplicate the percentages but work the prices out on the fly.

    The reason we can't really run discounts out of context though is because they can be made up of any number of rules and include many different types of rewards so it's really not possible to know what their outcome is without giving them an order to calculate against.

    I haven't looked into this yet, but I have been thinking how you could handle price breaks, but it would potentially be a bit of custom setup right now.

    My thought was could a product node use a nested content type editor to create a price list with each entry having a minimum quantity. Then, if you override the IProductAdapter so that it can return appropriate prices from the NC instance, that would make the prices available. Unfortunately the IProductAdapeter assumes a product only has one price, but you could then further override the IOrderLineCalculator which is responsible for calculating a unit price for an order line, and in here get the order line quantity, and then fetch the product node and work out which unit price should apply based on the quantity.

    Like I say, I haven't fully tested this yet, but in theory it should work.

    Hope something here helps

    Matt

  • Matt Brailsford 2758 posts 13973 karma points MVP 7x c-trib
    1 week ago
    Matt Brailsford
    1

    Ok, I did a quick test, and it does look like the order line calculator could work.

    What I've done is created a product node and added a price property as usual, but the I've also added another property with the alias priceBreaks which is a nested content instance made up of an element type with two properties

    • Price The price for this price break using the Vendr price property editor
    • Min Quantity A numeric input for the minimum number of items allowed for this price to apply.

    I've then created an IOrderLineCalculator which you can find the code for on a gist here https://gist.github.com/mattbrailsford/518c5771de89a0fc35d0644ca126d229

    using System;
    using System.Collections.Generic;
    using Umbraco.Core.Models.PublishedContent;
    using Umbraco.Web;
    using Vendr.Core.Calculators;
    using Vendr.Core.Models;
    using Vendr.Core.Services;
    using Vendr.Web.Models;
    
    namespace Vendr.DemoStore.Web.Calculators
    {
        public class PriceBreakOrderLineCalculator : OrderLineCalculator
        {
            private readonly IStoreService _storeService;
            private readonly IProductPriceFreezerService _productPriceFreezerService;
            private readonly IUmbracoContextAccessor _umbracoContextAccessor;
    
            public PriceBreakOrderLineCalculator(ITaxService taxService, IStoreService storeService, IProductPriceFreezerService productPriceFreezerService,
                IUmbracoContextAccessor umbracoContextAccessor) 
                : base(taxService, storeService, productPriceFreezerService)
            {
                _storeService = storeService;
                _productPriceFreezerService = productPriceFreezerService;
                _umbracoContextAccessor = umbracoContextAccessor;
            }
    
            public override Price CalculateOrderLineUnitPrice(OrderReadOnly order, OrderLineReadOnly orderLine, Guid currencyId, TaxRate taxRate)
            {
                var store = _storeService.GetStore(order.StoreId);
    
                // See if the product node has a priceBreaks value
                // if not, return the default calculated unit price
                var node = _umbracoContextAccessor.UmbracoContext.Content.GetById(Guid.Parse(orderLine.ProductReference));
                if (!node.HasValue("priceBreaks"))
                    return base.CalculateOrderLineUnitPrice(order, orderLine, currencyId, taxRate);
    
                // If we have price breaks, thaw any price we hold
                _productPriceFreezerService.ThawFrozenProductPrice(order.StoreId, order.Id, orderLine.ProductReference, currencyId);
    
                // Work out which price break applies
                var priceBreaks = node.Value<IEnumerable<IPublishedElement>>("priceBreaks");
    
                IPublishedElement targetPriceBreak = null;
    
                foreach (var priceBreak in priceBreaks)
                {
                    var minQuantity = priceBreak.Value<int>("minQuantity");
                    if (orderLine.Quantity >= minQuantity)
                    {
                        targetPriceBreak = priceBreak;
                    }
                    else
                    {
                        break;
                    }
                }
    
                if (targetPriceBreak == null)
                    throw new Exception("No valid price break found");
    
                // Get the price from the price break
                var prices = targetPriceBreak.Value<PricePropertyValue>("price");
                var price = prices.GetPriceFor(currencyId);
    
                // Return the price
                return Price.Calculate(price.Value, taxRate, currencyId, store.PricesIncludeTax);
            }
        }
    }
    

    This is then setup to replace the existing order line calculator by adding the following in a composer.

    composition.RegisterUnique<IOrderLineCalculator, PriceBreakOrderLineCalculator>();
    

    Now when you add a product to the order, when it exceeds a minimum quantity of a price break, the unit price becomes the new price.

    There are a few elements missing here though. You'd need to handle displaying the price breaks on the product page in some way outside of Vendr. Likely you'll probably need to loop the nested content item, then retrieve the price for the current currency and format the value yourself. This is normally taken care of by Vendr, but as it only expects one price, it can't currently handle this.

    Also, I've kinda disabled price freezing if price breaks apply. I need to think harder whether we can freeze the price here (I think we can, but I'll need to think harder).

    Anywho. It's up to you which approach you take, but thought I'd try this out anyway to see if it is possible, which it is.

    Matt

  • James 7 posts 87 karma points notactivated
    1 week ago
    James
    0

    That's really helpful Matt, thanks.

    I have used your PriceBreakOrderLineCalculator example and it seems to be working fine. I can output the prices upfront easily with a custom partial of my nested content model providing the logic.

    1. What are the implications of the price freeze being disabled?
    2. Any other things to watch out for if I continue down this route?

    James

  • Matt Brailsford 2758 posts 13973 karma points MVP 7x c-trib
    1 week ago
    Matt Brailsford
    0

    Hi James

    Cool. I’ll review it again tomorrow now I know you are going down that route and investigate the ramifications of the price freezing.

    I think beyond that, everything else should be fine. Maybe the only other minor thing is a lack of any indication why the price is less, but I don’t think that’s a major problem.

    Matt

  • James 7 posts 87 karma points notactivated
    1 week ago
    James
    0

    Some potentially strange behaviour now. My PriceBreakOrderLineCalculator.CalculateOrderLineUnitPrice(...) method is never hit and the price is now defaulting to the one on the Product, not the PriceBreaks nested content. I'm sure it was working earlier.

    Anything obvious to check? I have it registered in a composer as you suggested but at no point during run time is the method called. Adding, removing from cart does not hit it.

  • Matt Brailsford 2758 posts 13973 karma points MVP 7x c-trib
    1 week ago
    Matt Brailsford
    0

    Have you set your composer to compose after the Vendr one?

    https://vendr.net/docs/core/1-2-0/key-concepts/dependency-injection/#registering-dependencies

    Matt

  • James 7 posts 87 karma points notactivated
    1 week ago
    James
    0

    I have now! Sorry, should have checked that. Sorted now. Thanks again, your solution seems to be working very nicely.

    James

  • Matt Brailsford 2758 posts 13973 karma points MVP 7x c-trib
    1 week ago
    Matt Brailsford
    101

    Ok, here we go. I've updated the order line calculator to now freeze prices for the price breaks also. 😁😎

    I have to bypass the product prize freezer and work directly with the underlying price freezer service, but this all looks to work.

    I've updated the original Gist at https://gist.github.com/mattbrailsford/518c5771de89a0fc35d0644ca126d229 but you can also find the code below.

    using System;
    using System.Collections.Generic;
    using Umbraco.Core.Models.PublishedContent;
    using Umbraco.Web;
    using Vendr.Core.Calculators;
    using Vendr.Core.Models;
    using Vendr.Core.Services;
    using Vendr.Web.Models;
    
    namespace Vendr.DemoStore.Web.Calculators
    {
        public class PriceBreakOrderLineCalculator : OrderLineCalculator
        {
            private readonly IStoreService _storeService;
            private readonly IPriceFreezerService _priceFreezerService;
            private readonly IUmbracoContextAccessor _umbracoContextAccessor;
    
            public PriceBreakOrderLineCalculator(ITaxService taxService, IStoreService storeService, IProductPriceFreezerService productPriceFreezerService,
                IPriceFreezerService priceFreezerService, IUmbracoContextAccessor umbracoContextAccessor) 
                : base(taxService, storeService, productPriceFreezerService)
            {
                _storeService = storeService;
                _priceFreezerService = priceFreezerService;
                _umbracoContextAccessor = umbracoContextAccessor;
            }
    
            public override Price CalculateOrderLineUnitPrice(OrderReadOnly order, OrderLineReadOnly orderLine, Guid currencyId, TaxRate taxRate)
            {
                var store = _storeService.GetStore(order.StoreId);
    
                // See if the product node has a priceBreaks value
                var node = _umbracoContextAccessor.UmbracoContext.Content.GetById(Guid.Parse(orderLine.ProductReference));
                if (node.HasValue("priceBreaks"))
                {
                    // Work out which price break applies (if any)
                    var priceBreaks = node.Value<IEnumerable<IPublishedElement>>("priceBreaks");
    
                    IPublishedElement targetPriceBreak = null;
    
                    foreach (var priceBreak in priceBreaks)
                    {
                        var minQuantity = priceBreak.Value<int>("minQuantity");
                        if (orderLine.Quantity >= minQuantity)
                        {
                            targetPriceBreak = priceBreak;
                        }
                        else
                        {
                            break;
                        }
                    }
    
                    // See if we have a price break
                    if (targetPriceBreak != null)
                    {
                        // See if we have a frozen price for this price break and if so, use it
                        var key = $"{order.StoreId}_{order.Id}_{orderLine.ProductReference}_{targetPriceBreak.Key}";
    
                        var price = _priceFreezerService.GetFrozenPrice(order.Id, key, currencyId);
                        if (price == null || price.Equals(ProductPrice.ZeroValue(currencyId)))
                        {
                            // No frozen price yet, so work out the price and freeze it (if we can)
                            var priceBreakPrices = targetPriceBreak.Value<PricePropertyValue>("price");
                            var priceBreakPrice = priceBreakPrices.GetPriceFor(currencyId);
                            if (priceBreakPrice != null)
                            {
                                // Freeze the price if the order isn't new
                                // (ie, it doesn't exist in the DB yet and so an FK would fail)
                                if (!order.IsNew)
                                {
                                    _priceFreezerService.FreezePrice(order.Id, key, priceBreakPrice);
                                }
    
                                price = priceBreakPrice;
                            }
                        }
    
                        return Price.Calculate(price.Value, taxRate, currencyId, store.PricesIncludeTax);
                    }
                }
    
                return base.CalculateOrderLineUnitPrice(order, orderLine, currencyId, taxRate);
            }
        }
    }
    
  • James 7 posts 87 karma points notactivated
    1 week ago
    James
    0

    Brilliant Matt, thanks. I will give this version a go. Will report back if I encounter any issues.

    James

Please Sign in or register to post replies

Write your reply to:

Draft