Preview shipping price with marketing campaigns rules
Hi all.
Running Umbraco 7.15.3 and TeaCommerce 3.3.2
I am working on a custom ShippingCalculator, that can preview shipping prices based on rules and shipping price awards from the marketing campaigns, before a shipping method is added to the order.
But there are some rule methods i do not understand. These are OrderLineAmountRule, ProductRule and PropertyRule, as they all require previous forfilled orderlines and accumulate statement.
How do i get previous forfilled orderlines?
What is the purpose of accumulate?
Is DiscountCodeRule.IsFulfilledBy broke?
FreeShippingCalculator.cs
namespace BiggusDickus.TeaCommerce.Calculators
{
[SuppressDependency("TeaCommerce.Api.PriceCalculators.IShippingCalculator", "TeaCommerce.Api")]
public class FreeShippingCalculator : ShippingCalculator
{
public FreeShippingCalculator(IVatGroupService vatGroupService) : base(vatGroupService) { }
public override Price CalculatePrice(ShippingMethod shippingMethod, Currency currency, Order order)
{
if (order == null) return base.CalculatePrice(shippingMethod, currency, null);
decimal returnedShippingPrice;
var currentShippingPrice = returnedShippingPrice = base.CalculatePrice(shippingMethod, currency, order).WithVat;
// Shipping discount can only be set on Order Amount Awards.
var campaigns = CampaignService.Instance.GetAllActive(order.StoreId).Where(c => c.Awards.Any(a => a.AwardAlias == "OrderAmountAward"));
foreach (var campaign in campaigns)
{
var awards = campaign.Awards.Select(x => x as OrderAmountAward).ToList();
if (awards.Any(x => x.Type == OrderAmountAwardType.ShipmentPrice))
{
// RuleGroups is 'AND' statements where Rules are 'OR' statements.
var groupPassed = new bool[campaign.RulesGroups.Count];
for (var i = 0; i < campaign.RulesGroups.Count; i++)
{
foreach (var rule in campaign.RulesGroups[i].Rules)
{
var rulePasses = false;
// OBS! Some rules need implementation.
switch (rule.RuleAlias)
{
case "DiscountCodeRule":
if (rule is DiscountCodeRule discountCodeRule)
{
/*
* Does not work as expected. Return false when added a approved discount code.
* rulePasses = discountCodeRule.IsFulfilledBy(order);
*/
// This do however seems to work as expected.
foreach (var appliedDiscountCode in order.DiscountCodes.Where(x => x.IsFulfilled))
{
rulePasses = DiscountCodeService.Instance.GetValid(order.StoreId, appliedDiscountCode.Code, discountCodeRule.Id) != null && !rulePasses;
}
}
break;
case "OrderAmountRule":
if (rule is OrderAmountRule amountRule)
{
// Seems to work as expected
rulePasses = amountRule.IsFulfilledBy(order);
}
break;
case "OrderLineAmountRule":
if (rule is OrderLineAmountRule lineAmountRule)
{
/*
* Cannot get this to work as lineAmountRule.IsFulfilledBy requires Previous Fulfilled Orderlines and i do not know how to get these.
* I do also not know what lineAmountRule.Accumulate is or does.
*/
}
break;
case "OrderLineQuantityRule":
if (rule is OrderLineQuantityRule lineQuantityRule)
{
/*
* Cannot get this to work as lineQuantityRule.IsFulfilledBy requires Previous Fulfilled Orderlines and i do not know how to get these.
* I do also not know what lineQuantityRule.Accumulate is or does.
*/
}
break;
case "PaymentMethodRule":
if (rule is PaymentMethodRule paymentMethodRule)
{
rulePasses = order.PaymentInformation.PaymentMethodId.HasValue && order.PaymentInformation.PaymentMethodId.Value == paymentMethodRule.PaymentMethodId;
}
break;
case "ProductRule":
if (rule is ProductRule productRule)
{
/*
* Cannot get this to work as productRule.IsFulfilledBy requires Previous Fulfilled Orderlines and i do not know how to get these.
* I do also not know what lineAmountRule.Accumulate is or does.
*/
}
break;
case "PropertyRule":
if (rule is PropertyRule propertyRule)
{
/*
* Cannot get this to work as lineQuantityRule.IsFulfilledBy requires Previous Fulfilled Orderlines and i do not know how to get these.
* I do also not know what lineAmountRule.Accumulate is or does.
*/
}
break;
case "ShippingMethodRule":
if (rule is ShippingMethodRule shippingMethodRule)
{
rulePasses = shippingMethodRule.ShippingMethodId.HasValue && shippingMethodRule.ShippingMethodId.Value == shippingMethod.Id;
}
break;
}
// Breaking the foreach loop (NOT THE "FOR") if the rule passes.
if (!rulePasses) continue;
groupPassed[i] = true;
break;
}
}
// We only handle the awards if all RuleGroups have passed. This means all the ruleGroups are forfilled.
if (!groupPassed.Contains(false))
{
foreach (var award in awards.Where(x => x.Type == OrderAmountAwardType.ShipmentPrice))
{
if (award.UsePercentage)
{
/*
* currentShippingPrice is used to calculate the procent from the base price, so you could set 4 discounts,
* one on 62.5,125,250 and 500. then it would add 100% discount in total when you buy for more than 500.
* The percentage received from the award is converted from 100% to 1 and 50% to 0.5.
*/
var discount = award.Percentage * currentShippingPrice ?? 0M;
returnedShippingPrice -= discount;
}
else
{
var awardAmount = award.Amounts.First();
returnedShippingPrice -= awardAmount.Value;
}
}
// If the calculations is lower than zero, then we set the price to zero, so we don't loose money.
if (returnedShippingPrice < 0M)
{
returnedShippingPrice = 0M;
}
}
}
}
return new Price(returnedShippingPrice, order.VatRate, currency);
}
}
}
using System.Linq;
using TeaCommerce.Api.Dependency;
using TeaCommerce.Api.Marketing.Models.Rules;
using TeaCommerce.Api.Marketing.Services;
using TeaCommerce.Api.Models;
using TeaCommerce.Api.PriceCalculators;
using TeaCommerce.Api.Services;
using TeaCommerce.Api.Marketing.Models.Awards;
using TeaCommerce.Umbraco.Configuration.Marketing.Models.Rules;
The way order line rules work is that they act as a filter so order lines that match the first rule are fed into the second rule etc so it narrows them down such that by the end of it, the only items left are ones that match all the rules.
The "Accumulate" flag should denote not to do this such it should apply across all order lines.
On the initial pass, for fulfilledOrderlines you should pass all the order order lines.
RE IsFullfiledBy being broken, in what sense do you mean?
I am still confused about the accumulate, so bare with me. If i use the rule method OrderLine Amount Rule and set it to Total Price and check the accumulate box. How would that differ from not checking it?
On the OrderLineAmountRule.IsFulfilledBy, do i simply add order and the order.OrderLines unfiltered like OrderLineAmountRule.IsFulfilledBy(order, order.OrderLines) and then check if it returns Any()?
The DiscountCodeRule.IsFulfilledBy is not broke, but it return false when it should return true. I checked this by adding some discount code and entered one of them to the order and then checked the price afterwards.
I added the discount code rules and download the list
Then i used the code HTVHY5X5SP
But the discountCodeRule.IsFulfilledBy(order) returns false, when it should return true.
It's my understanding (which may be wrong as I didn't write this implementation and I find it somewhat confusing myself) that with accumulate set to false, running order lines rules, the first rule will run and determine which orderlines meet that rule, it then passes those order lines into the next rule to filter down, so by the end of all the rules, the order lines it returns will be all the order lines that meet ALL the rules. Where as, accumulate should not filter so it should end up returning ANY order lines that meet any of the rules.
RE IsFulfilledBy, that's correct, however, I think there is more to the process that just running that method.
If you want to apply all the discounts to an order you should be able to call DiscountService.Instance.ApplyDiscounts(order) to have them all applied. This will then run through all the rules and apply them. Could you do this, then inspect the order to see what discounts are applied?
So basically if the rule passes with accumulate checked on a order line rule amount, then it also pass on another order line rule amount with accumulate checked?
My goal is to be able to use the marketing campaigns rules that have a shipping discount, to display the real shipping price before the customer add the shipping method to the order.
I was hopeing this could have done the trick :)
if (rule is OrderLineAmountRule lineAmountRule)
{
rulePasses = lineAmountRule.IsFulfilledBy(order, order.OrderLines).Any();
}
The DiscountService.Instance.ApplyDiscounts(order) applied the free shipping. Do you recommend i use it on the CustomShippingCalculator or is it not supposed to be used like that?
I could also use it where i apply the discount code, TC.AddDiscountCode(model.StoreId, model.DiscountCode) ?
In all honesty I'm getting confused with accumulate myself 😂
The best way I could think to do this would by to have some kind of button to say "Calculate Shipping". When clicked, it could go to a custom API controller or something and this could take the current order, deep clone it, then run the calculator to see what the shipping would be and return that value.
By deep cloning you don't affect the actual current order, and once you've got your value, you can throw the clone away.
Would a simply copy do it var orderCopy = order.Copy() or do i need to use MemberwiseClone, since i think the Order object is not [Serializable] for the BinaryFormatter
Finally question, how do i run the calculator on a clone? As far as i can see, the Shipping Calculator is called by TC.GetPriceForShippingMethod(storeId, ShippingMethodId) and TC is session based?
order.Copy() should do the trick for cloning wise.
I think from a calculation perspective, you'll need to do two things.
1) Call DiscountService.Instance.ApplyDiscounts(order) to apply discounts
2) Call OrderCalculator.Instance.CalculateOrder(order, OrderCalculationMode.All) which will recalculate the entire order including shipping
I think this would be your safest bet as running individual calculators is likely to miss things (ie, the ShippingCalculator only calculates the regular shipping price, discounts are applied afterwards).
I ended up with static class instaed of a CustomShippingCalculator. Because i found out that OrderCalculator.Instance.CalculateOrder(orderCopy, OrderCalculationMode.All) calls the public override Price CalculatePrice from the ShippingCalculator
public static Price GetShippingPriceBeforeAdded(Order order, long shippingMethodId)
{
if (order == null) return null;
var orderCopy = order.Copy();
orderCopy.ChangeShippingMethod(shippingMethodId);
DiscountService.Instance.ApplyDiscounts(orderCopy);
OrderCalculator.Instance.CalculateOrder(orderCopy, OrderCalculationMode.All);
var price = orderCopy.ShipmentInformation.TotalPrice.Value.WithVat;
orderCopy.DisposeIfDisposable();
var currency = TC.GetCurrency(order.StoreId, order.CurrencyId);
return new Price(price, order.VatRate, currency);
}
Preview shipping price with marketing campaigns rules
Hi all.
Running Umbraco 7.15.3 and TeaCommerce 3.3.2
I am working on a custom ShippingCalculator, that can preview shipping prices based on rules and shipping price awards from the marketing campaigns, before a shipping method is added to the order.
But there are some rule methods i do not understand. These are OrderLineAmountRule, ProductRule and PropertyRule, as they all require previous forfilled orderlines and accumulate statement.
FreeShippingCalculator.cs
The usings could not be inserted.
Hey Bo,
The way order line rules work is that they act as a filter so order lines that match the first rule are fed into the second rule etc so it narrows them down such that by the end of it, the only items left are ones that match all the rules.
The "Accumulate" flag should denote not to do this such it should apply across all order lines.
On the initial pass, for fulfilledOrderlines you should pass all the order order lines.
RE IsFullfiledBy being broken, in what sense do you mean?
Matt
Hi Matt.
Thanks for replying.
I am still confused about the accumulate, so bare with me. If i use the rule method OrderLine Amount Rule and set it to Total Price and check the accumulate box. How would that differ from not checking it?
On the OrderLineAmountRule.IsFulfilledBy, do i simply add order and the order.OrderLines unfiltered like OrderLineAmountRule.IsFulfilledBy(order, order.OrderLines) and then check if it returns Any()?
The DiscountCodeRule.IsFulfilledBy is not broke, but it return false when it should return true. I checked this by adding some discount code and entered one of them to the order and then checked the price afterwards.
I added the discount code rules and download the list
Then i used the code HTVHY5X5SP
But the discountCodeRule.IsFulfilledBy(order) returns false, when it should return true.
Hey Bo,
It's my understanding (which may be wrong as I didn't write this implementation and I find it somewhat confusing myself) that with accumulate set to false, running order lines rules, the first rule will run and determine which orderlines meet that rule, it then passes those order lines into the next rule to filter down, so by the end of all the rules, the order lines it returns will be all the order lines that meet ALL the rules. Where as, accumulate should not filter so it should end up returning ANY order lines that meet any of the rules.
RE IsFulfilledBy, that's correct, however, I think there is more to the process that just running that method.
If you want to apply all the discounts to an order you should be able to call
DiscountService.Instance.ApplyDiscounts(order)
to have them all applied. This will then run through all the rules and apply them. Could you do this, then inspect the order to see what discounts are applied?Hi Matt.
So basically if the rule passes with accumulate checked on a order line rule amount, then it also pass on another order line rule amount with accumulate checked?
My goal is to be able to use the marketing campaigns rules that have a shipping discount, to display the real shipping price before the customer add the shipping method to the order.
I was hopeing this could have done the trick :)
The
DiscountService.Instance.ApplyDiscounts(order)
applied the free shipping. Do you recommend i use it on the CustomShippingCalculator or is it not supposed to be used like that?I could also use it where i apply the discount code,
TC.AddDiscountCode(model.StoreId, model.DiscountCode)
?Hey Bo,
In all honesty I'm getting confused with accumulate myself 😂
The best way I could think to do this would by to have some kind of button to say "Calculate Shipping". When clicked, it could go to a custom API controller or something and this could take the current order, deep clone it, then run the calculator to see what the shipping would be and return that value.
By deep cloning you don't affect the actual current order, and once you've got your value, you can throw the clone away.
That's how I would tackle it anyways.
Matt
Hi Matt.
Would a simply copy do it
var orderCopy = order.Copy()
or do i need to useMemberwiseClone
, since i think the Order object is not[Serializable]
for theBinaryFormatter
Finally question, how do i run the calculator on a clone? As far as i can see, the Shipping Calculator is called by
TC.GetPriceForShippingMethod(storeId, ShippingMethodId)
and TC is session based?I was thinking something like this
Hey Bo,
order.Copy()
should do the trick for cloning wise.I think from a calculation perspective, you'll need to do two things.
1) Call
DiscountService.Instance.ApplyDiscounts(order)
to apply discounts2) Call
OrderCalculator.Instance.CalculateOrder(order, OrderCalculationMode.All)
which will recalculate the entire order including shippingI think this would be your safest bet as running individual calculators is likely to miss things (ie, the ShippingCalculator only calculates the regular shipping price, discounts are applied afterwards).
Hope this helps
Matt
Okay Matt.
I ended up with static class instaed of a CustomShippingCalculator. Because i found out that
OrderCalculator.Instance.CalculateOrder(orderCopy, OrderCalculationMode.All)
calls thepublic override Price CalculatePrice
from theShippingCalculator
Thanks for the help :)
Hey Bo,
No worries. Glad you were able to get there in the end 👍
Matt
is working on a reply...