We are currently reworking the processor so it will work as you expect when returning the saved amount the engine will modify the dicscount of the ILineItem for you. This should be released in a couple weeks time.
1. In the version you are wroking with you will need to set the ILineItem.LineItemDiscountAmount or IlineItem.OrderLevelDiscountAmount manually.
2. You need to enable VNextWorkflows to save PromotionInformation right now. http://world.episerver.com/documentation/Items/Developers-Guide/EPiServer-Commerce/9/Marketing/promotions-and-workflow-activities-beta/
Mark, thanks for the answer, but I have more questions :)
1. Why order level discount should be applied to LineItem? And which line item should I use - first or it doesn't matter?
2. I have VNextWorkflows enabled (using code in initializable module). Should I call Cart Validate workflow or there are some new workflows to run?
1. Depends on the type of promotion. Order level promotions are stored with the distributed amount per item in OrderLevelDiscountAmount in order to be able process returns. For example $100 of your order would be would be $50 OrderLevelDiscount if you had two items that cost $100 each. Again we are changing this in the interface for promotion processors so you wont need to know these details just the amount you want to save. Also in Evaluate you should determine which lineitems matched in the promotion. Apply reward is supposed to use the items that were matched in evaluate. You also want to pass in the ContentLink to PromotionInformation so you know which sku was discounted for historical puprposes.
2. In your initalization module have a [ModuleDependency(typeof(Mediachase.Commerce.Initialization.CommerceInitialization))] Then just Addfeautre and EnableFeature no need to initalize. I will update the docuementation.
1. Thanks, this helps to understand the reason behind line item's OrderLevelDiscountAmount. But when I use IOrderFormCalculator's GetSubTotal() method, it calculates SubTotal taking OrderLevelDiscountAmount into account which is not right. SubTotal should be sum before applying OrderLevelDiscountAmount and Total should include OrderLevelDiscountAmount.
About ContentLink in the PromotionInformation - if I skip it I get Null reference exception when workflow runs, so I have to provide it always.
2. Now I use config file to enable new features and it works well.
Now I changed ApplyReward to this and it works:
public IEnumerable<PromotionInformation> ApplyReward() { if (Status != FulfillmentStatus.Fulfilled || OrderGroup == null || !HasLineItems()) { return new[] {NoReward()}; } var totalDiscount = CalculateDiscountAmount(); var lineItems = OrderGroup.Forms.First().Shipments.First().LineItems; var lineItemDiscount = totalDiscount/lineItems.Count; lineItems.ToList().ForEach(x => x.OrderLevelDiscountAmount = lineItemDiscount); return lineItems.Select(x => new PromotionInformation { Description = Description, IsActive = true, SavedAmount = lineItemDiscount, ContentLink = _referenceConverter.GetContentLink(x.Code) }); }
In the CalculateDiscountAmount() method I have to manually calculate line item total before discounts:
private decimal CalculateDiscountAmount() { var orderForm = OrderGroup.Forms.First(); if (!orderForm.Shipments.Any()) { return 0; } var itemTotal = orderForm.Shipments.First().LineItems.Sum(x => x.PlacedPrice*x.Quantity); var totalDiscount = itemTotal*(DiscountPercent/100M); return totalDiscount; }
It would be nice if IOrderFormCalculator would contain method which would calculate line item sum before any discounts and also fix GetSubTotal() as mentioned in 1..
While everything seems to work, there is one issue I found. When there is only one line item and I am trying to remove it I get this exception:
The provided content link does not have a value. Parameter name: contentLink [ArgumentNullException: The provided content link does not have a value. Parameter name: contentLink] EPiServer.Core.DefaultContentLoader.Get(ContentReference contentLink, LoaderOptions loaderOptions) +502 Mediachase.Commerce.Workflow.Activities.CalculateDiscountsVNextActivity.Execute(ActivityExecutionContext executionContext) +314 Mediachase.Commerce.WorkflowCompatibility.Activity.Execute() +40 Mediachase.Commerce.Engine.<>c__DisplayClass1`1.<Do>b__0() +314 Mediachase.Commerce.Engine.ActivityFlowRunner.Execute() +124 Mediachase.Commerce.Engine.ExecutionManager.ExecuteActivityFlow(String name, ActivityFlowContext context) +275
It seems that one Promotion in the cart has empty content link now. I am removing line item from cart like this:
var lineItem = cart.GetLineItem(code); PurchaseOrderManager.RemoveLineItemFromOrder(cart, lineItem.LineItemId);
Is it the right way to remove line item so that promotions will work?
We will look into IOrderFormCalculator.GetSubTotal it should be total before discounts, tax, handling etc.
Yes that is the correct way to remove the lineItem. It may be you need to save the cart for now to get past the error. We will look into this as well.
Thanks for the feedback
Just tried to save cart before running workflow and it didn't help. Still same error. Tried to save Cart with OrderRepository and also with cart.AcceptChanges() and no difference.
Can you check the PromotionInfromation table I think there might be some orphaned records in there that has no contentLink
I deleted the cart in Commerce Manager and started again. PromotionInformation table was empty. Added new item to the cart and applied discount. One record appeared in PromotionInformation table and it has valid ContentReference. Now I am trying to remove item and exception still appears. PromotionInformation table still have same record with valid ContentReference.
Okay, I will file a bug. PurchaseOrderManager.RemoveLineItemFromOrder should remove the promotionInformation for the remove lineitem. To work around I suggest you remove the PromotionInformation manually for removed LineItem
I am trying to remove PromotionInformation, but it seems to not work at least as I am doing it.
So I tried to remove it like this:
PurchaseOrderManager.RemoveLineItemFromOrder(cart, lineItem.LineItemId); var lineItemLink = _referenceConverter.GetContentLink(code); var orderGroup = (IOrderGroup) cart; var promotion = orderGroup.Promotions.FirstOrDefault(x => x.ContentLink.Equals(lineItemLink, true)); if (promotion != null) { orderGroup.Promotions.Remove(promotion); cart.PromotionInformationRepository.Service.Save(orderGroup.Promotions, cart.OrderGroupId); }
It finds promotion and removes from Promotions collection, but after save it doesn't remove promotions from database. I also tried to save Cart itself, but it didn't help.
OK, I looked into PromotionsInformationSave SP and see the problem there. It does not support deleting removed promotion information. So your solution is now call
cart.PromotionInformationRepository.Service.Delete(cart.OrderGroupId);
before saving.
I'll file a bug for this.
Thanks,
/Q
So it will delete all promotions and will add back those which should be in Promotions collection?
Yes it'll work that way. The "current" collection in OrderGroup.Promotions should win and be saved.
/Q
Now I do it like this:
var lineItem = cart.GetLineItem(code); PurchaseOrderManager.RemoveLineItemFromOrder(cart, lineItem.LineItemId); var lineItemLink = _referenceConverter.GetContentLink(code); var orderGroup = (IOrderGroup) cart; var promotion = orderGroup.Promotions.FirstOrDefault(x => x.ContentLink.Equals(lineItemLink, true)); if (promotion != null) { orderGroup.Promotions.Remove(promotion); cart.PromotionInformationRepository.Service.Delete(cart.OrderGroupId); cart.PromotionInformationRepository.Service.Save(orderGroup.Promotions, cart.OrderGroupId); } cart.RunWorkflow(OrderGroupWorkflowManager.CartValidateWorkflowName); cart.AcceptChanges()
But I still get same error running workflow when deleting line item first time. When checking DB, PromotionInformation is deleted. Line item still in cart.
Then I click delete in UI second time and line item gets deleted.
Seems that workflow still uses cached promotions from somewhere.
Thanks Quan and Mark for help!
Finally I get also removal of last item to work. The issue was in my promoton result class on ApplyReward. When there is no promotion I returned single promotion with IsActive = false, but for it to work properly I have to return empty sequence. So the code for ApplyReward now is:
public IEnumerable<PromotionInformation> ApplyReward() { if (Status != FulfillmentStatus.Fulfilled || OrderGroup == null || !HasLineItems()) { return Enumerable.Empty<PromotionInformation>(); } var lineItems = OrderGroup.Forms.First().Shipments.First().LineItems; lineItems.ToList().ForEach(x => x.OrderLevelDiscountAmount = CalculateOrderLevelDiscount(x)); return lineItems .Where(x => x.OrderLevelDiscountAmount > 0) .Select(x => new PromotionInformation { Description = Description, IsActive = true, SavedAmount = x.OrderLevelDiscountAmount, ContentLink = _referenceConverter.GetContentLink(x.Code) }); }
I am trying to use some of the new BETA features and have some questions how to properly use it. I am trying new Promotions and created my own promotion implementation like in this article: http://www.david-tec.com/2015/07/creating-a-custom-promotion-with-the-new-episerver-commerce-9-promotion-engine-beta---part-1/
There is a small difference in IPromotionResult implementation's ApplyReward method I made - I am not modifying anything, but returning reward I want to be applied to whole order (in the sample I skipped discount calculation and use constant):
I assumed that consumer of this method will apply SavedAmount to OrderGroup (Cart), but I am stuck to achieve it.
In the controller I tried to call IPromotionEngine's method Run, I tried to call Cart validate workflow, tried to save Cart with IOrderRepository, tried to save Cart just with Cart.AcceptChanges(), but my promotion is not applied.
This works partially:
It runs my ApplyReward method, but result is not used. I also see that after _promotionEngine.Run(cart) new promotion is added to ((IOrderGroup)cart).Promotions collection, but _orderRepository.Save(cart) seems not saving items in this collection. After loading cart again ((IOrderGroup)cart).Promotions collection is empty.
So the questions:
1. How to apply discount returned by ApplyReward or should ApplyReward update order?
2. How to save ((IOrderGroup)cart).Promotions that those doesn't disappear?