Custom LineItemValidator

Episerver has quite nice mechanism on validating cart, line item, shipment , , promotion and inventory. I was looking into customizing the line item validtion, to inject some business logic. To do this:

  1. First create new class and inherit from ILineItemValidator
   public class CustomLineItemValidator : ILineItemValidator
    {
        private readonly ILineItemValidator defaultImplementation;
        private readonly IOrderRepository orderRepository;

        public CustomLineItemValidator(ILineItemValidator defaultImplementation,
            IOrderRepository orderRepository)
        {
            this.defaultImplementation = defaultImplementation;
            this.orderRepository = orderRepository;
        }

        [Obsolete]
        public bool Validate(ILineItem lineItem, IMarket market, Action<ILineItem, ValidationIssue> onValidationError)
        {
            if (AlreadyPurchased(lineItem))
            {
                onValidationError(lineItem, ValidationIssue.RemovedDueToUnavailableItem);

                return false;
            }

            return defaultImplementation.Validate(lineItem, market, onValidationError);
        }

        public bool Validate(ILineItem lineItem, MarketId marketId, Action<ILineItem, ValidationIssue> onValidationError)
        {
            if (AlreadyPurchased(lineItem))
            {
                onValidationError(lineItem, ValidationIssue.RemovedDueToUnavailableItem);

                return false;
            }

            return defaultImplementation.Validate(lineItem, marketId, onValidationError);
        }

        private bool AlreadyPurchased(ILineItem lineItem)
        {
            var alreadyPurchased = false;
            var customerId = lineItem?.ParentOrderGroup?.CustomerId;

            if (customerId != null)
            {
                var orders = orderRepository.Load<IPurchaseOrder>(customerId.Value);

                if (orders != null)
                {

                    alreadyPurchased = orders.Any(a => a.GetAllLineItems()
                                                            .Any(l => string.Equals(l.Code,
                                                                                    lineItem.Code,
                                                                                    StringComparison.InvariantCultureIgnoreCase)));

                }
            }

            return alreadyPurchased;
        }
    }

In our example above, we are checking to make sure if the user already purchased the item before and if that is a case we return false as a result of validation. It may be weird but our customer has this requirement and it is meaningful in their context. As you can see we are re-using existing line item validator as well.

2. Next step is to inject our new validator to the DI:

    [InitializableModule]
    public class DependencyResolverInitialization : IConfigurableModule
    {
        private static readonly ILogger logger = LogManager.GetLogger();

        public void ConfigureContainer(ServiceConfigurationContext context)
        {
            var services = context.Services;

            services.Intercept<ILineItemValidator>(
                (locator, defaultImplementation) =>
                    new CustomLineItemValidator(defaultImplementation,
                                locator.GetInstance<IOrderRepository>()));
        }
    }

As you can see on DependencyResolverInitialization class, I added a new section to intercept the ILineItemValidator. If you are not sure what I’m speaking of you can read more about it here.

If you are having ‘Commerce Manager’ make sure you do same thing in both CommerceManager and Shopfront projects!

Create a Campaign, Discount programmatically – Episerver Commerce

We had a scenario that customer wants to generate a coupon dynamically and use the coupon as credit for the user. The code as everything else in Episerver is quite straight forward:

The first step is to create a sales campaign:

        private SalesCampaign EnsureCampaignCreated()
        {
            var campaign = contentRepository
                .GetChildren<SalesCampaign>(SalesCampaignFolder.CampaignRoot)?
                .FirstOrDefault(a => a.Name == "Credit");
            
            if (campaign == null)
            {
                campaign = contentRepository
                    .GetDefault<SalesCampaign>(SalesCampaignFolder.CampaignRoot);
                campaign.Name = "Credit";
                campaign.Created = DateTime.UtcNow;
                campaign.IsActive = true;
                campaign.ValidFrom = DateTime.Today;
                campaign.ValidUntil = DateTime.Today.AddYears(1);
                var campaignContentRefrence = contentRepository.Save(campaign, SaveAction.Publish, AccessLevel.NoAccess);

                campaign = contentRepository.Get<SalesCampaign>(campaignContentRefrence);
            }

            return campaign;
        }

As you can see we just make sure the campaign is being created and our key in this stage is ‘Campaign Name’ which may be not good enough for some scenarios but for us is enough. Second I’m loading ALL campaigns and iterate through them to find a specific campaign. Again in our scenario, it will not that much effect but if the commerce you are dealing with has MANY campaigns you can use LoaderOptione as below:

Next step is to create a discount:

        private string CreateDiscountPromotion(ContentReference campaignLink, string orderNumber, Money couponAmount)
        {
            var catalougLink = siteSettingsService.StartPage.DefaultCatalogue;

            var couponCode = $"CREDIT-{Guid.NewGuid().ToString().Replace("-", "").Substring(6)}";

            var promotion = contentRepository.GetDefault<BuyQuantityGetItemDiscount>(campaignLink);
            promotion.IsActive = true;
            promotion.Name = $"CREDIT-{orderNumber}";
            promotion.Coupon.Code = couponCode;
            promotion.Condition.Items = new List<ContentReference>() { catalougLink };
            promotion.Condition.RequiredQuantity = 1;
            promotion.Condition.MatchRecursive = true;
            promotion.DiscountTarget.Items = new List<ContentReference>() { catalougLink };
            promotion.Discount.UseAmounts = true;
            promotion.Discount.Amounts = new List<Money>() { couponAmount };

            contentRepository.Save(promotion, SaveAction.Publish, AccessLevel.NoAccess);

            return couponCode;
        }

As you can see generating a discount is very straight forward. I used ‘BuyQuantityGetItemDiscount’ which fits for my purpose but you can use any out of the box or custom discount you want. Discount which I used ‘BuyQuantityGetItemDiscount’ require to define what catalog item can this discount can be applied. I’m including all current site catalog (I’m getting it from start page which is custom property) but you can use any other method (e.g. iterate through all catalogs and include all catalogs). There is a flag ‘MatchRecursive’ which is set to true, means all descendants of selected items will include in promotion.

So finally we need to call these method full code as bellow:

        private string CreateCreditVoucher(string orderNumber, Money couponAmount)
        {
            var campaign = EnsureCampaignCreated();

            return CreateDiscountPromotion(campaign.ContentLink, orderNumber, couponAmount);
        }

        private SalesCampaign EnsureCampaignCreated()
        {
            var campaign = contentRepository
                .GetChildren<SalesCampaign>(SalesCampaignFolder.CampaignRoot)?
                .FirstOrDefault(a => a.Name == "Credit");
            
            if (campaign == null)
            {
                campaign = contentRepository
                    .GetDefault<SalesCampaign>(SalesCampaignFolder.CampaignRoot);
                campaign.Name = "Credit";
                campaign.Created = DateTime.UtcNow;
                campaign.IsActive = true;
                campaign.ValidFrom = DateTime.Today;
                campaign.ValidUntil = DateTime.Today.AddYears(1);
                var campaignContentRefrence = contentRepository.Save(campaign, SaveAction.Publish, AccessLevel.NoAccess);

                campaign = contentRepository.Get<SalesCampaign>(campaignContentRefrence);
            }

            return campaign;
        }

        private string CreateDiscountPromotion(ContentReference campaignLink, string orderNumber, Money couponAmount)
        {
            var catalougLink = siteSettingsService.StartPage.DefaultCatalogue;

            var couponCode = $"CREDIT-{Guid.NewGuid().ToString().Replace("-", "").Substring(6)}";

            var promotion = contentRepository.GetDefault<BuyQuantityGetItemDiscount>(campaignLink);
            promotion.IsActive = true;
            promotion.Name = $"CREDIT-{orderNumber}";
            promotion.Coupon.Code = couponCode;
            promotion.Condition.Items = new List<ContentReference>() { catalougLink };
            promotion.Condition.RequiredQuantity = 1;
            promotion.Condition.MatchRecursive = true;
            promotion.DiscountTarget.Items = new List<ContentReference>() { catalougLink };
            promotion.Discount.UseAmounts = true;
            promotion.Discount.Amounts = new List<Money>() { couponAmount };
            
            contentRepository.Save(promotion, SaveAction.Publish, AccessLevel.NoAccess);

            return couponCode;
        }

Discount and promotion has far more features that I haven’t included, you can read more about it HERE