Copied to clipboard

Flag this post as spam?

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


  • Søren Mastrup 116 posts 536 karma points c-trib
    25 days ago
    Søren Mastrup
    0

    Add custom fee to order

    I have been trying to add a fee to an order if certain conditions is met. In my specific case I need to add the fee if the total price of orderlines, with a certain property, is above a specific amount.

    My approach until now has been to do something on "ValidateOrderSave", but I am unsure on how I should add the fee to the order.

    Any pointers on how to do this?

  • Matt Brailsford 2481 posts 12084 karma points MVP 7x c-trib
    25 days ago
    Matt Brailsford
    0

    Hi Soren,

    ValidateOrderSave probably isn't the right place as the Validation events should only ever be used to validate whether an action is allowed to occur or not.

    I guess the first question is when you say "is above a specific amount" are you referring to the order line total (unit price * quantity)? Or the the products unit price?

    There are a few options, but it kinda of depends on the answer to the above question first and foremost.

    Matt

  • Søren Mastrup 116 posts 536 karma points c-trib
    25 days ago
    Søren Mastrup
    0

    It is the order line total I'm after.

    More specific: if the sum of the order line total of all order lines with a certain property is above a specific amount I need to add a fee to the order.

  • Matt Brailsford 2481 posts 12084 karma points MVP 7x c-trib
    25 days ago
    Matt Brailsford
    0

    Hi Soren,

    Ok, in the situation then, I think you might need to extend the price calculator pipeline.

    We don't have a lot of docs around this at the moment, but hopefully this will explain what you need to do.

    So the price calculator pipeline is a series of tasks that are run in sequence in order to calculate the Order total. With this pipeline, it's also possible to add your own tasks into it so you can alter how the order is calculated. In your situation, I think given you want to apply the fee at the order level, you'll want to add a task into the CalculateOrderPrices pipeline.

    The first thing you'll need to do is define a pipeline task, which will look like the following:

    public class MyOrderCalculationPipelineTask : PipelineTaskWithTypedArgsBase<OrderCalculationPipelineArgs, OrderCalculation>
    {
        public override PipelineResult<OrderCalculation> Execute(OrderCalculationPipelineArgs args)
        {
            /// Do your thing
    
            return Ok(args.Model);
        }
    }
    

    Inside this method you can modify the calculation. The calculation is at this point held seperate to the Order as we calculate everything in a pipeline and then apply it to the Order at the end. So, all the prices you might want to modify will be held in args.Calculation. For your situation I think you'll want to update the args.Calculation.TotalPrice.WithoutDiscounts.

    In the args object you'll also get access to the current Order via args.Order so from there you can loop over order lines looking for the ones that contain the property you need. Remember though, those OrderLines haven't currently been updated with the newly calculated prices yet so once you have found the order lines you are interested in, you will want to look up their calculated prices in the args.Calculation.OrderLines property which is a Dictionary accepting an OrderLineId as it's key, and it's value is that order lines current calculation.

    So, from here you can loop through the order line prices and work out which ones exceed your limit and then keep a running fee total, and after this, you can append that fee to the args.Calculation.TotalPrice.WithoutDiscounts value.

    Ok, so now you have a task, you'll need to inject it into the pipeline. For this you'll need to have a composer defined inside which you'll need to register your task as follows:

    composition.WithCalculateOrderPricesPipeline()
        .InsertAfter<CalculateOrderTotalPriceWithoutDiscountTask, MyOrderCalculationPipelineTask>();
    

    For your situation I think you'll want to register your task after the built in CalculateOrderTotalPriceWithoutDiscountTasktask which calculates the TotalPrice.WithoutDiscounts value, from which you can append your fee, then things like discounts apply after this.

    Essentially above then, this is injecting your task to run after the CalculateOrderTotalPriceWithoutDiscountTask into the order calculation pipeline.

    And I think that's everything you should need to implement.

    It's a lot to explain, but hopefully it will make sense to you in the implementation. The key points are

    1. Extend the order pipeline by injecting a task
    2. In the task, you can access the order via args.Order
    3. Don't use any prices on the Order object, instead use the current args.Calculation for the in progress calculation values

    /Matt

  • Matt Brailsford 2481 posts 12084 karma points MVP 7x c-trib
    25 days ago
    Matt Brailsford
    0

    PS we do have some docs on the Pipelines concept, but it doesn't hold the detail mentioned above (it may still help give some context)

    https://vendr.net/docs/core/1-0-0/key-concepts/pipelines/

  • Bjarne Fyrstenborg 1149 posts 3275 karma points MVP 4x c-trib
    19 days ago
    Bjarne Fyrstenborg
    0

    Hi Matt

    In the pipeline task I guess the order lines includes all order lines in cart. Is it possible to add a fee via the calculation based on only some of the order lines, e.g. order lines with a specific property value?

    Where would you return the fee here? Should it set an updated value on a specific property in args?

    public class DepositOrderCalculationPipelineTask : PipelineTaskWithTypedArgsBase<OrderCalculationPipelineArgs, OrderCalculation>
    {
        public override PipelineResult<OrderCalculation> Execute(OrderCalculationPipelineArgs args)
        {
            var order = args.Order;
    
            // Get rental order lines
            var orderLines = order.OrderLines.Where(x => x.Properties["IsRental"]?.Value?.ToLower() == "true");
    
            var totalAmount = args.Calculation.OrderLines.Sum(x => x.Value.TotalPrice);
    
            if (totalAmount >= 2000)
            {
                // Add fee to order
            }
    
            return Ok(args.Model);
        }
    }
    

    If you wanted to show the "Deposit" amount on order confirmation page or in email confirmation, I guess you could set/remove an order property here, based on the match and where the fee is added/removed?

    Besides this I was wondering if in Vendr also would be possible to solve this using a different approach by adding e.g. a "Deposit" discount via code with a negative value, which would act as adding a fee? Not sure if this would make sense in Vendr?

    /Bjarne

  • Matt Brailsford 2481 posts 12084 karma points MVP 7x c-trib
    19 days ago
    Matt Brailsford
    0

    Hey Bjarne,

    I think what you have so far is right. In terms of applying the fee you'll want to replace the existing args.Calculation.TotalPrice.WithoutDiscounts with the new price including fee like

    args.Calculation.TotalPrice.WithoutDiscounts = Price.OfSameCurrency(args.Calculation.TotalPrice.WithoutDiscounts,
        args.Calculation.TotalPrice.WithoutDiscounts.WithoutTax + fee,
        args.Calculation.TotalPrice.WithoutDiscounts.Tax + feeTax);
    

    Regarding showing this on the order, yea, I'm not sure we have anything in place for this at the moment. Technically the args.Order is a writable order just down-cast to a read only order, so for now, you probably could cast it to it's writable form and add a property to it (you shouldn't need to save the order at this point as it will get saved after the calculation).

    Thinking about this from a higher level, I wonder if we need to introduce the concept of "Adjustments" to price objects where the price is calculated and these adjustments are applied and can also contain a label for it. Discounts and Gift Cards then could also become adjustments. When extending the pipelines it could then become easier as you could just add another adjustment to it.

    I'll need to give that some thought, but for now, I think the above is how you'd need to go for the time being.

  • Matt Brailsford 2481 posts 12084 karma points MVP 7x c-trib
    19 days ago
    Matt Brailsford
    0

    PS I haven't tried discounts with a negative value, but you could try it and see what happens. I think we may have some logic though which attempts to limit discounts so that could affect it, but I'm not quite sure.

  • Bjarne Fyrstenborg 1149 posts 3275 karma points MVP 4x c-trib
    19 days ago
    Bjarne Fyrstenborg
    0

    Hi Matt

    Okay, I guess it could look something like this:

    var fee = 500M;
    var feeTax = fee * args.Calculation.TaxRate.Value;
    
    args.Calculation.TotalPrice.WithoutDiscounts = 
                Price.OfSameCurrency(args.Calculation.TotalPrice.WithoutDiscounts,
                                    args.Calculation.TotalPrice.WithoutDiscounts.WithoutTax + fee,
                                    args.Calculation.TotalPrice.WithoutDiscounts.Tax + feeTax);
    

    You mentioned the prices should be using args.Calculation instead of prices from args.Order.

    The args.Calculation contains a property OrderLines which is Dictionary<Guid, OrderLineCalculation>. I guess the key isn't the order line guid?

    If the calculation only need to be based on order line total of some order lines (e.g. order lines with a specific property value), what is the best way to filter these?

    Using args.Order.OrderLines or args.Calculation.OrderLines?

    var orderLines = order.OrderLines.Where(x => x.Properties["IsRental"]?.Value?.ToLower() == "true");
    

    And then get sum of order line prices in args.Calculation.OrderLines?

    Regarding adding a manual discount (not sure if it would work), I guess it would append an discount to the existing discounts?

    try
    {
        using (var uow = _uowProvider.Create())
        {
            var o = args.Order.AsWritable(uow);
    
            var discount = new FulfilledDiscount(System.Guid.Empty, "Deposit", DiscountType.Automatic);
    
            o.Discounts.Append(discount);
    
            uow.Complete();
        }
    }
    catch
    {
    
    }
    

    I guess this won't work. It could use Redeem() like this, but I guess it require to code exists in Vendr.

    https://github.com/vendrhub/vendr-demo-store/blob/dev/src/Vendr.DemoStore/Web/Controllers/CheckoutSurfaceController.cs#L36

    Is there any logic in place at the moment to apply discounts from code based on some logic?

    /Bjarne

  • Matt Brailsford 2481 posts 12084 karma points MVP 7x c-trib
    19 days ago
    Matt Brailsford
    0

    Hey Bjarne,

    As I said to Soren, the args.Calculation.OrderLines is the list of all order line calculation key'd by the order line. This is just the calculations though, so if you need properties from the order lines, you will need to loop through the order lines on the order, but then lookup the calculation based on the order line id

         // Get rental order lines
        var orderLines = order.OrderLines.Where(x => x.Properties["IsRental"]?.Value?.ToLower() == "true");
    
        var totalAmount = orderLines.Sum(x => args.Calculation.OrderLines[x.Id].TotalPrice.Value);
    
        if (totalAmount >= 2000)
        {
            var fee = 500M;
            var feeTax = fee * args.Calculation.TaxRate.Value;
    
            args.Calculation.TotalPrice.WithoutDiscounts = Price.OfSameCurrency(args.Calculation.TotalPrice.WithoutDiscounts,
                args.Calculation.TotalPrice.WithoutDiscounts.WithoutTax + fee,
                args.Calculation.TotalPrice.WithoutDiscounts.Tax + feeTax);
        }
    

    (I haven't tested the above code, I'm just piecing it together based on the above posts)

    RE applying discounts based on some logic, this is exactly what automatic discounts are. They automatically apply to an order if some rules match, so I guess you'd need to create a custom rule based upon your need.

  • Matt Brailsford 2481 posts 12084 karma points MVP 7x c-trib
    19 days ago
    Matt Brailsford
    0

    I have thought of another way you could do this actually. If you created the fee as a product, you could do the calculation and dynamically add the fee as a product on the order.

  • Bjarne Fyrstenborg 1149 posts 3275 karma points MVP 4x c-trib
    19 days ago
    Bjarne Fyrstenborg
    0

    Yes, I thought about this as well, as it could add/remove the product from the order based on a condition. Not sure if this approach also would use this pipeline task or something else.

    However with this approach I think it by default show up when the other order lines, unless you exclude this order line. I think this would be more a fee similar to shipping and payment fees, so if shown on confirmation page, it would probably be listed below order lines, but I guess this depends on the use-case.

  • Matt Brailsford 2481 posts 12084 karma points MVP 7x c-trib
    19 days ago
    Matt Brailsford
    0

    Yea, I think probably the route you are going will be what you need, it's just not the smoothest yet. You are the pioneers 😁

    Hopefully the pipeline + order properties will do the trick for you for now but I have created an issue on github to track the idea of price adjustments here if you want to join the discussion https://github.com/vendrhub/vendr/issues/100

  • Bjarne Fyrstenborg 1149 posts 3275 karma points MVP 4x c-trib
    19 days ago
    Bjarne Fyrstenborg
    0

    If I want to set a ordre property here in this pipeline task, what is the best approach?

    It see I can do the following:

    if (totalAmount >= 2000)
    {
        // Add deposit fee to order
        ...
    
        args.AdditionalData.Add("DepositAmount", fee.ToString());
    }
    else
    {
        args.AdditionalData.Remove("DepositAmount");
    }
    
    return Ok(args.Model);
    

    Not sure what the purpose of AdditionalData property?

    args.Order is an OrderReadOnly object and therefore Properties is readonly.

    So it seems I need to do something like this to set order property

    try
    {
        using (var uow = _uowProvider.Create())
        {
            var o = args.Order.AsWritable(uow);
    
            o.SetProperty("DepositAmount", fee.ToString());
    
            uow.Complete();
        }
    }
    catch
    {
    
    }
    
  • Matt Brailsford 2481 posts 12084 karma points MVP 7x c-trib
    18 days ago
    Matt Brailsford
    0

    As I mentioned previously, args.Order is already a writable order, it's just down-cast to OrderReadOnly so you should probably be able to do the following:

    var writableOrder = (Order)args.Order;
    writableOrder.SetProperty("DepositAmount", fee.ToString());
    

    The args.AdditionalData is just a general data store for the pipeline itself in case any given pipeline task wants to store some data a later task in the pipeline might want to have access to. It bares no relevance on the order itself.

    /Matt

  • Bjarne Fyrstenborg 1149 posts 3275 karma points MVP 4x c-trib
    18 days ago
    Bjarne Fyrstenborg
    0

    Hi Matt

    I doesn't seem to be possible to cast args.order (which is OrderReadOnly) to Order.

    var writableOrder = (Order)args.Order;
    

    It throw the following exception.

    An object of type 'Vendr.Core.Models.OrderReadOnly' cannot be converted to type 'Vendr.Core.Models.Order'.

    Tested with Vendr v1.1.3

    Setting the property using IUnitOfWorkProviderapproach does work though and not necessary Save() using OrderService here.

  • Matt Brailsford 2481 posts 12084 karma points MVP 7x c-trib
    18 days ago
    Matt Brailsford
    0

    Ahh, ok. Maybe I am converting it to ReadOnly rather than just casting it, in which the UOW approach should be fine.

  • Bjarne Fyrstenborg 1149 posts 3275 karma points MVP 4x c-trib
    18 days ago
    Bjarne Fyrstenborg
    101

    Yeah, this would work instead.

    try
    {
        using (var uow = _uowProvider.Create())
        {
            var writableOrder = args.Order.AsWritable(uow);
    
            if (totalAmount >= 2000)
            {
                writableOrder.SetProperty("DepositAmount", fee.ToString());
            }
            else
            {
                writableOrder.RemoveProperty("DepositAmount");
            }
    
            // Order is saved it pipeline, so it shouldn't be necessary
            // to save order here using OrderService.
    
            uow.Complete();
        }
    }
    catch (Exception ex)
    {
        _logger.Error<DepositOrderCalculationPipelineTask>(ex);
    }
    
Please Sign in or register to post replies

Write your reply to:

Draft