The beauty of new promotion system

This is going to be a relatively short post. If you are using Episerver Commerce 9, you probably know that we are working on a new promotion system. It’s still BETA, but some of our customers already use it, and from what I heard they are really happy with it.

One of the reasons we create a new promotion system is the old one is not developer-friendly. Have you ever tried to create a promotion in old system, by code?

This is an “simple” example of how to create a new campaign and a couple of promotions:

private int CreateCampaigns()
        {
            var dto = new CampaignDto();
            var campaignRow = dto.Campaign.NewCampaignRow();
            campaignRow.Name = "QuickSilver";
            campaignRow.Created = DateTime.UtcNow;
            campaignRow.IsArchived = false;
            campaignRow.ApplicationId = AppContext.Current.ApplicationId;
            campaignRow.Modified = DateTime.UtcNow;
            campaignRow.IsActive = true;
            campaignRow.StartDate = DateTime.Today;
            campaignRow.EndDate = DateTime.Today.AddYears(1);
            dto.Campaign.AddCampaignRow(campaignRow);
            CampaignManager.SaveCampaign(dto);
            return dto.Campaign.First().CampaignId;
        }

        private void CreatePromotions(int campaignId)
        {
            var dto = new PromotionDto();

            CreatePromotion(dto, "25 % off Mens Shoes", 0.00m, PromotionType.Percentage, "EntryCustomDiscount", "entry", campaignId);
            CreatePromotion(dto, "$50 off Order over $500", 50.00m, PromotionType.ValueBased, "OrderVolumeDiscount", "order", campaignId);
            CreatePromotion(dto, "$10 off shipping from Women's Shoes", 10.00m, PromotionType.ValueBased, "BuySKUFromCategoryXGetDiscountedShipping", "shipping", campaignId);
            PromotionManager.SavePromotion(dto);
            dto = PromotionManager.GetPromotionDto();
            foreach (var promotion in dto.Promotion)
            {
                foreach (var languageCode in new[] { "en", "sv" })
                {
                    var name = String.Empty;
                    switch (promotion.PromotionId)
                    {
                        case 1:
                            name = languageCode.Equals("en") ? "25 % off Mens Shoes" : "25% rabatt på herrskor";
                            break;
                        case 2:
                            name = languageCode.Equals("en") ? "$50 off Order over $500" : "$50 rabatt på order över $500";
                            break;
                        case 3:
                            name = languageCode.Equals("en") ? "$10 off shipping from Women's Shoes" : "$10 rabatt på frakt av Damskor";
                            break;
                    }
                    var promoLanguageRow = dto.PromotionLanguage.NewPromotionLanguageRow();
                    promoLanguageRow.PromotionId = promotion.PromotionId;
                    promoLanguageRow.LanguageCode = languageCode;
                    promoLanguageRow.DisplayName = name;
                    dto.PromotionLanguage.Rows.Add(promoLanguageRow);
                }
            }
            PromotionManager.SavePromotion(dto);
            UpdatePromotionParams();
            CreateExpression(1, "25 % off Mens Shoes");
            CreateExpression(2, "$50 off Order over $500");
            CreateExpression(3, "$10 off shipping from Women's Shoes");

        }

        private void CreatePromotion(PromotionDto dto, string name, decimal reward, PromotionType rewardType, string promotionType, string promotionGroup, int campaignId)
        {
            var promotionRow = dto.Promotion.NewPromotionRow();
            promotionRow.ApplicationId = AppContext.Current.ApplicationId;
            promotionRow.Name = name;
            promotionRow.StartDate = DateTime.Today;
            promotionRow.EndDate = DateTime.Today.AddYears(1);
            promotionRow.Created = DateTime.UtcNow;
            promotionRow.ModifiedBy = "admin";
            promotionRow.Status = "active";
            promotionRow.OfferAmount = reward;
            promotionRow.OfferType = (int)rewardType;
            promotionRow.PromotionGroup = promotionGroup;
            promotionRow.CampaignId = campaignId;
            promotionRow.ExclusivityType = "none";
            promotionRow.PromotionType = promotionType;
            promotionRow.PerOrderLimit = 0;
            promotionRow.ApplicationLimit = 0;
            promotionRow.CustomerLimit = 0;
            promotionRow.Priority = 1;
            promotionRow.CouponCode = "";
            if (name.Equals("25 % off Mens Shoes"))
            {
                promotionRow.OfferAmount = 25m;
                promotionRow.OfferType = 0;
            }
            //In commerce manager, this promotion type is displayed quite differently from others. 
            //The percentage based offer type has value "0" and value based offer type has value "1". 
            //But in C# code, PromotionType.Percentage = 1 and PromotionType.ValueBased = 2
            else if (name.Equals("$10 off shipping from Women's Shoes"))
            {
                promotionRow.OfferType = 1;
            }
            dto.Promotion.Rows.Add(promotionRow);
            return;
        }

        private void UpdatePromotionParams()
        {
            var sql = "EXEC(N'UPDATE [dbo].[Promotion] SET Params = 0x0001000000ffffffff01000000000000000c02000000534d6564696163686173652e436f6e736f6c654d616e616765722c2056657273696f6e3d382e31312e342e3635362c2043756c747572653d6e65757472616c2c205075626c69634b6579546f6b656e3d6e756c6c0c03000000634d6564696163686173652e427573696e657373466f756e646174696f6e2c2056657273696f6e3d382e31312e342e3635362c2043756c747572653d6e65757472616c2c205075626c69634b6579546f6b656e3d34316432653761363135626132383663050100000036417070735f4d61726b6574696e675f50726f6d6f74696f6e735f456e747279437573746f6d446973636f756e742b53657474696e67730200000013436f6e646974696f6e45787072657373696f6e1054617267657445787072657373696f6e04043c4d6564696163686173652e427573696e657373466f756e646174696f6e2e46696c74657245787072657373696f6e4e6f6465436f6c6c656374696f6e030000003c4d6564696163686173652e427573696e657373466f756e646174696f6e2e46696c74657245787072657373696f6e4e6f6465436f6c6c656374696f6e03000000020000000904000000090500000005040000003c4d6564696163686173652e427573696e657373466f756e646174696f6e2e46696c74657245787072657373696f6e4e6f6465436f6c6c656374696f6e020000000b5f706172656e744e6f646512436f6c6c656374696f6e60312b6974656d730403324d6564696163686173652e427573696e657373466f756e646174696f6e2e46696c74657245787072657373696f6e4e6f646503000000bc0153797374656d2e436f6c6c656374696f6e732e47656e657269632e4c69737460315b5b4d6564696163686173652e427573696e657373466f756e646174696f6e2e46696c74657245787072657373696f6e4e6f64652c204d6564696163686173652e427573696e657373466f756e646174696f6e2c2056657273696f6e3d382e31312e342e3635362c2043756c747572653d6e65757472616c2c205075626c69634b6579546f6b656e3d343164326537613631356261323836635d5d0300000009060000000907000000010500000004000000090800000009090000000c0a0000004953797374656d2c2056657273696f6e3d342e302e302e302c2043756c747572653d6e65757472616c2c205075626c69634b6579546f6b656e3d623737613563353631393334653038390506000000324d6564696163686173652e427573696e657373466f756e646174696f6e2e46696c74657245787072657373696f6e4e6f64650b0000000b5f617474726962757465730b5f6368696c644e6f6465730c5f6465736372697074696f6e045f6b65790b5f706172656e744e6f6465095f726561646f6e6c79055f6e616d650a5f636f6e646974696f6e065f76616c7565095f6e6f646554797065075f6d6574686f6404040101040001040204043253797374656d2e436f6c6c656374696f6e732e5370656369616c697a65642e4e616d6556616c7565436f6c6c656374696f6e0a0000003c4d6564696163686173652e427573696e657373466f756e646174696f6e2e46696c74657245787072657373696f6e4e6f6465436f6c6c656374696f6e03000000324d6564696163686173652e427573696e657373466f756e646174696f6e2e46696c74657245787072657373696f6e4e6f646503000000012e4d6564696163686173652e427573696e657373466f756e646174696f6e2e436f6e646974696f6e456c656d656e7403000000364d6564696163686173652e427573696e657373466f756e646174696f6e2e46696c74657245787072657373696f6e4e6f646554797065030000002b4d6564696163686173652e427573696e657373466f756e646174696f6e2e4d6574686f64456c656d656e7403000000030000000a0904000000060c00000000060d0000002430303030303030302d303030302d303030302d303030302d3030303030303030303030300a01090c0000000a0a05f1ffffff364d6564696163686173652e427573696e657373466f756e646174696f6e2e46696c74657245787072657373696f6e4e6f646554797065010000000776616c75655f5f000803000000020000000a0407000000bc0153797374656d2e436f6c6c656374696f6e732e47656e657269632e4c69737460315b5b4d6564696163686173652e427573696e657373466f756e646174696f6e2e46696c74657245787072657373696f6e4e6f64652c204d6564696163686173652e427573696e657373466f756e646174696f6e2c2056657273696f6e3d382e31312e342e3635362c2043756c747572653d6e65757472616c2c205075626c69634b6579546f6b656e3d343164326537613631356261323836635d5d03000000065f6974656d73055f73697a65085f76657273696f6e040000344d6564696163686173652e427573696e657373466f756e646174696f6e2e46696c74657245787072657373696f6e4e6f64655b5d030000000808091000000001000000010000000108000000060000000a0905000000090c00000006130000002430303030303030302d303030302d303030302d303030302d3030303030303030303030300a01090c0000000a0a01ebfffffff1ffffff020000000a01090000000700000009160000000100000001000000071000000000010000000400000004324d6564696163686173652e427573696e657373466f756e646174696f6e2e46696c74657245787072657373696f6e4e6f64650300000009170000000d03071600000000010000000400000004324d6564696163686173652e427573696e657373466f756e646174696f6e2e46696c74657245787072657373696f6e4e6f64650300000009180000000d030117000000060000000a0a0a0619000000267b37373731443737372d453332372d343230302d383738442d4145383446443039453642437d090600000000061b0000001a5461726765744c696e654974656d2e436174616c6f674e6f6465091c000000061d0000000573686f657301e2fffffff1ffffff000000000a011800000006000000091f0000000a0a0620000000267b37314346374533322d354631422d346237312d424330392d3733453734423333323837397d09080000000006220000000e6765742025206f6666206974656d09230000000806000000000000394001dcfffffff1ffffff000000000a051c0000002e4d6564696163686173652e427573696e657373466f756e646174696f6e2e436f6e646974696f6e456c656d656e7407000000045f6b6579055f6e616d650c5f6465736372697074696f6e055f74797065125f637573746f6d436f6e74726f6c50617468125f637573746f6d436f6e74726f6c54797065065f6974656d7301010104010104324d6564696163686173652e427573696e657373466f756e646174696f6e2e436f6e646974696f6e456c656d656e7454797065030000003b4d6564696163686173652e427573696e657373466f756e646174696f6e2e436f6e646974696f6e53656c6563744974656d436f6c6c656374696f6e03000000030000000625000000267b41383331414132302d353244372d346162372d384635382d3644333735353241334237317d06260000000d457175616c7320285465787429092600000005d9ffffff324d6564696163686173652e427573696e657373466f756e646174696f6e2e436f6e646974696f6e456c656d656e7454797065010000000776616c75655f5f000803000000ff0000000628000000347e2f417070732f4d61726b6574696e672f45787072657373696f6e46756e6374696f6e732f5465787446696c7465722e617363780a0a051f0000003253797374656d2e436f6c6c656374696f6e732e5370656369616c697a65642e4e616d6556616c7565436f6c6c656374696f6e0700000008526561644f6e6c790c4861736850726f766964657208436f6d706172657205436f756e74044b6579730656616c7565730756657273696f6e00030300060500013253797374656d2e436f6c6c656374696f6e732e43617365496e73656e73697469766548617368436f646550726f76696465722a53797374656d2e436f6c6c656374696f6e732e43617365496e73656e736974697665436f6d706172657208080a000000000929000000092a00000001000000092b000000092c0000000200000001230000001c000000062d000000267b41394244334637362d444642452d346633612d393031332d3038343230423545334439387d062e00000006726577617264092e00000001d1ffffffd9ffffffff00000006300000003e7e2f417070732f4d61726b6574696e672f45787072657373696f6e46756e6374696f6e732f446563696d616c50657263656e7446696c7465722e617363780a0a04290000003253797374656d2e436f6c6c656374696f6e732e43617365496e73656e73697469766548617368436f646550726f766964657201000000066d5f74657874031d53797374656d2e476c6f62616c697a6174696f6e2e54657874496e666f0931000000042a0000002a53797374656d2e436f6c6c656374696f6e732e43617365496e73656e736974697665436f6d7061726572010000000d6d5f636f6d70617265496e666f032053797374656d2e476c6f62616c697a6174696f6e2e436f6d70617265496e666f0932000000112b0000000100000006330000001e66696c74657245787072657373696f6e4368696c64456e61626c654b6579102c00000001000000093400000004310000001d53797374656d2e476c6f62616c697a6174696f6e2e54657874496e666f070000000f6d5f6c697374536570617261746f720c6d5f6973526561644f6e6c790d6d5f63756c747572654e616d6511637573746f6d43756c747572654e616d650b6d5f6e446174614974656d116d5f757365557365724f766572726964650d6d5f77696e33324c616e67494401000101000000010801080635000000012c01090c000000090c00000000000000007f00000004320000002053797374656d2e476c6f62616c697a6174696f6e2e436f6d70617265496e666f04000000066d5f6e616d650977696e33324c4349440763756c747572650d6d5f536f727456657273696f6e0100000308082053797374656d2e476c6f62616c697a6174696f6e2e536f727456657273696f6e090c000000000000007f0000000a04340000001c53797374656d2e436f6c6c656374696f6e732e41727261794c69737403000000065f6974656d73055f73697a65085f76657273696f6e05000008080937000000010000000100000010370000000100000006380000000546616c73650b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 where PromotionId = 1')";
            ExecuteSql(sql);
            sql = "UPDATE [dbo].[Promotion] SET Params = 0x3c3f786d6c2076657273696f6e3d22312e30223f3e0d0a3c53657474696e677320786d6c6e733a7873693d22687474703a2f2f7777772e77332e6f72672f323030312f584d4c536368656d612d696e7374616e63652220786d6c6e733a7873643d22687474703a2f2f7777772e77332e6f72672f323030312f584d4c536368656d61223e0d0a20203c526577617264547970653e57686f6c654f726465723c2f526577617264547970653e0d0a20203c416d6f756e744f66663e35303c2f416d6f756e744f66663e0d0a20203c416d6f756e74547970653e56616c75653c2f416d6f756e74547970653e0d0a20203c4d696e4f72646572416d6f756e743e3530303c2f4d696e4f72646572416d6f756e743e0d0a3c2f53657474696e67733e where PromotionId = 2";
            ExecuteSql(sql);
            sql = "UPDATE [dbo].[Promotion] SET Params = 0x3c3f786d6c2076657273696f6e3d22312e30223f3e0d0a3c53657474696e677320786d6c6e733a7873693d22687474703a2f2f7777772e77332e6f72672f323030312f584d4c536368656d612d696e7374616e63652220786d6c6e733a7873643d22687474703a2f2f7777772e77332e6f72672f323030312f584d4c536368656d61223e0d0a20203c43617465676f7279436f64653e73686f65732d773c2f43617465676f7279436f64653e0d0a20203c4d696e696d756d5175616e746974793e313c2f4d696e696d756d5175616e746974793e0d0a20203c5368697070696e674d6574686f6449643e66633763326435332d376331632d343239382d383138392d6638623166386538353433393c2f5368697070696e674d6574686f6449643e0d0a20203c526577617264547970653e57686f6c654f726465723c2f526577617264547970653e0d0a20203c416d6f756e744f66663e31303c2f416d6f756e744f66663e0d0a20203c416d6f756e74547970653e56616c75653c2f416d6f756e74547970653e0d0a3c2f53657474696e67733e where PromotionId = 3";
            ExecuteSql(sql);
        }

        private void CreateExpression(int promotionId, string name)
        {
            var xml = String.Empty;
            if (name.Equals("25 % off Mens Shoes"))
            {
                using (var sr = new StreamReader(Path.Combine(HostingEnvironment.ApplicationPhysicalPath, @"App_Data\Promotions\25_Percent_off_Mens_Shoes.xml")))
                {
                    xml = sr.ReadToEnd();
                }

            }
            else if (name.Equals("$50 off Order over $500"))
            {
                using (var sr = new StreamReader(Path.Combine(HostingEnvironment.ApplicationPhysicalPath, @"App_Data\Promotions\50_off_Order_over_500.xml")))
                {
                    xml = sr.ReadToEnd();
                }
            }
            else if (name.Equals("$10 off shipping from Women's Shoes"))
            {
                using (var sr = new StreamReader(Path.Combine(HostingEnvironment.ApplicationPhysicalPath, @"App_Data\Promotions\10_off_shipping_from_Womens_Shoes.xml")))
                {
                    xml = sr.ReadToEnd();
                }
                xml = xml.Replace("fc7c2d53-7c1c-4298-8189-f8b1f8e85439", ShippingManager.GetShippingMethods("en").ShippingMethod.FirstOrDefault(x => x.LanguageId.Equals("en") && x.Name.Contains("Express") && x.Currency.Equals("USD")).ShippingMethodId.ToString().ToLower());
            }
            xml = xml.Replace("'", "''");
            var sql = String.Format("INSERT INTO [dbo].[Expression] ([ApplicationId], [Name], [Description], [Category], [ExpressionXml], [Created], [Modified], [ModifiedBy]) VALUES (N'{0}', N'{1}', N'{1}', N'Promotion', '{2}', N'20150430 09:55:05.570', NULL, N'admin')", AppContext.Current.ApplicationId, name.Replace("'", "''"), xml);
            ExecuteSql(sql);
            sql = String.Format("INSERT INTO [dbo].[PromotionCondition] ([PromotionId], [ExpressionId], [CatalogName], [CatalogNodeId], [CatalogEntryId]) VALUES ({0}, {0}, NULL, NULL, NULL)", promotionId);
            ExecuteSql(sql);
        }

        private void ExecuteSql(String sql)
        {
            using (var connection = new SqlConnection(ConfigurationManager.ConnectionStrings["EcfSqlConnection"].ConnectionString))
            {
                var command = new SqlCommand(sql, connection)
                {
                    CommandType = CommandType.Text
                };
                connection.Open();
                command.ExecuteNonQuery();
            }
        }

Sorry if I bored you – and yes, you might recognize this is from QuickSilver – it’ll create 3 default promotions. Note that this is not the entirely thing, you still need to include the three expression files. For example, 25_Percent_off_Mens_Shoes.xml, which is only 170 lines long. I decided to not include it here, for your convenience.

I can’t simply write this code. I will just copy it somewhere and try to make it work. In most of the cases, it’s harmless to copy and paste, but you won’t know the day when it hurts you!

Here comes dragon

And today I tried to convert these to new promotion system. Here’s what I get:

private ContentReference CreateCampaigns()
        {
            var campaign = _contentRepository.Service.GetDefault<SalesCampaign>(SalesCampaignFolder.CampaignRoot);
            campaign.Name = "QuickSilver";
            campaign.Created = DateTime.UtcNow;
            campaign.IsActive = true;
            campaign.ValidFrom = DateTime.Today;
            campaign.ValidUntil = DateTime.Today.AddYears(1);
            return _contentRepository.Service.Save(campaign, SaveAction.Publish, AccessLevel.NoAccess);
        }

        private void CreateBuyFromMenShoesGetDiscountPromotion(ContentReference campaignLink)
        {
            var categoryLink = _referenceConverter.Service.GetContentLink("shoes", CatalogContentType.CatalogNode);
            var promotion = _contentRepository.Service.GetDefault<BuyQuantityGetItemDiscount>(campaignLink);
            promotion.IsActive = true;
            promotion.Name = "25 % off Mens Shoes";
            promotion.Condition.Items = new List<ContentReference>() { categoryLink };
            promotion.Condition.RequiredQuantity = 1;
            promotion.DiscountTarget.Items = new List<ContentReference>() { categoryLink };
            promotion.Discount.UseAmounts = false;
            promotion.Discount.Percentage = 25m;
            _contentRepository.Service.Save(promotion, SaveAction.Publish, AccessLevel.NoAccess);
        }

        private void CreateSpendAmountGetDiscountPromotion(ContentReference campaignLink)
        {
            var promotion = _contentRepository.Service.GetDefault<SpendAmountGetOrderDiscount>(campaignLink);
            promotion.IsActive = true;
            promotion.Name = "$50 off Order over $500";
            promotion.Condition.Amounts = new List<Money>() { new Money(500m, Currency.USD) };
            promotion.Discount.UseAmounts = true;
            promotion.Discount.Amounts = new List<Money>() { new Money(50m, Currency.USD) };
            _contentRepository.Service.Save(promotion, SaveAction.Publish, AccessLevel.NoAccess);
        }

        private void CreateBuyFromWomenShoesGetShippingDiscountPromotion(ContentReference campaignLink)
        {
            var categoryLink = _referenceConverter.Service.GetContentLink("shoes-w", CatalogContentType.CatalogNode);
            var promotion = _contentRepository.Service.GetDefault<BuyQuantityGetShippingDiscount>(campaignLink);
            promotion.IsActive = true;
            promotion.Name = "$10 off shipping from Women's Shoes";
            promotion.Condition.Items = new List<ContentReference>() { categoryLink };
            promotion.ShippingMethods = GetShippingMethodIds();
            promotion.Condition.RequiredQuantity = 1;
            promotion.Discount.UseAmounts = true;
            promotion.Discount.Amounts = new List<Money>() { new Money(10m, Currency.USD) };
            _contentRepository.Service.Save(promotion, SaveAction.Publish, AccessLevel.NoAccess);
        }

        private IList<Guid> GetShippingMethodIds()
        {
            var shippingMethods = new List<Guid>();
            var marketService = ServiceLocator.Current.GetInstance<IMarketService>();
            var enabledMarkets = marketService.GetAllMarkets().Where(x => x.IsEnabled).ToList();
            foreach (var language in enabledMarkets.SelectMany(x => x.Languages).Distinct())
            {
                var languageId = language.TwoLetterISOLanguageName;
                var dto = ShippingManager.GetShippingMethods(languageId);
                foreach (var shippingMethodRow in dto.ShippingMethod)
                {
                    shippingMethods.Add(shippingMethodRow.ShippingMethodId);
                }
            }
            return shippingMethods;
        }

Of course no extra files or such.

I will leave the conclusion to you, but in my opinion, now you can create this kind of promotion with your eyes shut:

Next promotion type
Next promotion! Beers for everyone

You’re excited? I’m too! Credits to my teammates who have been working hard to make this happens. Stay tuned – the new promotion system will be officially released (aka out of BETA) very soon.

Leave a Reply

Your email address will not be published. Required fields are marked *