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.
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.
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.
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:
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
Extend the order pipeline by injecting a task
In the task, you can access the order via args.Order
Don't use any prices on the Order object, instead use the current args.Calculation for the in progress calculation values
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?
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
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.
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.
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.
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.
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.
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.
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
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.
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);
}
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?
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
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.
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:
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 theargs.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 theargs.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:
For your situation I think you'll want to register your task after the built in
CalculateOrderTotalPriceWithoutDiscountTask
task which calculates theTotalPrice.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
args.Order
args.Calculation
for the in progress calculation values/Matt
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/
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
?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
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 likeRegarding 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.
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.
Hi Matt
Okay, I guess it could look something like this:
You mentioned the prices should be using
args.Calculation
instead of prices fromargs.Order
.The
args.Calculation
contains a propertyOrderLines
which isDictionary<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
orargs.Calculation.OrderLines
?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?
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
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(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.
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.
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.
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
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:
Not sure what the purpose of
AdditionalData
property?args.Order
is anOrderReadOnly
object and thereforeProperties
is readonly.So it seems I need to do something like this to set order property
As I mentioned previously,
args.Order
is already a writable order, it's just down-cast toOrderReadOnly
so you should probably be able to do the following: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
Hi Matt
I doesn't seem to be possible to cast
args.order
(which isOrderReadOnly
) toOrder
.It throw the following exception.
Tested with Vendr v1.1.3
Setting the property using
IUnitOfWorkProvider
approach does work though and not necessarySave()
usingOrderService
here.Ahh, ok. Maybe I am converting it to ReadOnly rather than just casting it, in which the UOW approach should be fine.
Yeah, this would work instead.
is working on a reply...