<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Sanjay Katiyar</title><link href="http://world.optimizely.com" /><updated>2025-04-28T10:57:39.0000000Z</updated><id>https://world.optimizely.com/blogs/sanjay-katiyar/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Optimizely Product Recommendation Troubleshooting</title><link href="https://world.optimizely.com/blogs/sanjay-katiyar/dates/2025/4/optimizely-product-recommendation-troubleshooting/" /><id>&lt;p class=&quot;MsoNormal&quot;&gt;In today&amp;rsquo;s fast-paced digital landscape, &lt;strong&gt;personalization is everything&lt;/strong&gt;. Customers expect relevant, tailored experiences whenever they interact with a brand &amp;mdash; and meeting that expectation can make or break your success. That&amp;rsquo;s where &lt;strong&gt;Optimizely&#39;s Product Recommendation&lt;/strong&gt; feature shines.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;What is Optimizely Product Recommendation?&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Optimizely&amp;rsquo;s Product Recommendation feature is a &lt;strong&gt;powerful, AI-driven tool&lt;/strong&gt; designed to help brands offer hyper-personalized product suggestions to their customers. It&amp;rsquo;s built into the Optimizely Commerce platform and integrates seamlessly with both &lt;strong&gt;Commerce Cloud&lt;/strong&gt; and &lt;strong&gt;Customized Commerce (formerly Episerver Commerce)&lt;/strong&gt;.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Instead of relying on static product lists, Optimizely uses &lt;strong&gt;machine learning algorithms&lt;/strong&gt; and &lt;strong&gt;real-time customer behavior&lt;/strong&gt; to dynamically surface the right products to the right audience at the right time.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Why Product Recommendations Matter?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Boost Conversion Rates&lt;/strong&gt;&lt;br /&gt;Relevant product suggestions keep customers engaged and help them discover products they might not have found otherwise, leading to increased sales.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Enhance Customer Experience&lt;/strong&gt;&lt;br /&gt;When users feel like a site &amp;ldquo;understands&amp;rdquo; them, they&amp;rsquo;re more likely to stay, browse, and buy.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Increase Average Order Value&lt;/strong&gt;&lt;br /&gt;Smart cross-selling and upselling through recommendations can encourage customers to add more to their cart.&lt;/li&gt;
&lt;li class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Reduce Bounce Rates&lt;/strong&gt;&lt;br /&gt;Presenting enticing alternatives or related items keeps visitors on your site longer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Troubleshooting&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;During the implementation, we faced a few challenges, which I have outlined here.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compatible Package&lt;/strong&gt;&lt;br /&gt;Before starting, ensure you verify the Commerce version you are using and identify the compatible versions of related packages.&lt;br /&gt;For example, in my case, I was using EPiServer.Commerce 13.33.0, so I installed EPiServer.Personalization.Commerce and EPiServer.Tracking.Commerce version 3.2.37.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Configuration&amp;nbsp;&lt;/strong&gt;&lt;br /&gt;If you are using a single catalog, follow the &lt;strong&gt;single-site&lt;/strong&gt; configuration; otherwise, use the &lt;strong&gt;multi-site&lt;/strong&gt; configuration approach.&amp;nbsp;Since I was working with a multi-site, channel-based setup, I made a mistake during the configuration &amp;mdash; I used &lt;strong&gt;Web&lt;/strong&gt; instead of &lt;strong&gt;web&lt;/strong&gt;, and similarly &lt;strong&gt;Mobile&lt;/strong&gt; instead of &lt;strong&gt;mobile&lt;/strong&gt;. Please note that these values are&amp;nbsp;&lt;strong&gt;case-sensitive&lt;/strong&gt;, so ensure you use the correct lower-case terms to avoid configuration issues.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Single Site&lt;/strong&gt;&lt;strong&gt;&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code&gt;&amp;lt;add key=&quot;episerver:personalization.BaseApiUrl&quot;  value=&quot;https://mysite.uat.productrecs.optimizely.com&quot; /&amp;gt;
&amp;lt;add key=&quot;episerver:personalization.Site&quot; value=&quot;MySite&quot; /&amp;gt;
&amp;lt;add key=&quot;episerver:personalization.ClientToken&quot; value=&quot;MyClientToken&quot; /&amp;gt;
&amp;lt;add key=&quot;episerver:personalization.AdminToken&quot; value=&quot;MyAdminToken&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Multi-Site:&lt;/strong&gt;&lt;br /&gt;The multi-site configuration is &lt;strong&gt;scope-based&lt;/strong&gt;. If you are using more than one catalog, you need to set up the configuration in a repeated form as shown below.&lt;/p&gt;
&lt;pre class=&quot;language-html&quot;&gt;&lt;code&gt;&amp;lt;add key=&quot;episerver:personalization.ScopeAliasMapping.MyScope&quot; value=&quot;SiteId&quot;/&amp;gt;
&amp;lt;add key=&quot;episerver:personalization.CatalogNameForFeed.MyScope&quot; value=&quot;CatalogName&quot;/&amp;gt;
&amp;lt;add key=&quot;episerver:personalization.BaseApiUrl.MyScope&quot;  value=&quot;https://sitename.uat.productrecs.episerver.net&quot; /&amp;gt;
&amp;lt;add key=&quot;episerver:personalization.Site.MyScope&quot; value=&quot;sitename&quot; /&amp;gt;
&amp;lt;add key=&quot;episerver:personalization.AdminToken.MyScope&quot; value=&quot;MyAdminToken&quot; /&amp;gt;
&amp;lt;add key=&quot;episerver:personalization.ClientToken.MyScope.web&quot; value=&quot;MyWebToken&quot; /&amp;gt;
&amp;lt;add key=&quot;episerver:personalization.ClientToken.MyScope.mobile&quot; value=&quot;MyMobileToken&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Sync Specific Pricing (Group Price)&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;The &#39;Product Export Job&#39; syncs only the &#39;All Customers&#39; pricing group. To sync prices for a specific pricing group, use IEntryPriceService to fetch the price for that group.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;e.g.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class DefaultEntryPriceService : IEntryPriceService
{
    private readonly IPromotionEngine _promotionEngine;
    private readonly IMarketService _marketService;
    private readonly IPriceService _priceService;
    private readonly bool _calculateDiscountPrices;
    private readonly IPricingService _pricingService;

    public DefaultEntryPriceService(IPromotionEngine promotionEngine, IMarketService marketService, IPricingService pricingService)
    {
        _promotionEngine = promotionEngine;
        _marketService = marketService;
        _pricingService = pricingService;
        _calculateDiscountPrices = !bool.TryParse(ConfigurationManager.AppSettings[&quot;episerver:personalization.CalculateDiscountPrices&quot;], out _calculateDiscountPrices) || _calculateDiscountPrices;
    }

    public IEnumerable&amp;lt;EntryPrice&amp;gt; GetPrices(IEnumerable&amp;lt;EntryContentBase&amp;gt; entries, DateTime validOn, string scope)
    {
        CustomEntryPriceService entryPriceService = this;

        List&amp;lt;CatalogKey&amp;gt; catalogKeys = entries
            .Where(x =&amp;gt; x is IPricing)
            .Select(new Func&amp;lt;EntryContentBase, CatalogKey&amp;gt;(entryPriceService.GetCatalogKey))
            .ToList();

        Dictionary&amp;lt;string, ContentReference&amp;gt; codeContentLinkMap = entries.ToDictionary(c =&amp;gt; c.Code, c =&amp;gt; c.ContentLink);
        IEnumerable&amp;lt;IMarket&amp;gt; source1 = entryPriceService._marketService.GetAllMarkets().Where(x =&amp;gt; x.IsEnabled);
        List&amp;lt;IPriceValue&amp;gt; source2 = new List&amp;lt;IPriceValue&amp;gt;();
        foreach (IMarket market in source1)
        {
            foreach (CatalogKey key in catalogKeys)
            {
				//Read price for specific group from your service
                var price = _pricingService.GetConsumerListPricing(key.CatalogEntryCode, market);
                source2.Add(new PriceValue
                {
                    CatalogKey = key,
                    MarketId = market.MarketId,
                    UnitPrice = new Money(price ?? decimal.Zero, market.DefaultCurrency)
                });
            }
        }

        foreach (var priceValue in source2.GroupBy(c =&amp;gt; new
        {
            c.CatalogKey,
            c.UnitPrice.Currency
        }).Select(g =&amp;gt; g.OrderBy(x =&amp;gt; x.UnitPrice.Amount).First()).ToList())
        {
            ContentReference contentLink;
            if (codeContentLinkMap.TryGetValue(priceValue.CatalogKey.CatalogEntryCode, out contentLink))
            {
                Money salePrice = priceValue.UnitPrice;
                if (entryPriceService._calculateDiscountPrices)
                {
                    var market = _marketService.GetMarket(priceValue.MarketId);
					
					//Read Discounted Price
                    var discountPrice = _pricingService.GetConsumerDiscountPricing(
                        priceValue.CatalogKey.CatalogEntryCode,
                        market) ?? decimal.Zero;

                    salePrice = discountPrice == 0
                        ? priceValue.UnitPrice
                        : new Money(discountPrice, market.DefaultCurrency);
                }

                yield return new EntryPrice(contentLink, priceValue.UnitPrice, salePrice);
            }
        }
    }

    private CatalogKey GetCatalogKey(EntryContentBase entryContent)
    {
        return new CatalogKey(entryContent.Code);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Target Specific Markets:&lt;br /&gt;&lt;/strong&gt;When targeting products for specific markets, use ICatalogItemFilter to filter the products from your catalog before syncing the feed into product recommendations.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;In our case, for one of the sites, we are targeting more than 16 markets. However, we encountered an issue syncing the feed for the China market, &lt;em&gt;as the Unicode product URLs were not compatible with the product recommendations feed (Optimizely need to fix the Unicode issue)&lt;/em&gt;. As a result, we decided to launch this feature for specific markets only.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;e.g.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt; public class DefaultCatalogItemFilter : ICatalogItemFilter
 {
     private readonly IPublishedStateAssessor _publishedStateAssessor;
     private List&amp;lt;string&amp;gt; languages = new List&amp;lt;string&amp;gt;() { &quot;en&quot;, &quot;en-CA&quot;, &quot;fr-CA&quot; };

     public DefaultCatalogItemFilter(IPublishedStateAssessor publishedStateAssessor)
     {
         _publishedStateAssessor = publishedStateAssessor;
     }

     public bool ShouldFilter(CatalogContentBase content, string scope)
     {
         if (_publishedStateAssessor.IsPublished(content, PublishedStateCondition.None) &amp;amp;&amp;amp;
             languages.Any(x =&amp;gt; string.Equals(x, content.Language.Name, StringComparison.InvariantCultureIgnoreCase)))
         {
             return false;
         }

         return !_publishedStateAssessor.IsPublished(content, PublishedStateCondition.None);
     }
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Sync Specific Attributes in the Product Feed&lt;/strong&gt;&lt;br /&gt;To target specific attributes for syncing into the feed for catalog entries, use IEntryAttributeService. This service helps you apply certain rules to control how products are displayed in the recommendation area.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;In our case, we are using a single codebase for over 14 catalogs. However, the products and variants have some uncommon properties that cannot be included in the feed for other sites.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class DefaultEntryAttributeService : IEntryAttributeService
{
    private readonly CatalogFeedSettings _catalogFeedSettings;
    private readonly IVariantAttributeService _variantAttributeService;

    public DefaultEntryAttributeService(CatalogFeedSettings catalogFeedSettings, IVariantAttributeService variantAttributeService)
    {
        _catalogFeedSettings = catalogFeedSettings;
        _variantAttributeService = variantAttributeService;
    }

    public bool CanBeRecommended(EntryContentBase content, decimal stock, string scope)
    {
        return stock &amp;gt; 0M;
    }

    public IDictionary&amp;lt;string, string&amp;gt; GetAttributes(EntryContentBase content, string scope)
    {
        List&amp;lt;string&amp;gt; userMetaFieldNames = GetUserMetaFields(content).ToList();
        Dictionary&amp;lt;string, string&amp;gt; attributes = new Dictionary&amp;lt;string, string&amp;gt;();

        if (!userMetaFieldNames.Any())
            return attributes;

		// Read attributes from your list that you would like to exclude from feed
        _catalogFeedSettings.ExcludedAttributes = RecommendationHelper.ExcludeAttributes; 

        HashSet&amp;lt;string&amp;gt; excludedAttributes = new HashSet&amp;lt;string&amp;gt;(_catalogFeedSettings.ExcludedAttributes, StringComparer.OrdinalIgnoreCase);

        foreach (PropertyData propertyData in content.Property.Where(x =&amp;gt; IsValidContentProperty(x, userMetaFieldNames, excludedAttributes)))
        {
            attributes.Add(propertyData.Name, propertyData.Value.ToString());
        }

        if (content is MyVariant variant)
        {
            foreach (var attr in RecommendationHelper.IncludeAttributes) // Include specific attributes
            {
                if (attributes.Any(x =&amp;gt; string.Equals(x.Key, attr, StringComparison.OrdinalIgnoreCase)))
                    continue;

				// Read the value for attribute if exists in catalog entries
                var value = _variantAttributeService.GetAttributeValueByName(attr, variant); 
                if (!string.IsNullOrWhiteSpace(value))
                {
                    attributes.Add(attr, value);
                }
            }            
        }

        return attributes;
    }

    public string GetDescription(EntryContentBase entryContent, string scope)
    {
        return entryContent[_catalogFeedSettings.DescriptionPropertyName]?.ToString();
    }

    public string GetTitle(EntryContentBase entryContent, string scope)
    {
        return !string.IsNullOrEmpty(entryContent.DisplayName) ? entryContent.DisplayName : entryContent.Name;
    }

    private bool IsValidContentProperty(
       PropertyData property,
       IEnumerable&amp;lt;string&amp;gt; userMetaFieldNames,
       HashSet&amp;lt;string&amp;gt; excludedAttributes)
    {
        return userMetaFieldNames.Any(x =&amp;gt; x.Equals(property.Name))
            &amp;amp;&amp;amp; !property.Name.Equals(&quot;_ExcludedCatalogEntryMarkets&quot;)
            &amp;amp;&amp;amp; !property.Name.Equals(&quot;DisplayName&quot;)
            &amp;amp;&amp;amp; !property.Name.Equals(&quot;ContentAssetIdInternal&quot;)
            &amp;amp;&amp;amp; !property.Name.StartsWith(&quot;Epi_&quot;)
            &amp;amp;&amp;amp; property.Value != null
            &amp;amp;&amp;amp; !excludedAttributes.Contains(property.Name);
    }

    public IEnumerable&amp;lt;string&amp;gt; GetUserMetaFields(EntryContentBase content)
    {
        var metaClass = Mediachase.MetaDataPlus.Configurator.MetaClass.Load(new MetaDataContext()
        {
            UseCurrentThreadCulture = false,
            Language = content.Language.Name
        }, content.MetaClassId);

        return 
             metaClass != null 
            ? metaClass.GetUserMetaFields().Select(x =&amp;gt; x.Name).ToList() 
            : null ?? Enumerable.Empty&amp;lt;string&amp;gt;();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;strong&gt;Product Page Tracking&lt;/strong&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Use the product code instead of the variant code to track the product.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&lt;img src=&quot;/link/47871e11ed3048428b9f49763a18a8d0.aspx&quot; width=&quot;591&quot; height=&quot;275&quot; /&gt;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;If you&#39;re encountering any challenges with the native implementation, feel free to reach out for assistance.&lt;/p&gt;
&lt;p class=&quot;MsoNormal&quot;&gt;Cheers!&lt;/p&gt;</id><updated>2025-04-28T10:57:39.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Integration Bynder (DAM) with Optimizely</title><link href="https://world.optimizely.com/blogs/sanjay-katiyar/dates/2024/7/integration-bynder-dam-with-optimizely/" /><id>&lt;p&gt;Bynder is a comprehensive digital asset management (DAM) platform that enables businesses to efficiently manage, store, organize, and share their digital content and branding assets, including images, videos, documents, and other media files. For more information, visit &lt;a href=&quot;https://www.bynder.com/en/&quot;&gt;Bynder&lt;/a&gt;. This blog will guide you through integrating Bynder with the Optimilzey platform, ensuring a smooth development process, and leveraging Bynder&#39;s extensive features to meet your project needs&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To integrate Bynder with Optimizely, follow these steps:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;flex flex-grow flex-col max-w-full&quot;&gt;
&lt;div class=&quot;min-h-[20px] text-message flex w-full flex-col items-end gap-2 whitespace-pre-wrap break-words [.text-message+&amp;amp;]:mt-5 overflow-x-auto&quot;&gt;
&lt;div class=&quot;flex w-full flex-col gap-1 empty:hidden first:pt-[3px]&quot;&gt;
&lt;div class=&quot;markdown prose w-full break-words dark:prose-invert light&quot;&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Contact Bynder Support:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Contact your Customer Success Manager or Bynder support at &lt;a&gt;support@bynder.com&lt;/a&gt; to learn more about the Optimizely integration and request the integration package.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Obtain the Installation Package:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You will receive an installation package after discussing your needs with Bynder support.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Review the Documentation:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check the &lt;code&gt;readme.md&lt;/code&gt; file included in the package for detailed installation and configuration instructions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Installation and Configuration:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Since the Bynder package is unavailable on nuget.org, follow the steps outlined in the &lt;code&gt;readme.md&lt;/code&gt; file to manually install and configure the integration.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;For additional information and support, visit the &lt;a href=&quot;https://support.bynder.com/hc/en-us/articles/360015350220-Optimizely-Integration&quot;&gt;Bynder Optimizely Integration Guide&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;mt-1 flex gap-3 empty:hidden ml-3&quot;&gt;
&lt;div class=&quot;items-center justify-start rounded-xl p-1 z-10 -mt-1 bg-token-main-surface-primary md:absolute md:border md:border-token-border-light flex&quot;&gt;
&lt;div class=&quot;flex items-center&quot;&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;&lt;strong&gt;Installation&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Step 1: Add the provided Bynder.x.x.x.nupkg package by support into the project.&lt;/p&gt;
&lt;p&gt;Step 2: Click on the &amp;lsquo;Package Manager Console&amp;rsquo; setting icon and add the package source for Bynder from the physical project directory location.&lt;/p&gt;
&lt;p&gt;Step 3: Add the Bynder package relative folder path reference into &lt;strong&gt;nuget.config&lt;/strong&gt; because when you start the build and deployment nuget package picks the package location from the given soruce.&lt;/p&gt;
&lt;p&gt;Step 4: Run command: Install-Package Bynder&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/defe49e98cf04e54a7e7d84ec8f92367.aspx&quot; width=&quot;686&quot; height=&quot;314&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;&lt;strong&gt;Configuration:&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Step 1: Register the Bynder into Startup.cs&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;img src=&quot;/link/6272b96493c041508173edc17f5594c5.aspx&quot; width=&quot;685&quot; height=&quot;304&quot; /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;appsettings.json&lt;/code&gt;&lt;/strong&gt;: For environment-specific settings, this file is commonly used. It&amp;rsquo;s a good place to store configuration values like &lt;code&gt;BaseUrl&lt;/code&gt;, &lt;code&gt;ClientId&lt;/code&gt;, and &lt;code&gt;ClientSecret&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflow Files&lt;/strong&gt;: If you prefer, you can also manage these settings in workflow files, depending on your deployment or CI/CD setup.&lt;/li&gt;
&lt;li&gt;The recommendations in this article we simplified for demonstration purposes. For production, make sure to follow security best practices, such as storing secrets securely and using environment-specific configurations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Step 2:&amp;nbsp;Login into CMS area --&amp;nbsp; Add-ons -- Bynder&lt;/p&gt;
&lt;p&gt;Step 3:&amp;nbsp;To integrate Bynder with Optimizely and manage assets, follow these steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Get Authorization Token&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click on &lt;strong&gt;&amp;lsquo;Get Token&amp;rsquo;&lt;/strong&gt; to initiate the authorization process. This token will allow you to add items from Bynder DAM into Optimizely.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Whitelist Redirect URL&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ensure that your &lt;strong&gt;&amp;lsquo;RedirectUrl&amp;rsquo;&lt;/strong&gt; is whitelisted in Bynder. This step is crucial for successful authentication and authorization.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Troubleshooting&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you encounter issues with adding Bynder assets, try deleting the existing token and generating a new one. This often resolves authorization problems.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/link/26ef027898fa46f0b760d0d3c4780117.aspx&quot; width=&quot;685&quot; height=&quot;303&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Step 4: To configure &lt;code&gt;AllowedTypes&lt;/code&gt; and &lt;code&gt;UIHints&lt;/code&gt; for Bynder and Optimizely assets at the property level, you would typically do this in your code where you define asset properties. Here&amp;rsquo;s how you might set this up:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[Display(GroupName = GroupNames.Content,Order = 10)]
[AllowedTypes(typeof(VideoFile), typeof(BynderVideoAsset))]
public virtual ContentReference? Video { get; set; }

[Display(GroupName = GroupNames.Content,    Order = 10)]
[UIHint(UIHint.Image)]
[AllowedTypes(typeof(ImageFile), typeof(BynderImageAsset))]
public virtual ContentReference? Image { get; set; }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;AllowedTypes:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VideoFile&lt;/strong&gt;: Use this if you want to optimize or make changes to existing video assets in Optimizely, without removing any current functionality.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ImageFile&lt;/strong&gt;: Use this for optimizing or updating existing image assets in Optimizely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BynderVideoAsset&lt;/strong&gt;: Use this to refer to or use video assets stored in Bynder, rather than those uploaded directly to Optimizely&#39;s media folder.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BynderImageAsset&lt;/strong&gt;: Use this to refer to or use image assets stored in Bynder, rather than those uploaded directly to Optimizely&#39;s media folder.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;UIHints &lt;/strong&gt;- You can use one of the UIHints from below to display the thumbnail image.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[UIHint(UIHint.Image)] &amp;ndash; This will display a thumbnail image for both types of assets&amp;mdash;Bynder and Optimizely. It&#39;s a general UI hint that applies to any asset type you specify as an image.&lt;/li&gt;
&lt;li&gt;[UIHint(Bynder.UIHints.Bynder)] &amp;ndash; This will specifically display a thumbnail image only for Bynder assets. It won&#39;t affect Optimizely assets.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; Note&lt;/strong&gt;: If you are unable to see the thumbnail image for the Bynder image asset then Login into the Bynder portal and edit the image with permission &lt;strong&gt;&amp;lsquo;Mark as public&amp;rsquo;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;img src=&quot;/link/cdf4ea66993944b49edf57759fcad524.aspx&quot; /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Step 5: Upload Your Assets from Bynder or Optimizely media asset and test.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/2e296e3ba0404fc6b441d5a90d5269dc.aspx&quot; width=&quot;413&quot; height=&quot;299&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Step 6: Bynder Jobs to sync and update the contents.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/9c3301aebb194589a75245f001d1c1ff.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Here is some additional help to understand the model type and rendering for other Bynder media assets:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Model:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class BynderBlock : BlockData
{
      [AllowedTypes(typeof(BynderImageAsset))]
      [Display(GroupName = GroupNames.Content, Order = 10)]
      [UIHint(Bynder.UIHints.Bynder)]
      public virtual ContentReference? BynderImage { get; set; }

      [AllowedTypes(typeof(BynderAudioAsset))]
      [Display(GroupName = GroupNames.Content, Order = 20)]
      public virtual ContentReference? BynderAudio { get; set; }

      [AllowedTypes(typeof(BynderVideoAsset))]
      [Display(GroupName = GroupNames.Content, Order = 30)]
      public virtual ContentReference? BynderVideo { get; set; }

      [AllowedTypes(typeof(BynderDocumentAsset))]
      [Display(GroupName = GroupNames.Content, Order = 40)]
      public virtual ContentReference? BynderDocument { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Rendering:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;@Html.PropertyFor(x =&amp;gt; x.CurrentBlock.BynderImage)

OR
 
var url= Model.CurrentBlock.BynderImage.GetUrl()
&amp;lt;img src=&quot;@url&quot; alt =&quot;Bynder asset&quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To create an extension method that reads the image URL from Bynder media asset types, you can follow these steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a static class to hold the extension method.&lt;/li&gt;
&lt;li&gt;Define the extension method to extract the URL from the media asset type.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here&#39;s an example of how you might implement such an extension method:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public static string GetUrl(this ContentReference contentReference)
{
    if (!string.equals(contentReference.ProviderName, &quot;bynder-assets-provider&quot;))
    {
      return _urlResolver.GetUrl(contentReference);
    }

    IBynderAsset assetData = _contentLoader.Get&amp;lt;IBynderAsset&amp;gt;(contentReference);
    if (assetData != null &amp;amp;&amp;amp; assetData.AssetType.Equals(&quot;image&quot;))
    {
        return assetData.ImageUrl;
    }
    else if (assetData != null &amp;amp;&amp;amp; assetData.AssetType.Equals(&quot;video&quot;))
    {
        var videoAsset = assetData as BynderVideoAsset;
        return videoAsset?.VideoPreviewURLs.FirstOrDefault() ?? string.Empty;
    }
    else if (assetData != null &amp;amp;&amp;amp; assetData.AssetType.Equals(&quot;document&quot;))
    {
        var documentAsset = assetData as BynderAssetData;
        return documentAsset?.TransformBaseUrl ?? documentAsset?.Original ?? string.Empty;
    }
    else if (assetData != null &amp;amp;&amp;amp; assetData.AssetType.Equals(&quot;audio&quot;))
    {
        var audioAsset = assetData as BynderAssetData;
        return audioAsset?.TransformBaseUrl ?? audioAsset?.Original ?? string.Empty;
    }

    return string.Empty;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Hope this guide helps you set up the basic configuration for integrating Bynder with Optimizely.&lt;/p&gt;
&lt;p&gt;Please leave your feedback in the comment box.&lt;/p&gt;
&lt;p&gt;Thanks for your visit!&lt;/p&gt;</id><updated>2024-07-22T06:33:02.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Remove a segment from the URL in CMS 12</title><link href="https://world.optimizely.com/blogs/sanjay-katiyar/dates/2024/6/remove-a-segment-from-the-url-in-cms-12/" /><id>&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: I have created thousands of pages dynamically using schedule jobs with different templates (e.g. one column, two columns, etc..) and stored them under one of the specific container page templates but some of the page&amp;rsquo;s URLs were renamed.&lt;/p&gt;
&lt;p&gt;So, I have two problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;How to ignore the container page name segment from the URL,&lt;/li&gt;
&lt;li&gt;Redirect to the new page without using any Add-ons or mapping the old one to the new URL.&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;&lt;strong&gt;Page Hierarchy&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;&lt;strong&gt;Name in URL&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;&lt;strong&gt;New URL (simple address url)&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;&lt;strong&gt;Expected result&lt;/strong&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;p&gt;Start Page -&amp;gt; Container Page -&amp;gt; One col page&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;study&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;students/study&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;- Remove the &amp;lsquo;container-page&amp;rsquo; segment from URL.&lt;/p&gt;
&lt;p&gt;- Apply 301 redirection for New URLs without adding any Add-Ons&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;https://local.alloy.com/container-page/study&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;&amp;nbsp;https://local.alloy.com/students/study&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;https://local.alloy.com/study&lt;/p&gt;
&lt;p&gt;https://local.alloy.com/students/study&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: To fix the above problem we need to add the simple address for new URLs in each page and implement custom 301 redirections.&lt;/p&gt;
&lt;p&gt;Register middleware into &lt;strong&gt;startup.cs &lt;/strong&gt;file to bypass the container page segment from the URL&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt; public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IConfiguration configuration)
 {
      app.UseMiddleware&amp;lt;SkipContainerPageMiddleware&amp;gt;();
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create the container page middleware to read the simple address and apply 301 redirection:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class SkipContainerPageMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ISettingsService _settingsService;
    private readonly IContentLoader _contentLoader;

    public SkipContainerPageMiddleware(RequestDelegate next, ISettingsService settingsService, IContentLoader contentLoader)
    {
        _next = next;
        _settingsService = settingsService;
        _contentLoader = contentLoader;
    }

    public async Task Invoke(HttpContext context)
    {
       //Create a content reference type property in GlobalPageSettings or HomePage to read the container page location where all pages created
        var folderLocation = _settingsService.GetSiteSettings&amp;lt;GlobalPageSettings&amp;gt;()?.OEmbedFolderLocation ?? ContentReference.EmptyReference; 
        var folderName = &quot;/&quot; + _contentLoader.Get&amp;lt;IContent&amp;gt;(folderLocation).Name;

        if (context.Request.Path.StartsWithSegments(folderName, StringComparison.OrdinalIgnoreCase))
        {
            context.Request.Path = context.Request.Path.HasValue
                ? context?.Request?.Path.Value.Substring(folderName.Length)
                : string.Empty;

            if (context != null)
            {
                var url = GetSimpleAddress(folderName, context);
                if (url != null)
                {
                    var newURl = $&quot;{context.Request.Scheme}://{context.Request.Host}/{url.Trim(&#39;/&#39;)}&quot;;
                    context.Response.Redirect(newURl, true);
                    return;
                }
            }
        }

        if (context != null)
            await _next(context);
    }

    private string? GetSimpleAddress(string parentSegment, HttpContext context)
    {
        var parentPage = _contentLoader
            .GetChildren&amp;lt;ContainerPage&amp;gt;(ContentReference.StartPage)
            .FirstOrDefault(x =&amp;gt; string.Equals(parentSegment.Trim(&#39;/&#39;), x.URLSegment, StringComparison.InvariantCultureIgnoreCase))?
            .ContentLink
            ?? ContentReference.EmptyReference;

        var url = context.Request.Path.HasValue ? context?.Request.Path.Value.Trim(&#39;/&#39;) : string.Empty;
        if (string.IsNullOrWhiteSpace(url) || parentPage == ContentReference.EmptyReference)
            return default;

        PageData? page = null;
        foreach (var item in url.Split(&#39;/&#39;))
        {
            page = RecursivePageBySegment(parentPage, item);
            if (page != null)
                parentPage = page.ContentLink;
        }

        return page?.ExternalURL ?? url;
    }

    private PageData? RecursivePageBySegment(ContentReference parentPage, string urlSegment)
    {
        var children = _contentLoader.GetChildren&amp;lt;PageData&amp;gt;(parentPage);
        if (children.Count() == 0)
        {
            return _contentLoader.Get&amp;lt;PageData&amp;gt;(parentPage);
        }

        var page = children.FirstOrDefault(page =&amp;gt; page.URLSegment.Equals(urlSegment, StringComparison.OrdinalIgnoreCase));
        return page ?? default;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I hope this blog helps you achieve similar type functionality!&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</id><updated>2024-06-21T04:58:46.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Custom promotion - Buy at least X items from catalog entries and get a discount on related catalog entries.</title><link href="https://world.optimizely.com/blogs/sanjay-katiyar/dates/2022/12/buy-2x-products-from-a-category-and-get-x-product-discount-on-b-category-products-/" /><id>&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Buy at least X items from catalog entries and get related catalog entries at a discount that satisfy the below formula.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;or&lt;/p&gt;
&lt;p&gt;Create a custom promotion to &amp;lsquo;Buy Products for Discount from Other Selections&amp;rsquo; and apply the below formula to get a discount on other selections.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;&lt;em&gt;Formula = m x n,&amp;nbsp; then get the discounts on n items.&lt;/em&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;m = Spend at least X items&lt;/p&gt;
&lt;p&gt;n = Multiplier&lt;/p&gt;
&lt;p&gt;e.g.&lt;/p&gt;
&lt;p&gt;m = 2&lt;/p&gt;
&lt;p&gt;n = 1, 2,3&amp;hellip;&amp;hellip;&lt;/p&gt;
&lt;table&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CART&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Main Product SKU Qty.&lt;/td&gt;
&lt;td&gt;Other Selection SKU Qty.&lt;/td&gt;
&lt;td&gt;Eligible Discount Qty.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2 x 1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2 x 2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2 x 3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2 x 4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;....&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;....&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;....&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;m x n&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;....&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;n&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you notice the above table &amp;lsquo;Other Selection SKU Qty.&amp;rsquo; column, customer added more than the eligible discounted qty in the cart. So, we need to make sure the discount is only eligible for &#39;Eligible Discount Qty.&#39; not for all &amp;lsquo;Other Selection SKU Qty.&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a custom entry promotion&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;   [ContentType(
        GUID = &quot;CB99C622-A170-4EF6-B28D-077B18BAFD81&quot;,
        GroupName = &quot;Custom - Promotions&quot;,
        DisplayName = &quot;Custom - Buy products for discount from other selection&quot;,
        Description = &quot;Buy at least X items from catalog entries and get related catalog entries at a discount e.g. (2 + 1),  (4 + 2),  (6 + 3) ... &quot;,
        Order = 30000)]
    [PromotionSettings(FixedRedemptionsPerOrder = 1)]
    [ImageUrl(&quot;~/Static/Img/CustomBuyQuantityGetItemDiscount.png&quot;)]
    public class CustomBuyQuantityGetItemDiscount : EntryPromotion, IMonetaryDiscount
    {
        [Display(Order = 10)]
        [PromotionRegion(&quot;Condition&quot;)]
        public virtual PurchaseQuantity Condition { get; set; }

        [Display(Order = 20)]
        [PromotionRegion(&quot;Reward&quot;)]
        public virtual DiscountItems DiscountTarget { get; set; }

        [Display(Order = 30)]
        [PromotionRegion(&quot;Discount&quot;)]
        public virtual MonetaryReward Discount { get; set; }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2. Create custom discount processor and override following methods:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GetPromotionItems
&lt;ul&gt;
&lt;li&gt;Return promotion conditions and reward items.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;RewardDescription
&lt;ul&gt;
&lt;li&gt;Read all discounted SKUs with the max eligible qty for discounts.&lt;/li&gt;
&lt;li&gt;Create Redemption Description into GetRedemptions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class CustomBuyQuantityGetItemDiscountProcessor : EntryPromotionProcessorBase&amp;lt;CustomBuyQuantityGetItemDiscount&amp;gt;
{
        private readonly IContentLoader _contentLoader;

        public CustomBuyQuantityGetItemDiscountProcessor(
            RedemptionDescriptionFactory redemptionDescriptionFactory,
            IContentLoader contentLoader)
            : base(redemptionDescriptionFactory)
        {
            _contentLoader = contentLoader ?? throw new ArgumentNullException(nameof(contentLoader));
        }

        protected override PromotionItems GetPromotionItems(CustomBuyQuantityGetItemDiscount promotionData)
        {
            return new PromotionItems(
                promotionData,
                new CatalogItemSelection(promotionData.Condition.Items, CatalogItemSelectionType.Specific, true),
                new CatalogItemSelection(promotionData.DiscountTarget.Items, CatalogItemSelectionType.Specific, true));
        }

        protected override RewardDescription Evaluate(
            CustomBuyQuantityGetItemDiscount promotionData,
            PromotionProcessorContext context)
        {
            var allLineItems = this.GetLineItems(context.OrderForm)?
                .Where(x =&amp;gt; !x.IsGift)?
                .ToList();

            if (promotionData?.DiscountTarget?.Items == null ||
                promotionData?.Condition?.Items == null ||
                promotionData?.Condition?.RequiredQuantity == 0 ||
                allLineItems.Count() == 0)
            {
                return
                    RewardDescription.CreateNotFulfilledDescription(
                    promotionData,
                    FulfillmentStatus.NotFulfilled);
            }

            //Read all excluded variants 
            var excludedVariants = new List&amp;lt;string&amp;gt;() { };
            if (promotionData.ExcludedItems != null &amp;amp;&amp;amp; promotionData.ExcludedItems.Count &amp;gt; 0)
            {
                foreach (ContentReference item in promotionData.ExcludedItems)
                {
                    if (_contentLoader.TryGet(item, out NodeContent content))
                    {
                        excludedVariants.AddRange(
                            _contentLoader.GetDescendantsOfType&amp;lt;VariationContent&amp;gt;(content.ContentLink)
                            .Select(x =&amp;gt; x.Code)
                            .ToList());
                    }
                    else if (_contentLoader.TryGet(item, out ProductContent productContent))
                    {
                        excludedVariants.AddRange(
                            _contentLoader.GetDescendantsOfType&amp;lt;VariationContent&amp;gt;(productContent.ContentLink)
                            .Select(x =&amp;gt; x.Code)
                            .ToList());
                    }
                    else
                    {
                        excludedVariants.Add(_contentLoader.TryGet&amp;lt;VariationContent&amp;gt;(item)?.Code);
                    }
                }
            }
          
           //Read all condition variants
            var conditionSkus = new List&amp;lt;string&amp;gt;() { };
            foreach (ContentReference item in promotionData.Condition.Items)
            {
                if (_contentLoader.TryGet(item, out NodeContent content))
                {
                    conditionSkus.AddRange(
                        _contentLoader.GetDescendantsOfType&amp;lt;VariationContent&amp;gt;(content.ContentLink)
                        .Select(x =&amp;gt; x.Code)
                        .ToList());
                }
                else if (_contentLoader.TryGet(item, out ProductContent productContent))
                {
                    conditionSkus.AddRange(
                        _contentLoader.GetDescendantsOfType&amp;lt;VariationContent&amp;gt;(productContent.ContentLink)
                        .Select(x =&amp;gt; x.Code)
                        .ToList());
                }
                else
                {
                    conditionSkus.Add(_contentLoader.TryGet&amp;lt;VariationContent&amp;gt;(item)?.Code);
                }
            }
            //Read all discounted variants
            var discountSkus = new List&amp;lt;string&amp;gt;() { };
            foreach (ContentReference item in promotionData.DiscountTarget.Items)
            {
                if (_contentLoader.TryGet(item, out NodeContent content))
                {
                    discountSkus.AddRange(
                        _contentLoader.GetDescendantsOfType&amp;lt;VariationContent&amp;gt;(content.ContentLink)
                        .Select(x =&amp;gt; x.Code)
                        .ToList());
                }
                else if (_contentLoader.TryGet(item, out ProductContent productContent))
                {
                    discountSkus.AddRange(
                        _contentLoader.GetDescendantsOfType&amp;lt;VariationContent&amp;gt;(productContent.ContentLink)
                        .Select(x =&amp;gt; x.Code)
                        .ToList());
                }
                else
                {
                    discountSkus.Add(_contentLoader.TryGet&amp;lt;VariationContent&amp;gt;(item)?.Code);
                }
            }

            //remove the discounted SKUs if exist in the excluded list.
            if (excludedVariants.Count() &amp;gt; 0)
            {
                discountSkus = discountSkus.Where(x =&amp;gt; !excludedVariants.Contains(x)).ToList();
                conditionSkus = conditionSkus.Where(x =&amp;gt; !excludedVariants.Contains(x)).ToList();
            }

            var totalPurchasedQty = allLineItems.Where(x =&amp;gt; conditionSkus.Contains(x.Code)).Sum(x =&amp;gt; x.Quantity);
            var maxDiscountedQty = Math.Floor(totalPurchasedQty / promotionData.Condition.RequiredQuantity);
            if (promotionData.DiscountTarget.MaxQuantity != null &amp;amp;&amp;amp;
                maxDiscountedQty &amp;gt; promotionData.DiscountTarget.MaxQuantity)
            {
                maxDiscountedQty = (decimal)promotionData.DiscountTarget.MaxQuantity;
            }

            if (maxDiscountedQty == decimal.Zero)
            {
                return RewardDescription.CreateNotFulfilledDescription(
                       promotionData,
                       FulfillmentStatus.InvalidCoupon);
            }

            var discountedLineItems = allLineItems
                .Where(x =&amp;gt; discountSkus.Contains(x.Code))
                .OrderByDescending(x =&amp;gt; x.PlacedPrice)
                .ThenBy(x =&amp;gt; x.Quantity)
                .ToList();

            return
                RewardDescription.CreateMoneyOrPercentageRewardDescription(
                         FulfillmentStatus.Fulfilled,
                         GetRedemptions(promotionData, context, discountedLineItems, maxDiscountedQty),
                         promotionData,
                         promotionData.Discount,
                         context.OrderGroup.Currency,
                         promotionData.Name);
        }

        private IEnumerable&amp;lt;RedemptionDescription&amp;gt; GetRedemptions(
          CustomBuyQuantityGetItemDiscount promotionData,
          PromotionProcessorContext context,
          List&amp;lt;ILineItem&amp;gt; discountedLineItems,
          decimal maxQty)
        {
            List&amp;lt;RedemptionDescription&amp;gt; redemptionDescriptionList = new List&amp;lt;RedemptionDescription&amp;gt;();
            var applicableCodes = discountedLineItems.Select(x =&amp;gt; x.Code);
            decimal val2 = GetLineItems(context.OrderForm).Where(li =&amp;gt; applicableCodes.Contains(li.Code)).Sum(li =&amp;gt; li.Quantity);
            decimal num = Math.Min(GetMaxRedemptions(promotionData.RedemptionLimits), val2);

            for (int index = 0; index &amp;lt; num; ++index)
            {
                AffectedEntries entries = context.EntryPrices.ExtractEntries(applicableCodes, Math.Min(maxQty, val2), promotionData);
                if (entries != null)
                {
                    redemptionDescriptionList.Add(this.CreateRedemptionDescription(affectedEntries: entries));
                }
            }

            return redemptionDescriptionList;
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wishing you a year full of blessing and filled with a new adventure.&lt;/p&gt;
&lt;p&gt;Happy new year 2023!&lt;/p&gt;
&lt;p&gt;Cheers&#129346;&lt;/p&gt;</id><updated>2023-01-03T16:05:53.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Delete unused properties and content types in CMS 12</title><link href="https://world.optimizely.com/blogs/sanjay-katiyar/dates/2022/10/delete-unused-properties-and-content-types-in-cms-12/" /><id>&lt;p&gt;The purpose of this blog is to delete unused properties, content references, and content types programmatically and keep clean content.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: I have created a block type (e.g. TeaserBlock) and using this block created multiple contents and used it in different places, but after some time the requirement was changed and I removed this block type completely from the code. So for cleanup, we need to remove this block type from the Admin --&amp;gt; Content Types area in CMS because it no longer exists. But when I tried to delete it, we got content reference warnings because we already created content using a specific block type and added those references at many places (e.g. Main Content Area of other pages).&lt;/p&gt;
&lt;p&gt;Then the question comes to mind how to delete it? So I tried the following solution and fixed it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/80aa19bf006d491caf7e754c575e93f5.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;We have two options two remove the missing content type and its references:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Remove all references from each content type and then delete it from the Admin -&amp;gt; Content Types area. - This is a time-consuming activity because you need to visit and delete each content type (moving and emptying the Trash folder).&lt;/li&gt;
&lt;li&gt;Write the code and clean up it programmatically.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I am using the second (2) option to achieve this.&lt;/p&gt;
&lt;p&gt;Where do you write the code? I will suggest in the &lt;strong&gt;Initialization Module&lt;/strong&gt; or create a&lt;strong&gt; Schedule Job &lt;/strong&gt;to delete the unused properties and content types. It&amp;rsquo;s totally up to you&amp;nbsp; :)&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;span style=&quot;font-size:&amp;#32;12pt;&quot;&gt;Delete the missing properties which are no longer exist in the code for Content Type (PageType/BlockType): (e.g. TeaserBlock -&amp;gt; Sub Title)&lt;/span&gt;&lt;/em&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt; private void DeleteUnUsedProperties()
 {
            var pageTypeRepository = ServiceLocator.Current.GetInstance&amp;lt;IContentTypeRepository&amp;lt;PageType&amp;gt;&amp;gt;();
            var propertyDefinitionRepository = ServiceLocator.Current.GetInstance&amp;lt;IPropertyDefinitionRepository&amp;gt;();
            foreach (var type in pageTypeRepository.List())
            {
                foreach (var property in type.PropertyDefinitions)
                {
                    if (property != null &amp;amp;&amp;amp; !property.ExistsOnModel)
                    {
                        propertyDefinitionRepository.Delete(property);
                    }
                }

               this.DeleteContentType(type);
            }

            var blockTypeRepository = ServiceLocator.Current.GetInstance&amp;lt;IContentTypeRepository&amp;lt;BlockType&amp;gt;&amp;gt;();
            foreach (var type in blockTypeRepository.List())
            {
                foreach (var property in type.PropertyDefinitions)
                {
                    if (property != null &amp;amp;&amp;amp; !property.ExistsOnModel)
                    {                     
                        propertyDefinitionRepository.Delete(property);
                    }
                }

                this.DeleteContentType(type);
            }
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;&lt;span style=&quot;font-size:&amp;#32;12pt;&quot;&gt;Delete the content references and missing content type (e.g. TeaserBlock)&lt;/span&gt;&lt;/em&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;  private void DeleteContentType(ContentType contentType)
  {
            if (contentType.ModelType != null)
            {
                return;
            }

            if (contentType.Saved != null &amp;amp;&amp;amp;
                contentType.Created.HasValue &amp;amp;&amp;amp;
                contentType.Created.Value.Year &amp;gt; 2021 &amp;amp;&amp;amp;
                contentType.IsAvailable)
            {
                // Find and deletes the content based on type.
                var contentModelUsage = ServiceLocator.Current.GetInstance&amp;lt;IContentModelUsage&amp;gt;();
                var contentUsages = contentModelUsage.ListContentOfContentType(contentType);
                var contentReferences = contentUsages
                    .Select(x =&amp;gt; x.ContentLink.ToReferenceWithoutVersion())
                    .Distinct();

                var contentRepository = ServiceLocator.Current.GetInstance&amp;lt;IContentRepository&amp;gt;();
                foreach (var contentItem in contentReferences)
                {
                    contentRepository.Delete(contentItem, true, EPiServer.Security.AccessLevel.NoAccess);
                }

                // Delete type of content.
                var contentTypeRepository = ServiceLocator.Current.GetInstance&amp;lt;IContentTypeRepository&amp;gt;();
                if (contentType.ID &amp;gt; 4)
                {
                    contentTypeRepository.Delete(contentType.ID);
                }
            }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: CotentType.ID &amp;gt; 4 means this will exclude the system/predefined page types e.g. Root Page.&lt;/p&gt;
&lt;p&gt;Please leave your feedback in the comment box.&lt;/p&gt;
&lt;p&gt;Thanks for your visit!&lt;/p&gt;</id><updated>2022-10-08T03:22:33.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Create a new Optimizely Commerce 14 Project</title><link href="https://world.optimizely.com/blogs/sanjay-katiyar/dates/2021/12/create-a-new-optimizely-commerce-14-project/" /><id>&lt;p&gt;The purpose of blog to create a new Optimizely commerce project in version &lt;strong&gt;14.x&lt;/strong&gt; using dotnet-episerver CLI and helps to remember the steps for developer to create a new commerce project. I have also mentioned few errors solution which I experienced during the project setup.&lt;/p&gt;
&lt;p&gt;Before to jump on steps make sure the &lt;a href=&quot;/link/b351930dd81d451ba795ac042b56a63c.aspx&quot;&gt;development environment&lt;/a&gt; &amp;amp; &lt;a href=&quot;/link/15a45be2c1664fde9b1c588592fa99f6.aspx&quot;&gt;system requirement&lt;/a&gt; is ready.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Install EPiServer templates:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;dotnet new -i EPiServer.Net.Templates --nuget-source https://nuget.optimizely.com/feed/packages.svc/ --force&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;--force:&lt;/strong&gt;&amp;nbsp;forces the modification and overwriting of required files for installation if required files exist.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;nbsp;&lt;strong&gt;Install EPiServer CLI:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;dotnet tool install EPiServer.Net.Cli --global --add-source https://nuget.optimizely.com/feed/packages.svc/&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Open the Visual Studio and click on Create a new project link and search &amp;ldquo;episerver&amp;rdquo;, you will see EPiServer installed templates for both CMS and Commerce. We are going to create a commerce project so choose &amp;ldquo;Episerver Commerce Empty Project&amp;rdquo; and click on next.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/3f2fc87e526f4cb6ad90fcfe07fabe03.aspx&quot; width=&quot;752&quot; height=&quot;271&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Enter the project name e.g. QuickDemo and create.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;img src=&quot;/link/3c807a16ed7a4c18bd1574ec82163997.aspx&quot; width=&quot;337&quot; height=&quot;338&quot; /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3:&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create database with admin user.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Open the package manager console in visual studio and select default project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OR&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you are using other command prompt, then reach out the project location using &lt;strong&gt;cd&lt;/strong&gt; command e.g., &amp;lsquo;C:\Optimizely\QuickDemo&amp;rsquo; and type EPiServer cli commands as mentioned below.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CMS Database:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;dotnet-episerver create-cms-database QuickDemo.csproj -S [ServerName] -U [ServerLoginUserName] -P [ServerLoginPassword] -dn QuickDemoCms -du [DataBaseUserName] -dp [DataBasePassword]&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&amp;nbsp;&lt;strong&gt;Commerce Database:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;dotnet-episerver create-commerce-database QuickDemo.csproj -S [ServerName] -U [ServerLoginUserName] -P [ServerLoginPassword] -dn QuickDemoCommerce -du [DataBaseUserName] -dp [DataBasePassword]&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Add admin user&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;dotnet-episerver add-admin-user QuickDemo.csproj -u admin -p Episerver123! -e admin@example.com -c EcfSqlConnection&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&amp;nbsp; -c = connection string name is the case sensitive so make sure the name is same as in your appsetting.json&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;-S:&lt;/strong&gt; Database server name&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;-U:&lt;/strong&gt; Database server login username&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;-P:&lt;/strong&gt; Database server login password&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;-dn:&lt;/strong&gt; Database name e.g. epiCms&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;-du:&lt;/strong&gt; Database username e.g. sa&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;-dp: &lt;/strong&gt;Database password&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;-u:&lt;/strong&gt; Admin user username/loginname e.g. admin or email&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;-p:&lt;/strong&gt; Admin user password&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;-e:&lt;/strong&gt; Admin user email&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;-c:&lt;/strong&gt; connection string name e.g. EcfSqlConnection&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Setup the launch browser for empty site in debug mode and press ctr+f5 or f5 (in windows) to run the site.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For login use &lt;strong&gt;/util/login&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;For CMS view type:&lt;strong&gt; /episerver/cms&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;img src=&quot;/link/1ce516ddbb4d48d49b743e0f33ba86ce.aspx&quot; width=&quot;625&quot; height=&quot;344&quot; /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5: &lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Enable Commerce Tab: &lt;/strong&gt;Once you login into the CMS area then make sure the &lt;img src=&quot;/link/070fdf6e1ce64b989a9beba7a0862c1b.aspx&quot; /&gt; &amp;nbsp;icon is visible for you if not exist then follow up the below steps and enable it&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Add new Administrators group:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Admin --&amp;gt; Access Rights --&amp;gt; Administer Groups --&amp;gt; And create a new Administrators group&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/8e15dde184b94fc9a746f303442dc2b5.aspx&quot; width=&quot;583&quot; height=&quot;214&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Assign the Administrators group to the created user:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Admin --&amp;gt; Access Rights --&amp;gt; Admin Users --&amp;gt; Click on the username&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/ce504e1b5301405b918be0c89928f7d5.aspx&quot; width=&quot;495&quot; height=&quot;446&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Now logout and login again then you will see the missing dotted icon on top navigation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/link/2d8fde655c5746a088d826dc73c867e3.aspx&quot; width=&quot;577&quot; height=&quot;112&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Okay, now time to visit into the commerce area so click on the dotted icon and select the commerce tab and you will see the new features of commerce 14.x.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;---&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[Optional]&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;During the commerce empty project setup I faced below errors which is highlighted with the solution.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Error 1:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Are you getting &amp;ldquo;Something Went Wrong&amp;rdquo; error when entering in commerce area?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/link/53d7bb6e95b548acb89fa3e9a4af4ebf.aspx&quot; width=&quot;519&quot; height=&quot;259&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Solution:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set the access rights for catalog root node&lt;strong&gt;.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class EnableCatalogRoot
{
        private readonly IContentLoader _contentLoader;
        private readonly ReferenceConverter _referenceConverter;
        private readonly IContentSecurityRepository _contentSecurityRepository;

        public EnableCatalogRoot(
            IContentLoader contentLoader,
            ReferenceConverter referenceConverter,
            IContentSecurityRepository contentSecurityRepository)
        {
            _contentLoader = contentLoader;
            _referenceConverter = referenceConverter;
            _contentSecurityRepository = contentSecurityRepository;
        }

        public void SetCatalogAccessRights()
        {
            if (_contentLoader.TryGet(_referenceConverter.GetRootLink(), out IContent content))
            {
                var contentSecurable = (IContentSecurable)content;
                var writableClone = (IContentSecurityDescriptor)contentSecurable.GetContentSecurityDescriptor().CreateWritableClone();
                writableClone.AddEntry(new AccessControlEntry(Roles.Administrators, AccessLevel.FullAccess, SecurityEntityType.Role));
                writableClone.AddEntry(new AccessControlEntry(Roles.WebAdmins, AccessLevel.FullAccess, SecurityEntityType.Role));
                writableClone.AddEntry(new AccessControlEntry(EveryoneRole.RoleName, AccessLevel.Read, SecurityEntityType.Role));

                _contentSecurityRepository.Save(content.ContentLink, writableClone, SecuritySaveType.Replace);
            }
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Register the class into Startup.cs&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;e.g. services.AddSingleton&amp;lt;EnableCatalogRoot&amp;gt;();&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Call the SetCatalogAccessRights() method into the SiteInitialization.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/link/18202b811ade427c9097a8333d914e1d.aspx&quot; width=&quot;585&quot; height=&quot;129&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now error is no more after the running the site.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Error 2:&amp;nbsp; &amp;ldquo;An error occurred while starting the application&amp;rdquo;&amp;nbsp;&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;img src=&quot;/link/0aefa5824e0342b38b8967a812d42c53.aspx&quot; /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Upgrade or downgrade &lt;strong&gt;Configuration.ConfigurationManager&lt;/strong&gt; according to your version 5.0.0.0 or 6.0.0.0 in my case I used 5.0.0.0 version.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Merry Christmas!!!&lt;/strong&gt;&lt;/p&gt;</id><updated>2021-12-16T10:48:08.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Optimizely Developers Meetup India - 26th July</title><link href="https://world.optimizely.com/blogs/sanjay-katiyar/dates/2021/7/optimizely-developers-meetup-india/" /><id>&lt;p&gt;&lt;span style=&quot;font-size:&amp;#32;14pt;&quot;&gt;&lt;strong&gt;Monday, July 26, 2021, 11:30 AM to 12:30 PM (IST)&amp;nbsp;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;RSVP: &lt;a href=&quot;https://www.meetup.com/Optimizely-formerlyEpiserver-Meetup-India/events/279586090/&quot;&gt;https://www.meetup.com/Optimizely-formerlyEpiserver-Meetup-India/events/279586090/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agenda:&lt;/strong&gt;&lt;span&gt;&lt;br /&gt;Welcome and introductions.&lt;br /&gt;Optimizely commerce walkthrough&lt;br /&gt;Commerce 14.x highlights&lt;/span&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Your presence will be highly appreciated.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</id><updated>2021-07-21T14:18:11.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Update GeoIP2 database automatically</title><link href="https://world.optimizely.com/blogs/sanjay-katiyar/dates/2021/5/update-geoip2-database-automatically/" /><id>&lt;p&gt;The purpose of the blog to update the latest GeoIP2 database automatically. Because the GeoLite2 Country, City, and ASN databases are updated weekly, every Tuesday of the week. So we required to update the database up to date, which we can accomplish through the schedule job.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;About GeoIP2:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MaxMind GeoIP2 offerings provide IP geolocation and proxy detection for a wide range of applications including content customization, advertising, digital rights management, compliance, fraud detection, and security.&lt;/p&gt;
&lt;p&gt;The GeoIP2 Database MaxMind provides both binary and CSV databases for GeoIP2. Both formats provide additional data not available in our legacy databases including localized names for cities, subdivisions, and countries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Requirement:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In one of my current projects, we required to read the zip code/postal code on basis of the client IP Address and populate into the address area. Similarly, you can retrieve the Country, City Name, Latitude &amp;amp; Longitude, Metro Code etc.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution: &lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create the schedule job and download GeoLiteCity.dat.gz.&lt;/li&gt;
&lt;li&gt;Uncompressed the file and copy the &lt;strong&gt;GeoLite2-City.mmdb&lt;/strong&gt; database file on physical location which you have define in basePath.&lt;/li&gt;
&lt;li&gt;Read the client IP Address.&lt;/li&gt;
&lt;li&gt;Read the downloaded &lt;strong&gt;GeoLite2-City.mmdb&lt;/strong&gt; database through &lt;em&gt;DatabaseReader&lt;/em&gt; and search the IP address into city database and retrieve zip code/postal code.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Okay, so let&#39;s do some coding for achieving the above approaches:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step1: &lt;/strong&gt;Create the schedule job.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class GeoIp2DataBaseUpdateJob : ScheduledJobBase
{
        private bool _stopSignaled;
        private const string GeoLiteCityFileDownloadUrl = &quot;https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&amp;amp;suffix=tar.gz&quot;;

        public GeoIp2DataBaseUpdateJob()
        {
            this.IsStoppable = true;
        }

        /// &amp;lt;summary&amp;gt;
        /// Called when a user clicks on Stop for a manually started job, or when ASP.NET shuts down.
        /// &amp;lt;/summary&amp;gt;
        public override void Stop()
        {
            base.Stop();
            _stopSignaled = true;
        }

        /// &amp;lt;summary&amp;gt;
        /// Called when a scheduled job executes
        /// &amp;lt;/summary&amp;gt;
        /// &amp;lt;returns&amp;gt;A status message to be stored in the database log and visible from admin mode&amp;lt;/returns&amp;gt;
        public override string Execute()
        {
                   // 1. Download file
            var result = DownloadFile()
                // 2. Unzip file
                .Bind(UnzipFile)
                // 3. Replace at physical location
                .Bind(ReplaceCurrentFile)
                .Match(
                    right =&amp;gt; $&quot;&#128077; GeoIP2 database updated successfully.&quot;,
                    left =&amp;gt; $&quot;&#128078; GeoIP2 database update failed.&quot;);

            if (_stopSignaled)
            {
                return &quot;Stop of job was called&quot;;
            }

            return result;
        }

        private static Either&amp;lt;Exception, string&amp;gt; DownloadFile()
        {
            var licenseKey = ConfigurationManager.AppSettings[&quot;Geolocation.LicenseKey&quot;];
            var uri = new Uri(GeoLiteCityFileDownloadUrl + $&quot;&amp;amp;license_key={licenseKey}&quot;);

            return Prelude.Try(
                    () =&amp;gt;
                    {
                        using (var client = new WebClient())
                        {
                            var tempDir = Path.GetDirectoryName(Path.GetTempPath());
                            var localFile = Path.Combine(tempDir, &quot;MaxMind/GeoLite2-City.tar.gz&quot;);
                            var maxMindFolderPath = Path.GetDirectoryName(localFile);
                            if (!Directory.Exists(maxMindFolderPath) &amp;amp;&amp;amp; !string.IsNullOrWhiteSpace(maxMindFolderPath))
                            {
                                Directory.CreateDirectory(maxMindFolderPath);
                            }

                            client.DownloadFile(uri, localFile);
                            return localFile;
                        }
                    })
                .Succ(localFile =&amp;gt; Prelude.Right&amp;lt;Exception, string&amp;gt;(localFile))
                .Fail(ex =&amp;gt; Prelude.Left&amp;lt;Exception, string&amp;gt;(ex));
        }

        private static Either&amp;lt;Exception, string&amp;gt; ReplaceCurrentFile(string unzippedFileName)
        {
            return Prelude.Try(
                    () =&amp;gt;
                    {
                        var maxMindDbFilePath = Path.Combine(EPiServerFrameworkSection.Instance.AppData.BasePath, &quot;MaxMind/GeoLite2-City.mmdb&quot;);
                        File.Copy(unzippedFileName, maxMindDbFilePath, true);

                        //Delete extracted folder
                        var dir = Path.GetDirectoryName(unzippedFileName);
                        if (Directory.Exists(dir))
                        {
                            Directory.Delete(dir, true);
                        }
                        return unzippedFileName;

                    })
                .Succ(fileName =&amp;gt; Prelude.Right&amp;lt;Exception, string&amp;gt;(fileName))
                .Fail(ex =&amp;gt; Prelude.Left&amp;lt;Exception, string&amp;gt;(ex));
        }

        private static Either&amp;lt;Exception, string&amp;gt; UnzipFile(string downloadedFileName)
        {
            return Prelude.Try(
                    () =&amp;gt;
                    {
                        var dir = Path.GetDirectoryName(downloadedFileName);
                        FileInfo tarFileInfo = new FileInfo(downloadedFileName);

                        DirectoryInfo targetDirectory = new DirectoryInfo(dir ?? string.Empty);
                        if (!targetDirectory.Exists)
                        {
                            targetDirectory.Create();
                        }
                        using (Stream sourceStream = new GZipInputStream(tarFileInfo.OpenRead()))
                        {
                            using (TarArchive tarArchive = TarArchive.CreateInputTarArchive(sourceStream, TarBuffer.DefaultBlockFactor))
                            {
                                tarArchive.ExtractContents(targetDirectory.FullName);
                            }
                        }

                        var filePath = Directory.GetFiles(dir ?? string.Empty, &quot;*.mmdb&quot;, SearchOption.AllDirectories)?.LastOrDefault();

                        //Delete .tar.gz file
                        if (File.Exists(downloadedFileName))
                        {
                            File.Delete(downloadedFileName);
                        }
                        return filePath;
                    })
                .Succ(fileName =&amp;gt; Prelude.Right&amp;lt;Exception, string&amp;gt;(fileName))
                .Fail(ex =&amp;gt; Prelude.Left&amp;lt;Exception, string&amp;gt;(ex));
        }

    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: &lt;/strong&gt;Update the web.config settings&lt;strong&gt;&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Set the database file path for geolocation provider if you are using for personalization.&lt;/li&gt;
&lt;li&gt;Add the &lt;em&gt;basePath &lt;/em&gt;for updating the file on a given physical location.&lt;/li&gt;
&lt;li&gt;Set MaxMind license key&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;episerver.framework&amp;gt;
  ...
  &amp;lt;geolocation defaultprovider=&quot;maxmind&quot;&amp;gt;
    &amp;lt;providers&amp;gt;
      &amp;lt;add databasefilename=&quot;C:\Program Files (x86)\MaxMind\GeoLite2-City.mmdb&quot; name=&quot;maxmind&quot; type=&quot;EPiServer.Personalization.Providers.MaxMind.GeolocationProvider, EPiServer.ApplicationModules&quot;&amp;gt;
    &amp;lt;/add&amp;gt;&amp;lt;/providers&amp;gt;
  &amp;lt;/geolocation&amp;gt;
  &amp;lt;appData basePath=&quot;C:\Program Files (x86)\MaxMind&quot; /&amp;gt;
&amp;lt;/episerver.framework&amp;gt;

&amp;lt;appSettings&amp;gt;
 ...
 &amp;lt;add key=&quot;Geolocation.LicenseKey&quot; value=&quot;{YourLicenseKey}&quot; 
&amp;lt;/appSettings&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Run the job and make sure the file is updating, if the code cause the error then update accordingly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; Step4: &lt;/strong&gt;Create the &lt;em&gt;GeoLocationUtility &lt;/em&gt;class and read the database file for client IP Address.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt; public class GeoLocationUtility
 {
        private static string _maxMindDatabaseFileName = &quot;GeoLite2-City.mmdb&quot;;

        public static GeoLocationViewModel GetGeoLocation(IPAddress address, NameValueCollection config)
        {
            string text = config[&quot;databaseFileName&quot;];

            if (!string.IsNullOrEmpty(text))
            {
                _maxMindDatabaseFileName = VirtualPathUtilityEx.RebasePhysicalPath(text);
                config.Remove(&quot;databaseFileName&quot;);
            }

            if (string.IsNullOrWhiteSpace(_maxMindDatabaseFileName)
                || !File.Exists(_maxMindDatabaseFileName)
                || address.AddressFamily != AddressFamily.InterNetwork &amp;amp;&amp;amp; address.AddressFamily != AddressFamily.InterNetworkV6)
            {
                return null;
            }

            var reader = new DatabaseReader(_maxMindDatabaseFileName);
            try
            {
                var dbResult = reader.City(address);
                var result = GeoLocationViewModel.Make(dbResult);
                return result;
            }
            catch
            { 
                //ignore exception
            }
            finally
            {
                reader.Dispose();
            }

            return null;
        }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 5: &lt;/strong&gt;Finally, call the utility method where you want to get the zip code/postal code value such as:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;  var maxMindDbFilePath = Path.Combine(EPiServerFrameworkSection.Instance.AppData.BasePath, &quot;MaxMind/GeoLite2-City.mmdb&quot;);
   var config = new NameValueCollection
   {
    {&quot;databaseFileName&quot;, maxMindDbFilePath}
   };

   var  postalCode= GeoLocationUtility.GetGeoLocation(ipAddress, config)?.PostalCode;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I hope you found this informative and helpful, Please leave your valuable comments in the comment box.&lt;/p&gt;
&lt;p&gt;Thanks for your visit!&lt;/p&gt;</id><updated>2021-05-04T06:49:16.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Extends order status in Episerver Commerce</title><link href="https://world.optimizely.com/blogs/sanjay-katiyar/dates/2021/2/extends-order-status-in-episerver-commerce/" /><id>&lt;p&gt;The purpose of the blog to create a new custom order status into the Episerver commerce and display that order status into commerce manager area for purchase order.&lt;/p&gt;
&lt;p&gt;Typically, EPiServer commerce provides some default order status e.g., OnHold, PartiallyShipped, InProgress, Completed, Cancelled and AwaitingExchange but in one of my project we need to create a new order status with the name of &amp;lsquo;Open&amp;rsquo; and display that order status into commerce area after the order submit or convert from cart to purchase order (_orderRepository.SaveAsPurchaseOrder(cart)).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note: &lt;/strong&gt;The extendable order status is available in Episerver Commerce Version 13&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;img src=&quot;/link/614f99ac5756479699256279b768207d.aspx&quot; width=&quot;1016&quot; height=&quot;352&quot; /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ticket Reference&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;The same issue has been reported in this ticket.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/link/fa763174b6114902847607de46244fd6.aspx&quot;&gt;https://world.episerver.com/forum/developer-forum/Episerver-Commerce/Thread-Container/2020/1/changing-order-status-away-from-a-custom-order-status/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Okay, So let&#39;s solve the problem with help of below steps...&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;: Create an order status custom helper class.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public static class OrderStatusCustom
{
  public static readonly OrderStatus Open = new OrderStatus(1001, &quot;Open&quot;); 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: &lt;/strong&gt;&amp;nbsp;Register the new order status into Episerver commerce initialization process.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;    [InitializableModule]
    [ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
    [ModuleDependency(typeof(EPiServer.Commerce.Initialization.InitializationModule))]
    public class CustomOrderStatusInitialization : IInitializableModule
    {
        public void Initialize(InitializationEngine context)
        {
           var customOrderStatus = OrderStatusCustom.Open;
            OrderStatus.RegisterStatus(customOrderStatus);       
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt;: Set the custom order status for the purchase order and save it on the order submit action.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var orderRepository = ServiceLocator.Current.GetInstance&amp;lt;IOrderRepository&amp;gt;();
var po = _orderRepository.Load&amp;lt;IPurchaseOrder&amp;gt;(orderLink.OrderGroupId);
po.OrderStatus = OrderStatusCustom.Open;
orderRepository.Save(purchaseOrder);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From the step 1 to 3 we have changed the purchased order status from &#39;InProgress&#39; (default) to &#39;Open&#39;. Now times to display the custom order status in the commerce manager for the purchase order detail and list.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; Add the order status string into the listed localization files in the format &lt;strong&gt;OrderStatus_&lt;/strong&gt;[CustomOrderStatusName]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;App_GlobalResources\Orders\OrderStrings.resx&amp;nbsp;&lt;/li&gt;
&lt;li&gt;App_GlobalResources\SharedStrings.resx e.g.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Key: OrderStatus_Open&amp;nbsp;&lt;br /&gt;Value: Open&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5:&amp;nbsp;&lt;/strong&gt; The order status display process in commerce area is like a hacking process where we need to change some existing files and place their own code.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/e6a2f579aa5348efa37a2ecf6835ce28.aspx&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Include the &lt;strong&gt;/Apps/Order/CustomPrimitives/OrderStatusTitle.ascx&lt;/strong&gt; &amp;amp; &lt;strong&gt;/Apps/Order/GridTemplatesOrderStatusTemplate.ascx&lt;/strong&gt; files into the CommerceManager project.&lt;/li&gt;
&lt;li&gt;Create &lt;strong&gt;OrderStatusTitle.cs &lt;/strong&gt;and &lt;strong&gt;GridTemplatesOrderStatusTemplate.cs&lt;/strong&gt; file and replace the code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;OrderStatusTitle.cs&amp;nbsp;&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;public class OrderStatusTitle : UserControl, IFormDocumentControl
{
        protected Label Label8;
        protected Label lblOrderStatus;
        protected Label Label1;
        protected Label lblCouponCode;

        public void LoadControlValues(object Sender)
        {
            IOrderGroup orderGroup = (IOrderGroup)Sender;
            if (orderGroup == null)
                return;

            var status = (string)this.GetGlobalResourceObject(&quot;OrderStrings&quot;, typeof(OrderStatus).Name + &quot;_&quot; + ((OrderGroup)orderGroup).Status);
            if (string.IsNullOrEmpty(status))
            {
                status = (string)this.GetGlobalResourceObject(&quot;OrderStrings&quot;, typeof(OrderStatus).Name + &quot;_&quot; + OrderStatusManager.GetOrderGroupStatus(orderGroup));
            }
            this.lblOrderStatus.Text = status;
        }

        public void SaveControlValues(object Sender)
        {
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;GridTemplatesOrderStatusTemplate.cs&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class OrderStatusTemplate : Mediachase.Commerce.Manager.Apps.Order.GridTemplates.OrderStatusTemplate
{
        protected void Page_Load(object sender, EventArgs e)
        {
            string str1 = this.DataItem is DataRowView ? &quot;&quot; : &quot;[undefined]&quot;;
            string str2 = string.Empty;
            IOrderGroup dataItem = this.DataItem as IOrderGroup;
            if (dataItem != null)
            {
                str1 = (string)this.GetGlobalResourceObject(&quot;OrderStrings&quot;, typeof(OrderStatus).Name + &quot;_&quot; + ((OrderGroup)dataItem).Status);
                if (string.IsNullOrEmpty(str1))
                {
                    str1 = (string)this.GetGlobalResourceObject(&quot;OrderStrings&quot;, typeof(OrderStatus).Name + &quot;_&quot; + OrderStatusManager.GetOrderGroupStatus(dataItem));
                }

                List&amp;lt;string&amp;gt; source = new List&amp;lt;string&amp;gt;();
                if (dataItem is IPurchaseOrder purchaseOrder)
                {
                    if (purchaseOrder.HasAwaitingStockReturns())
                        source.Add((string)this.GetGlobalResourceObject(&quot;OrderStrings&quot;, &quot;OrderStatus_HaveAwaitingStockReturns&quot;));
                    if (purchaseOrder.HasAwaitingReturnCompletable())
                        source.Add((string)this.GetGlobalResourceObject(&quot;OrderStrings&quot;, &quot;OrderStatus_HaveAwaitingReturnCompletable&quot;));
                    if (source.Count&amp;lt;string&amp;gt;() &amp;gt; 0)
                        str2 = &quot;(&quot; + string.Join(&quot;,&quot;, source.ToArray()) + &quot;)&quot;;
                }
            }
            this.label1.Text = str1;
            if (string.IsNullOrEmpty(str2))
                return;
            this.divAdditionalStatus.Visible = true;
            this.label2.Text = str2;
        }
 }&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Now open the OrderStatusTitle.ascx file and change the &#39;XXXXXXX&#39; &lt;strong&gt;Inherits&lt;/strong&gt; from the qualified namespace of commerce project e.g. QuickSilver.Commerce.Apps.Order.CustomPrimitives.OrderStatusTitle&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp; &amp;nbsp;&lt;code&gt;&amp;lt;%@ Control Language=&quot;C#&quot; AutoEventWireup=&quot;true&quot; CodeBehind=&quot;OrderStatusTitle.ascx.cs&quot; Inherits=&quot;XXXXXXX.Apps.Order.CustomPrimitives.OrderStatusTitle&quot; %&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Repeat the same process for GridTemplatesOrderStatusTemplate.acsx&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RESULT :&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Purchase order List:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/1653f96b80c6484192db435877eab02e.aspx&quot; width=&quot;1001&quot; height=&quot;173&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Purchase order Detail:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/c3f2635f2270473882255a3d196c2b8a.aspx&quot; width=&quot;992&quot; height=&quot;454&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Enjoy the coding and share your thoughts &#128522;&lt;/p&gt;
&lt;p&gt;Thanks for your visit!&lt;/p&gt;</id><updated>2021-02-22T07:09:30.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Multilingual cart validation message </title><link href="https://world.optimizely.com/blogs/sanjay-katiyar/dates/2021/1/validates-a-cart-in-multi-language/" /><id>&lt;p&gt;The purpose of this blog to display the cart validation messages market&#39;s language specific and make it user-friendly to the customer for the better understanding.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;About the Market in Episerver Commerce?&lt;/strong&gt;&lt;br /&gt;Market is a central of Episerver Commerce. A single site can have multiple markets, each with its own product catalog, language, currency, and promotions. Classes in this topic are available in the &lt;code&gt;Mediachase.Commerce&lt;/code&gt; or &lt;code&gt;Mediachase.Commerce.Markets&lt;/code&gt; namespaces.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt;&lt;br /&gt;In one of my site, I have implemented multi-market functionality and managed the content accordingly but there is no feature to display the cart validation message in readable format to the customer for market language specific.&lt;/p&gt;
&lt;p&gt;e.g.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;United State (en) : Display the cart validation message in &lt;strong&gt;English &lt;/strong&gt;language.&lt;/li&gt;
&lt;li&gt;France (fr) : Display the cart validation message in &lt;strong&gt;French &lt;/strong&gt;language. &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Episerver provides a class method &lt;code&gt;OrderValidationService.ValidateOrder(cart)&lt;/code&gt; to validate your cart before to save, using &lt;code&gt;IOrderRepository.Save(cart)&lt;/code&gt; method. With the help of this method we make sure the cart has enough quantity, prices are correct and up-to-date, and any promotions are applied correctly.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;ValidateOrder(cart)&lt;/code&gt; method returns &lt;code&gt;IDictionary&amp;lt;ILineItem, IList&amp;lt;ValidationIssue&amp;gt;&amp;gt; &lt;/code&gt;validation issue per ILineItem but&amp;nbsp; &lt;code&gt;ValidationIssue&lt;/code&gt; is an enum type that returns validation message in below formats which are not user-friendly and market language specific.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CannotProcessDueToMissingOrderStatus&lt;/li&gt;
&lt;li&gt;RemovedDueToCodeMissing&lt;/li&gt;
&lt;li&gt;RemovedDueToNotAvailableInMarket&lt;/li&gt;
&lt;li&gt;RemovedDueToUnavailableCatalog&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So let&amp;rsquo;s make cart validation message user-friendly in the simple way.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Create an XML file language specific and place the all possible cart validation message like below and placed into the &lt;strong&gt;lang &lt;/strong&gt;folder under the site root.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; standalone=&quot;yes&quot;?&amp;gt;
&amp;lt;languages&amp;gt;
  &amp;lt;language name=&quot;English&quot; id=&quot;en&quot;&amp;gt;
    &amp;lt;cart&amp;gt;
      &amp;lt;validation&amp;gt;
        &amp;lt;CannotProcessDueToMissingOrderStatus&amp;gt;Cannot process due to missing order status.&amp;lt;/CannotProcessDueToMissingOrderStatus&amp;gt;
        &amp;lt;RemovedDueToCodeMissing&amp;gt;The catalog entry code that maps to the line item has been removed or changed.&amp;lt;/RemovedDueToCodeMissing&amp;gt;
        &amp;lt;RemovedDueToNotAvailableInMarket&amp;gt;Item has been removed from the cart because it is not available in your market.&amp;lt;/RemovedDueToNotAvailableInMarket&amp;gt;
        &amp;lt;RemovedDueToUnavailableCatalog&amp;gt;Item has been removed from the cart because the catalog of this entry is not available.&amp;lt;/RemovedDueToUnavailableCatalog&amp;gt;
        &amp;lt;RemovedDueToUnavailableItem&amp;gt;Item has been removed from the cart because it is not available at this time.&amp;lt;/RemovedDueToUnavailableItem&amp;gt;
        &amp;lt;RemovedDueToInsufficientQuantityInInventory&amp;gt;Item has been removed from the cart because there is not enough available quantity.&amp;lt;/RemovedDueToInsufficientQuantityInInventory&amp;gt;
        &amp;lt;RemovedDueToInactiveWarehouse&amp;gt;Item has been removed from the cart because the selected warehouse is inactive.&amp;lt;/RemovedDueToInactiveWarehouse&amp;gt;
        &amp;lt;RemovedDueToMissingInventoryInformation&amp;gt;Item has been removed due to missing inventory information.&amp;lt;/RemovedDueToMissingInventoryInformation&amp;gt;
        &amp;lt;RemovedDueToInvalidPrice&amp;gt;Item has been removed due to an invalid price.&amp;lt;/RemovedDueToInvalidPrice&amp;gt;
        &amp;lt;RemovedDueToInvalidMaxQuantitySetting&amp;gt;Item has been removed due to an invalid setting for maximum quantity.&amp;lt;/RemovedDueToInvalidMaxQuantitySetting&amp;gt;
        &amp;lt;AdjustedQuantityByMinQuantity&amp;gt;Item quantity has been adjusted due to the minimum quantity threshold.&amp;lt;/AdjustedQuantityByMinQuantity&amp;gt;
        &amp;lt;AdjustedQuantityByMaxQuantity&amp;gt;Item quantity has been adjusted due to the maximum quantity threshold.&amp;lt;/AdjustedQuantityByMaxQuantity&amp;gt;
        &amp;lt;AdjustedQuantityByBackorderQuantity&amp;gt;Item quantity has been adjusted due to backorder quantity threshold.&amp;lt;/AdjustedQuantityByBackorderQuantity&amp;gt;
        &amp;lt;AdjustedQuantityByPreorderQuantity&amp;gt;Item quantity has been adjusted due to the preorder quantity threshold.&amp;lt;/AdjustedQuantityByPreorderQuantity&amp;gt;
        &amp;lt;AdjustedQuantityByAvailableQuantity&amp;gt;Item quantity has been adjusted due to the available quantity threshold.&amp;lt;/AdjustedQuantityByAvailableQuantity&amp;gt;
        &amp;lt;PlacedPricedChanged&amp;gt;This item&#39;s price has changed since it was added to your cart.&amp;lt;/PlacedPricedChanged&amp;gt;
        &amp;lt;RemovedGiftDueToInsufficientQuantityInInventory&amp;gt;Gift item has been removed from the cart because there is not enough available quantity.&amp;lt;/RemovedGiftDueToInsufficientQuantityInInventory&amp;gt;
        &amp;lt;RejectedInventoryRequestDueToInsufficientQuantity&amp;gt;The inventory request for item has been rejected because there is not enough available quantity.&amp;lt;/RejectedInventoryRequestDueToInsufficientQuantity&amp;gt;
      &amp;lt;/validation&amp;gt;
    &amp;lt;/cart&amp;gt;
  &amp;lt;/language&amp;gt;
  &amp;lt;language name=&quot;French&quot; id=&quot;fr&quot;&amp;gt;
    &amp;lt;cart&amp;gt;
      &amp;lt;validation&amp;gt;
        &amp;lt;CannotProcessDueToMissingOrderStatus&amp;gt;Il ne peut pas &amp;ecirc;tre trait&amp;eacute; en raison d&#39;un statut de commande manquant.&amp;lt;/CannotProcessDueToMissingOrderStatus&amp;gt;
        &amp;lt;RemovedDueToCodeMissing&amp;gt;Le code d&#39;entr&amp;eacute;e de catalogue qui correspond &amp;agrave; l&#39;&amp;eacute;l&amp;eacute;ment de campagne a &amp;eacute;t&amp;eacute; supprim&amp;eacute; ou modifi&amp;eacute;.&amp;lt;/RemovedDueToCodeMissing&amp;gt;
        &amp;lt;RemovedDueToNotAvailableInMarket&amp;gt;L&#39;article a &amp;eacute;t&amp;eacute; retir&amp;eacute; du panier car il n&#39;est pas disponible sur votre march&amp;eacute;.&amp;lt;/RemovedDueToNotAvailableInMarket&amp;gt;
        &amp;lt;RemovedDueToUnavailableCatalog&amp;gt;L&#39;article a &amp;eacute;t&amp;eacute; retir&amp;eacute; du panier car le catalogue de cette entr&amp;eacute;e n&#39;est pas disponible.&amp;lt;/RemovedDueToUnavailableCatalog&amp;gt;
        &amp;lt;RemovedDueToUnavailableItem&amp;gt;L&#39;article a &amp;eacute;t&amp;eacute; retir&amp;eacute; du panier car il n&#39;est pas disponible pour le moment.&amp;lt;/RemovedDueToUnavailableItem&amp;gt;
        &amp;lt;RemovedDueToInsufficientQuantityInInventory&amp;gt;L&#39;article a &amp;eacute;t&amp;eacute; retir&amp;eacute; du panier car la quantit&amp;eacute; disponible est insuffisante.&amp;lt;/RemovedDueToInsufficientQuantityInInventory&amp;gt;
        &amp;lt;RemovedDueToInactiveWarehouse&amp;gt;L&#39;article a &amp;eacute;t&amp;eacute; retir&amp;eacute; du panier car l&#39;entrep&amp;ocirc;t s&amp;eacute;lectionn&amp;eacute; est inactif.&amp;lt;/RemovedDueToInactiveWarehouse&amp;gt;
        &amp;lt;RemovedDueToMissingInventoryInformation&amp;gt;L&#39;article a &amp;eacute;t&amp;eacute; supprim&amp;eacute; en raison d&#39;informations d&#39;inventaire manquantes.&amp;lt;/RemovedDueToMissingInventoryInformation&amp;gt;
        &amp;lt;RemovedDueToInvalidPrice&amp;gt;L&#39;article a &amp;eacute;t&amp;eacute; supprim&amp;eacute; en raison d&#39;un prix non valide.&amp;lt;/RemovedDueToInvalidPrice&amp;gt;
        &amp;lt;RemovedDueToInvalidMaxQuantitySetting&amp;gt;L&#39;article a &amp;eacute;t&amp;eacute; supprim&amp;eacute; en raison d&#39;un param&amp;egrave;tre non valide pour la quantit&amp;eacute; maximale.&amp;lt;/RemovedDueToInvalidMaxQuantitySetting&amp;gt;
        &amp;lt;AdjustedQuantityByMinQuantity&amp;gt;La quantit&amp;eacute; d&#39;articles a &amp;eacute;t&amp;eacute; ajust&amp;eacute;e en raison du seuil de quantit&amp;eacute; minimale.&amp;lt;/AdjustedQuantityByMinQuantity&amp;gt;
        &amp;lt;AdjustedQuantityByMaxQuantity&amp;gt;La quantit&amp;eacute; d&#39;articles a &amp;eacute;t&amp;eacute; ajust&amp;eacute;e en raison du seuil de quantit&amp;eacute; maximale.&amp;lt;/AdjustedQuantityByMaxQuantity&amp;gt;
        &amp;lt;AdjustedQuantityByBackorderQuantity&amp;gt;La quantit&amp;eacute; d&#39;articles a &amp;eacute;t&amp;eacute; ajust&amp;eacute;e en raison du seuil de quantit&amp;eacute; de commandes en souffrance.&amp;lt;/AdjustedQuantityByBackorderQuantity&amp;gt;
        &amp;lt;AdjustedQuantityByPreorderQuantity&amp;gt;La quantit&amp;eacute; d&#39;articles a &amp;eacute;t&amp;eacute; ajust&amp;eacute;e en raison du seuil de quantit&amp;eacute; de pr&amp;eacute;commande.&amp;lt;/AdjustedQuantityByPreorderQuantity&amp;gt;
        &amp;lt;AdjustedQuantityByAvailableQuantity&amp;gt;La quantit&amp;eacute; d&#39;articles a &amp;eacute;t&amp;eacute; ajust&amp;eacute;e en raison du seuil de quantit&amp;eacute; disponible.&amp;lt;/AdjustedQuantityByAvailableQuantity&amp;gt;
        &amp;lt;PlacedPricedChanged&amp;gt;Le prix de cet article a chang&amp;eacute; depuis qu&#39;il a &amp;eacute;t&amp;eacute; ajout&amp;eacute; &amp;agrave; vos favoris.&amp;lt;/PlacedPricedChanged&amp;gt;
        &amp;lt;RemovedGiftDueToInsufficientQuantityInInventory&amp;gt;L&#39;article cadeau a &amp;eacute;t&amp;eacute; retir&amp;eacute; du panier car la quantit&amp;eacute; disponible est insuffisante.&amp;lt;/RemovedGiftDueToInsufficientQuantityInInventory&amp;gt;
        &amp;lt;RejectedInventoryRequestDueToInsufficientQuantity&amp;gt;La demande d&#39;inventaire pour l&#39;article a &amp;eacute;t&amp;eacute; rejet&amp;eacute;e car la quantit&amp;eacute; disponible n&#39;est pas suffisante.&amp;lt;/RejectedInventoryRequestDueToInsufficientQuantity&amp;gt;
      &amp;lt;/validation&amp;gt;
    &amp;lt;/cart&amp;gt;
  &amp;lt;/language&amp;gt;
&amp;lt;/languages&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: &lt;/strong&gt;Create a model class that will hold the error message and variant code.&lt;strong&gt;&lt;br /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class CartValidationIssue
{
        public string Message { get; set; }
 
        public string Code{ get; set; }

        public bool IsBlank =&amp;gt; string.IsNullOrWhiteSpace(this.Message);
       
        public static CartValidationIssue Make(string message, string code)
        {
            return new CartValidationIssue
            {
                Message = message,
                Code = code,
            };
        }
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3: &lt;/strong&gt;Create described methods where you are validating your cart and returns the validation messages in the list format after reading from XML file and show on the cart page or mini-cart area.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use &lt;code&gt;ICurrentMarket&lt;/code&gt; interface and get the current market culture&lt;/li&gt;
&lt;li&gt;Make sure you have selected correct default language for the current market in commerce manager for e.g. France choose default language &lt;em&gt;francais&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;img src=&quot;/link/1fa2f9c7ab8a4d1fa01689b17e515a0d.aspx&quot; width=&quot;556&quot; height=&quot;222&quot; /&gt;&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public List&amp;lt;CartValidationIssue&amp;gt; ValidateCart(ICart cart)
{

            var validationResult = _orderValidationService.ValidateOrder(cart);

            var errors =
                validationResult
                    ?.Select(lineItemIssueEntry =&amp;gt; new
                    {
                        LineItemIssues =
                            lineItemIssueEntry.Value
                                .Select(validationIssue =&amp;gt; new
                                {
                                    ValidationIssueMessage = this.GetCartValidationMessage(validationIssue),
                                    LineItemCode = lineItemIssueEntry.Key.Code,
                                })
                                .ToList(),
                    })
                    .SelectMany(lineItemIssueGroup =&amp;gt; lineItemIssueGroup.LineItemIssues)
                    .Select(x =&amp;gt; CartValidationIssue.Make(x.ValidationIssueMessage, x.LineItemCode))
                    .Where(x =&amp;gt; !x.IsBlank)
                    .ToList() ?? new List&amp;lt;CartValidationIssue&amp;gt;();

            return errors;
 }&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt; private string GetCartValidationMessage(ValidationIssue issue)
 {
            var market = _currentMarket.GetCurrentMarket();
            var cultureInfo = market.DefaultLanguage;

            switch (issue)
            {
                default:
                case ValidationIssue.None:
                    return null;

                case ValidationIssue.CannotProcessDueToMissingOrderStatus:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/CannotProcessDueToMissingOrderStatus&quot;, &quot;It cannot process due to missing order status.&quot;, cultureInfo);

                case ValidationIssue.RemovedDueToCodeMissing:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/RemovedDueToCodeMissing&quot;, &quot;The catalog entry code that maps to the line item has been removed or changed.&quot;, cultureInfo);

                case ValidationIssue.RemovedDueToNotAvailableInMarket:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/RemovedDueToNotAvailableInMarket&quot;, &quot;The catalog entry code that maps to the line item has been removed or changed.&quot;, cultureInfo);

                case ValidationIssue.RemovedDueToUnavailableCatalog:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/RemovedDueToUnavailableCatalog&quot;, &quot;The catalog entry code that maps to the line item has been removed or changed.&quot;, cultureInfo);

                case ValidationIssue.RemovedDueToUnavailableItem:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/RemovedDueToUnavailableItem&quot;, &quot;Item has been removed from the cart because it is not available at this time.&quot;, cultureInfo);

                case ValidationIssue.RemovedDueToInsufficientQuantityInInventory:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/RemovedDueToInsufficientQuantityInInventory&quot;, &quot;Item has been removed from the cart because there is not enough available quantity.&quot;, cultureInfo);

                case ValidationIssue.RemovedDueToInactiveWarehouse:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/RemovedDueToInactiveWarehouse&quot;, &quot;Item has been removed from the cart because the selected warehouse is inactive.&quot;, cultureInfo);

                case ValidationIssue.RemovedDueToMissingInventoryInformation:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/RemovedDueToMissingInventoryInformation&quot;, &quot;Item has been removed due to missing inventory information.&quot;, cultureInfo);

                case ValidationIssue.RemovedDueToInvalidPrice:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/RemovedDueToInvalidPrice&quot;, &quot;Item has been removed due to an invalid price.&quot;, cultureInfo);

                case ValidationIssue.RemovedDueToInvalidMaxQuantitySetting:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/RemovedDueToInvalidMaxQuantitySetting&quot;, &quot;Item has been removed due to an invalid setting for maximum quantity.&quot;, cultureInfo);

                case ValidationIssue.AdjustedQuantityByMinQuantity:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/AdjustedQuantityByMinQuantity&quot;, &quot;Item quantity has been adjusted due to the minimum quantity threshold&quot;, cultureInfo);

                case ValidationIssue.AdjustedQuantityByMaxQuantity:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/AdjustedQuantityByMaxQuantity&quot;, &quot;Item quantity has been adjusted due to the maximum quantity threshold.&quot;, cultureInfo);

                case ValidationIssue.AdjustedQuantityByBackorderQuantity:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/AdjustedQuantityByBackorderQuantity&quot;, &quot;Item quantity has been adjusted due to backorder quantity threshold.&quot;, cultureInfo);

                case ValidationIssue.AdjustedQuantityByPreorderQuantity:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/AdjustedQuantityByPreorderQuantity&quot;, &quot;Item quantity has been adjusted due to the preorder quantity threshold.&quot;, cultureInfo);

                case ValidationIssue.AdjustedQuantityByAvailableQuantity:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/AdjustedQuantityByAvailableQuantity&quot;, &quot;Item quantity has been adjusted due to the available quantity threshold.&quot;, cultureInfo);

                case ValidationIssue.PlacedPricedChanged:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/PlacedPricedChanged&quot;, &quot;This item&#39;s price has changed since it was added to your favorites.&quot;, cultureInfo);

                case ValidationIssue.RemovedGiftDueToInsufficientQuantityInInventory:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/RemovedGiftDueToInsufficientQuantityInInventory&quot;, &quot;Gift item has been removed from the cart because there is not enough available quantity.&quot;, cultureInfo);

                case ValidationIssue.RejectedInventoryRequestDueToInsufficientQuantity:
                    return LocalizationService.Current.GetStringByCulture(&quot;/cart/validation/RejectedInventoryRequestDueToInsufficientQuantity&quot;, &quot;The inventory request for item has been rejected because there is not enough available quantity.&quot;, cultureInfo);
            }
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;e.g. &lt;/strong&gt;The validation message display for France(fr) market in the French language.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;img src=&quot;/link/a09257ddf81f44d08058111b8f53f13c.aspx&quot; width=&quot;668&quot; height=&quot;307&quot; /&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Enjoy the coding and share your thoughts &#128522;&lt;/p&gt;
&lt;p&gt;Thanks for your visit!&lt;/p&gt;</id><updated>2021-02-01T05:49:54.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Customize order management in commerce manager</title><link href="https://world.optimizely.com/blogs/sanjay-katiyar/dates/2020/10/customize-order-management-in-commerce-manager/" /><id>&lt;p&gt;The purpose of the blog to customize the order management UI in the commerce manager and handle out-of-box functionality.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;In this blog I am going to cover two scenarios:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Display the Customer Name into the Customer Information section from the shipping address (First Name + Last Name) if the order placed by an anonymous/guest user.&lt;img src=&quot;/link/e369a023eee4410ca7c98eed7dd61b0e.aspx&quot; /&gt;&lt;/li&gt;
&lt;li&gt;Add a complete order button within the order summary section and trigger the button event.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;img src=&quot;/link/86b9c45f55824a5e9c160d678c7451b0.aspx&quot; width=&quot;653&quot; height=&quot;265&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The customization is based on the &lt;strong&gt;QuickSilver&lt;/strong&gt; solution but you can try the recommended file and code changes in your solution.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/5bef62b679e04d6bb48ae82a57c3dcb6.aspx&quot; width=&quot;349&quot; height=&quot;535&quot; /&gt;&lt;/p&gt;
&lt;p&gt;So, Let&amp;rsquo;s get start coding fun &#128522;&lt;/p&gt;
&lt;h4&gt;&lt;/h4&gt;
&lt;h3&gt;- Display the Customer Name into the Customer Information section from the shipping address (First Name + Last Name) if the order placed by an anonymous/guest user.&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Goto the EPiServer.Reference.Commerce.Manager site and expand the all folder as above screen and visit the folder.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;Folder Path:&lt;/em&gt; ..\EPiServer.Reference.Commerce.Manager\Apps\Order\CustomPrimitives&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Include the OrderCustomer.ascx and add a new class with the same name e.g. &amp;lsquo;OrderCustomer.cs&amp;rsquo;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/link/8832d4c684ab4fd1a680dcfafa26df12.aspx&quot; width=&quot;411&quot; height=&quot;416&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open OrderCustomer.ascx and copy the highlighted &lt;strong&gt;Inherits&lt;/strong&gt; tag value (&lt;em&gt;Mediachase.Commerce.Manager.Apps.Order.CustomPrimitives.OrderCustomer&lt;/em&gt;)and create the OrderCustomer.cs class and derived from the inherits value.&lt;img src=&quot;/link/a6b02b07da2045a59d26d09da1426e27.aspx&quot; /&gt;&lt;/li&gt;
&lt;li&gt;Override the OnPreRender(EventArgs e) method and fetch the purchase order shipping address detail using the order helper class
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using System;
using EPiServer.Commerce.Order;
using Mediachase.Commerce.Manager.Apps_Code.Order;

namespace EPiServer.Reference.Commerce.Manager.Apps.Order.CustomPrimitives
{
    public class OrderCustomer : Mediachase.Commerce.Manager.Apps.Order.CustomPrimitives.OrderCustomer
    {
        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);
            if (string.IsNullOrEmpty(this.lblCustomerName.Text))
            {
                var purchaseOrder = this.OrderGroupId &amp;gt; 0
                    ? OrderHelper.GetPurchaseOrderById(this.OrderGroupId)
                    : null;

                var address = purchaseOrder?.GetFirstShipment()?.ShippingAddress;
                if (address != null)
                {
                    this.lblCustomerName.Text = $@&quot;{address.FirstName} {address.LastName}&quot;;
                }
            }
        }
    }
}
​&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;And in the last re-place the &lt;strong&gt;&lt;em&gt;Inherits&lt;/em&gt;&lt;/strong&gt; attribute value from your created class including full qualified namespace + class name.&lt;img src=&quot;/link/a67714cd51e348c58b933b2ed1462214.aspx&quot; /&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Result (1):&lt;/strong&gt; Now refresh the purchase order and see the changes.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/36a49e0d58fd45f8b5d88d74d2ade13b.aspx&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;- Add a complete order button within the order summary section and trigger the button event:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Goto the EPiServer.Reference.Commerce.Manager site and expand all folder and open the folder.\&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Folder path:&lt;/em&gt; &amp;hellip;\EPiServer.Reference.Commerce.Manager\Apps\Order\Config\View\Forms.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Include the PurchaseOrder-ObjectView.xml in your solution, If you notice in this file you will find numbers are buttons are already added and display in the order summary section, so similarly add a new button with the command name &amp;lsquo;btn_CompleteOrderBtn&amp;rsquo; and permissions.&lt;img src=&quot;/link/f93ed9fef2de4074bdd01d4a3f3e288a.aspx&quot; width=&quot;1046&quot; height=&quot;318&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Create the CompletePurchaseOrderHandler class and inherit it from the TransactionCommandHandler and override the DoCommand method.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/64da555f1fad424c800c7eab3c8bd8d1.aspx&quot; width=&quot;440&quot; height=&quot;366&quot; /&gt;&lt;/p&gt;
&lt;p&gt;You can choose a transaction command handler on basis of the requirements e.g. purchase order, payment plan, and return form handler, etc...&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Commerce.Manager.Order.CommandHandlers.PurchaseOrderHandlers&lt;/li&gt;
&lt;li&gt;Commerce.Manager.Order.CommandHandlers.PaymentPlanHandlers&lt;/li&gt;
&lt;li&gt;Commerce.Manager.Order.CommandHandlers.ReturnFormHandlers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And also enable or disable the button on basis of requirement in the given example the IsCommandEnable method enables the button if the order status is InProgress.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;namespace EPiServer.Reference.Commerce.Manager.Features.Orders.OrderProcessing.Handlers
{
    public class CompletePurchaseOrderHandler: TransactionCommandHandler
    {
        protected override void DoCommand(IOrderGroup order, CommandParameters commandParameters)
        {
            order.OrderStatus = OrderStatus.Completed;
            var purchaseOrder = order as IPurchaseOrder;

            var shipments = purchaseOrder.GetFirstForm()?.Shipments?.ToList();
            
            if (shipments != null)
            {
                this.ShipmentProcessor.CompleteShipment(purchaseOrder, shipments);
            }

            this.SavePurchaseOrderChanges(purchaseOrder);
        }

        protected override bool IsCommandEnable(IOrderGroup order, CommandParameters cp)
        {
            // Enable button if 
            return order.OrderStatus.Equals(OrderStatus.InProgress) ;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Register the newly created button into the commands and add a handler and confirmation text that will ask for the confirmation before performing the action.&lt;img src=&quot;/link/5912abba34e9464b877fcfd828f9dee6.aspx&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Result (2):&lt;/strong&gt; Now build your solution and visit in the commerce area and refresh the purchase order screen, you will see a new button with the name &amp;lsquo;Complete Order&amp;rsquo; and when the user clicks on the button then the order will be mark as complete.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/11148cc0dd6447d4be2ae846de7d0422.aspx&quot; width=&quot;925&quot; height=&quot;383&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Enjoy the coding and share your thoughts &#128522;&lt;/p&gt;</id><updated>2020-10-07T13:51:37.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Exclusion of the partial routed folder/category from the Geta.Seo.Sitemaps</title><link href="https://world.optimizely.com/blogs/sanjay-katiyar/dates/2020/8/geta-seo-sitemap-customization/" /><id>&lt;p&gt;Purpose of the blog to exclude&amp;nbsp;&lt;strong&gt;routed&amp;nbsp;&lt;/strong&gt;commerce catalog folder/category content Urls from the&amp;nbsp;Geta.Seo.Sitemaps sitemap.xml before to it generates. This is out of box functionality in the current&amp;nbsp;Geta.Seo.Sitemaps version thus we have customized it using&amp;nbsp;the`CommerceSitemapXmlGenerator` or `CommerceAndStandardSitemapXmlGenerator` class in my current project for the solution.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Introduction:&amp;nbsp;&amp;nbsp;&lt;/strong&gt;&lt;span&gt;This tool allows you to generate XML sitemaps for search engines to better index your EPiServer sites with&amp;nbsp;&lt;/span&gt;&lt;span&gt;some additional specific features.&lt;/span&gt;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sitemap generation as a scheduled job&lt;/li&gt;
&lt;li&gt;filtering pages by virtual directories&lt;/li&gt;
&lt;li&gt;ability to include pages that are in a different branch than the one of the start page&lt;/li&gt;
&lt;li&gt;ability to generate sitemaps for mobile pages&lt;/li&gt;
&lt;li&gt;it also supports multi-site and multi-language environments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;table&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src=&quot;/link/278b1fe0add64c2d820a201defa9dcf0.aspx&quot; width=&quot;317&quot; height=&quot;239&quot; /&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src=&quot;/link/b16fb2e5f2814c5db139cf11faccf6a8.aspx&quot; width=&quot;783&quot; height=&quot;385&quot; /&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: The Geta.Seo.Sitemaps is not able to&amp;nbsp;&lt;strong&gt;avoid&lt;/strong&gt;&amp;nbsp;the &#39;Services&#39; folder Urls from the sitemap.xml because&amp;nbsp;the folder is partially &lt;strong&gt;routed&lt;/strong&gt; using the IPartialRouter interface. And in the URLs, the folder name does not exist like&amp;nbsp;&lt;a href=&quot;https://www.xyz.com/services/laundry/dry-clean&quot;&gt;https://www.xyz.com/services/laundry/dry-clean&lt;/a&gt;,&amp;nbsp;&lt;a href=&quot;https://www.xyz.com/service/laundary/normal-clean&quot;&gt;https://www.xyz.com/service/laundary/normal-clean&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;However, the ServiceFolderPage is&amp;nbsp;already inherited from &lt;strong&gt;IExcludeFromSitemap &lt;/strong&gt;as below, but still not able to avoid/exclude content from the sitemap.xml.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public class ServiceFolderPage : NodeContent, IExcludeFromSitemap
{
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;:&amp;nbsp;&amp;nbsp;Avoid Urls&amp;nbsp;e.g.&amp;nbsp; &lt;a href=&quot;https://www.xyz.com/dry-clean&quot;&gt;https://www.xyz.com/dry-clean&lt;/a&gt;,&amp;nbsp;&lt;a href=&quot;https://www.xyz.com/normal-clean&quot;&gt;https://www.xyz.com/normal-clean&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;from the generated sitemap.xml because these are services folder content URLs.&lt;/p&gt;
&lt;p&gt;1. Create a utility class like CustomCatalogUrlFilter and pass the current language content and avoid folders list into the &lt;strong&gt;IsUrlFiltered &lt;/strong&gt;method.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;   public class CustomCatalogUrlFilter
    {
        private readonly IContentLoader _contentLoader;

        public CustomCatalogUrlFilter(IContentLoader contentLoader)
        {
            _contentLoader = contentLoader ?? throw new ArgumentNullException(nameof(contentLoader));
        }

        /// &amp;lt;summary&amp;gt;
        /// Gets whether the current content should be filtered out of the Sitemap.
        /// &amp;lt;/summary&amp;gt;
        public bool IsUrlFiltered(IContent page, IList&amp;lt;string&amp;gt; avoidPaths)
        {
            // If the inputs are bad, do nothing.
            if (page == null || avoidPaths?.Any() != true)
                return false;

            // Get the URL segments of the current page and all its ancestors.
            var ancestorsAndSelfRouteSegments =
                _contentLoader
                    .GetAncestorsAndSelf(page)
                    ?.OfType&amp;lt;IRoutable&amp;gt;()
                    .Select(x =&amp;gt; x.RouteSegment)
                    .Where(x =&amp;gt; string.IsNullOrWhiteSpace(x) == false)
                    .ToList();

            // If there are no route segments then something. Return false to be safe.
            if (ancestorsAndSelfRouteSegments?.Any() != true)
                return false;

            // Combine the route segments into a path:
            string pagePathUpper = string.Join(&quot;/&quot;, ancestorsAndSelfRouteSegments).ToUpperInvariant();

            // Check to see whether any path to avoid exists within the current page&#39;s path.
            foreach (string avoidPathUpper in avoidPaths)
            {
                if (string.IsNullOrWhiteSpace(avoidPathUpper))
                    continue;

                // If the page&#39;s path contains a path to avoid, then the page should be filtered out. Return true.
                if (pagePathUpper.Contains(avoidPathUpper.ToUpperInvariant()))
                    return true;
            }

            return false;
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2. Create a custom&amp;nbsp;sitemap XML generator class and derived from the`CommerceSitemapXmlGenerator` or `CommerceAndStandardSitemapXmlGenerator` and override `AddFilteredContentElement` method.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt; public class CustomCommerceCatalogSitemapXmlGenerator : CommerceSitemapXmlGenerator
    {
        private readonly CustomCatalogUrlFilter _customCatalogUrlFilter;

        public CustomCommerceCatalogSitemapXmlGenerator(
            ISitemapRepository sitemapRepository,
            IContentRepository contentRepository,
            UrlResolver urlResolver,
            ISiteDefinitionRepository siteDefinitionRepository,
            ILanguageBranchRepository languageBranchRepository,
            ReferenceConverter referenceConverter,
            IContentFilter contentFilter,
            CustomUrlFilter customCatalogUrlFilter)
            : base(
                sitemapRepository,
                contentRepository,
                urlResolver,
                siteDefinitionRepository,
                languageBranchRepository,
                referenceConverter,
                contentFilter)
        {
            _customCatalogUrlFilter = customCatalogUrlFilter ?? throw new ArgumentNullException(nameof(customCatalogUrlFilter));
        }

        protected override void AddFilteredContentElement(CurrentLanguageContent languageContentInfo, IList&amp;lt;XElement&amp;gt; xmlElements)
        {
            if (ContentFilter.ShouldExcludeContent(languageContentInfo, SiteSettings, SitemapData))
            {
                return;
            }

            var content = languageContentInfo.Content;
            string url;

            var localizableContent = content as ILocalizable;

            if (localizableContent != null)
            {
                string language = string.IsNullOrWhiteSpace(this.SitemapData.Language)
                    ? languageContentInfo.CurrentLanguage.Name
                    : this.SitemapData.Language;

                url = this.UrlResolver.GetUrl(content.ContentLink, language);

                if (string.IsNullOrWhiteSpace(url))
                {
                    return;
                }

                // Make 100% sure we remove the language part in the URL if the sitemap host is mapped to the page&#39;s LanguageBranch.
                if (this.HostLanguageBranch != null &amp;amp;&amp;amp; localizableContent.Language.Name.Equals(this.HostLanguageBranch, StringComparison.InvariantCultureIgnoreCase))
                {
                    url = url.Replace(string.Format(&quot;/{0}/&quot;, this.HostLanguageBranch), &quot;/&quot;);
                }
            }
            else
            {
                url = this.UrlResolver.GetUrl(content.ContentLink);

                if (string.IsNullOrWhiteSpace(url))
                {
                    return;
                }
            }

            url = GetAbsoluteUrl(url);

            var fullContentUrl = new Uri(url);

            if (this.UrlSet.Contains(fullContentUrl.ToString()) || UrlFilter.IsUrlFiltered(fullContentUrl.AbsolutePath, this.SitemapData))
            {
                return;
            }

            // Custom code added to make sure Folder Pages are not ignored when handling paths to avoid:
            if (_customCatalogUrlFilter.IsUrlFiltered(content, this.SitemapData.PathsToAvoid))
                return;

            XElement contentElement = this.GenerateSiteElement(content, fullContentUrl.ToString());

            if (contentElement == null)
            {
                return;
            }

            xmlElements.Add(contentElement);
            this.UrlSet.Add(fullContentUrl.ToString());
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: I am assuming the routing is already done for the folder/category content which you want to exclude from the sitemap.xml.&lt;/p&gt;
&lt;p&gt;Enjoy!&lt;/p&gt;</id><updated>2020-08-02T19:57:19.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Episerver A/B testing and visitor group (personalization) metadata into an analytics payload for consumption with the Google Analytics</title><link href="https://world.optimizely.com/blogs/sanjay-katiyar/dates/2020/3/episerver-ab-testing-and-visitor-group-personalization-metadata-into-an-analytics-payload-for-consumption-with-the-google-analytics/" /><id>&lt;p&gt;The purpose of this blog post is to retrieve the real-time A/B testing and visitor group (personalization) details and feed into the Google Analytics for tracking real-time content item progress on your website.&lt;/p&gt;
&lt;h4&gt;&lt;/h4&gt;
&lt;h3&gt;A/B TESTING&lt;/h3&gt;
&lt;p&gt;A/B Testing is the variations of content items and that compares which variation performs best. When A/B testing running then majors number the conversions for the A and B version, one the gets the best result then wins.&lt;/p&gt;
&lt;p&gt;Episerver CMS and Episerver Commerce each have own three conversion goals and developer can define custom conversion goals using KPI interface. A/B testing is distributed as free AddOn which you can download from NuGet but you need to add&amp;nbsp;&lt;a href=&quot;http://nuget.episerver.com/feed/packages.svc/&quot;&gt;http://nuget.episerver.com/feed/packages.svc/&lt;/a&gt;&amp;nbsp;in Nuget package manager before download. The name of NuGet package that installs under the &lt;em&gt;~/module&lt;/em&gt; folder is &lt;em&gt;EPiServer.Marketing.Testing&lt;/em&gt; after installation you need to update Episerver database schema.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Episerver CMS Conversions Goals:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Landing page: The goal for the visitor to navigate the specify page and only click is counted as a conversion.&lt;/li&gt;
&lt;li&gt;Site Stickiness: The A/B test counts a conversion if a visitor goes from the target page to any other page on the site during the set time period (1-60 minutes).&lt;/li&gt;
&lt;li&gt;Time on Page: Visitors spend some time on the page for specifying numbers second.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Episerver Commerce Conversions Goals:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add to cart: Create a visitor group for the specific product and then the visitor adds that product to a cart, it is counted as a conversion.&lt;/li&gt;
&lt;li&gt;Purchase product: If a site visitor buys the added products on the cart, then it is counted as a conversion.&lt;/li&gt;
&lt;li&gt;Average order: The conversion goal to track completed orders on each of the test pages. The conversion goal totals up the values of all Episerver Commerce carts created by visitors included in the A/B test. The test determines which page variant creates the highest average value for all those carts when picking a winner. If a visitor creates multiple carts, all the (purchased) carts are included in the total, which means that the visitor can &amp;ldquo;convert&amp;rdquo; many times in the test duration. On Episerver Commerce websites using different currencies, the test converts all carts to the same currency.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Developers can create own custom conversions which known as KPI (Key performance indicator)&lt;/li&gt;
&lt;li&gt;For the Commerce-related conversion goals, you required Episerver Commerce and then you can create commerce-related visitor groups criteria for personalization&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;PERSONALIZATION&lt;/h3&gt;
&lt;p&gt;Personalization in Episerver target the website content to selected visitor groups. The personalization feature is based on customized visitor groups that you create based on a set of personalization criteria. Episerver provides two types of criteria one for CMS and another for commerce.&lt;/p&gt;
&lt;p&gt;List of &lt;strong&gt;CMS&lt;/strong&gt; and &lt;strong&gt;Commerce&lt;/strong&gt; visitor group criteria, you can see the difference in both.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/bc54870ebf1b453081f56996bae0ab02.aspx&quot; width=&quot;793&quot; height=&quot;567&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Let&#39;s create visitor groups (personalization) for the CMS and Commerce following the below steps.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Episerver CMS visitor group&lt;/strong&gt;&lt;br /&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go to visitor group area and click to &lt;strong&gt;create&lt;/strong&gt; button&lt;/li&gt;
&lt;li&gt;Tap on &lt;strong&gt;&lt;em&gt;Time and Place criteria&lt;/em&gt;&lt;/strong&gt; options and drag `Time on Site` from the right section in `Drop new creation here` in the left section.&lt;/li&gt;
&lt;li&gt;Enter specific time in seconds e.g. 10&lt;/li&gt;
&lt;li&gt;Enter the visitor group name e.g. Time On-Site Visitor Group&lt;/li&gt;
&lt;li&gt;Click to &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/link/a6ab301b3794464d8252db15f4f8bd05.aspx&quot; width=&quot;798&quot; height=&quot;304&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Episerver Commerce visitor group&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Go to visitor group area and click to &lt;strong&gt;create&lt;/strong&gt; button&lt;/li&gt;
&lt;li&gt;Tap on &lt;strong&gt;&lt;em&gt;Commerce criteria&lt;/em&gt;&lt;/strong&gt; options and `Product in Cart or Wish List` from the right section in `Drop new creation here` in the left section.&lt;/li&gt;
&lt;li&gt;Enter the specific product codes. e.g. 123456&lt;/li&gt;
&lt;li&gt;Enter visitor group name e.g. Add to cart visitor group&lt;/li&gt;
&lt;li&gt;Click to &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/link/ddf28d1fd211485695171200277f5465.aspx&quot; width=&quot;816&quot; height=&quot;362&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;The below screenshot is an example of the cart page AB testing where you can see CMS and Commerce related conversion goals with personalization.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/link/cbd1e34ec6bf43fbbe066bd339ebb35b.aspx&quot; width=&quot;818&quot; height=&quot;596&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/link/6f50a0380c004467972e95f8f09c38a3.aspx&quot; width=&quot;814&quot; height=&quot;531&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;CODE&lt;/h3&gt;
&lt;p&gt;Using the below code you can retrieve the real-time A/B testing and visitor group (personalization) details. I have divided the code into four main part.&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;View Model&lt;/li&gt;
&lt;li&gt;Factory or Service&lt;/li&gt;
&lt;li&gt;Controller&lt;/li&gt;
&lt;li&gt;Assign the payload result into Google Analytics&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;View Model&lt;/h5&gt;
&lt;p&gt;Create two view models with the name&amp;nbsp;&lt;em&gt;AnalyticsViewModel &lt;/em&gt;and&amp;nbsp;&lt;em&gt;VisitorGroupViewModel.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;AnalyticsViewModel &lt;/strong&gt;view model is the payload response view model that returns the real-time A/B testing and personalization details into the response of payload.&lt;/p&gt;
&lt;pre&gt;public class AnalyticsViewModel &lt;br /&gt;{&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public bool AbTestRunning { get; set; }&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp; &amp;nbsp; public string AbTestId { get; set; }&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public string AbTestVariant { get; set; }&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public bool PersonalizationRunning { get; set; }&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public string PersonalizationId { get; set; }&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public string PersonalizationType { get; set; }&lt;br /&gt;&lt;br /&gt;}&lt;br /&gt;&lt;br /&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;strong&gt;VisitorGroupViewModel&lt;/strong&gt; view model that helps to get the visitor group details into the created factory class.&lt;/p&gt;
&lt;pre&gt;public class VisitorGroupViewModel&lt;br /&gt;{&lt;br /&gt;   public string Id { get; set; }&lt;br /&gt;&lt;br /&gt;   public string Name { get; set; }&lt;br /&gt;}&lt;/pre&gt;
&lt;h5&gt;Factory or Service&lt;/h5&gt;
&lt;p&gt;Create a factory class with name &lt;strong&gt;AnalyticsViewModelFactory &lt;/strong&gt;within this factory you need to inject the following dependency that I listed below which helps to get visitor group (personalization) and A/B testing variation details for the current session and feed into the payload response.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IVisitorGroupRepository&lt;/li&gt;
&lt;li&gt;IVisitorGroupRoleRepository&lt;/li&gt;
&lt;li&gt;ITestManager&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;public class AnalyticsViewModelFactory&lt;br /&gt;{&lt;br /&gt;        private readonly IVisitorGroupRepository _visitorGroupRepository;&lt;br /&gt;        private readonly IVisitorGroupRoleRepository _visitorGroupRoleRepository;&lt;br /&gt;        private readonly ITestManager _testManager;&lt;br /&gt;&lt;br /&gt;        public AnalyticsViewModelFactory(&lt;br /&gt;            IVisitorGroupRepository visitorGroupRepository,&lt;br /&gt;            IVisitorGroupRoleRepository visitorGroupRoleRepository,&lt;br /&gt;            ITestManager testManager)&lt;br /&gt;        {&lt;br /&gt;            _visitorGroupRepository = visitorGroupRepository ?? throw new ArgumentNullException(nameof(visitorGroupRepository));&lt;br /&gt;            _visitorGroupRoleRepository = visitorGroupRoleRepository ?? throw new ArgumentNullException(nameof(visitorGroupRoleRepository));&lt;br /&gt;            _testManager = testManager ?? throw new ArgumentNullException(nameof(testManager));&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;        public AnalyticsViewModel Create(HttpContextBase httpContext)&lt;br /&gt;        {&lt;br /&gt;            var visitorGroups = this.GetVisitorGroupsByCurrentUser(httpContext);&lt;br /&gt;            var visitorIds = visitorGroups?.Select(x =&amp;gt; x.Id).ToList();&lt;br /&gt;            var visitorNames = visitorGroups?.Select(x =&amp;gt; x.Name).ToList();&lt;br /&gt;&lt;br /&gt;            var activeTests = _testManager?.GetActiveTests();&lt;br /&gt;            var variantNames = activeTests?.Select(x =&amp;gt; x.Title).ToList();&lt;br /&gt;            var testIds = activeTests?.Select(x =&amp;gt; x?.Id.ToString()).ToList();&lt;br /&gt;&lt;br /&gt;            return new AnalyticsViewModel&lt;br /&gt;            {&lt;br /&gt;                AbTestRunning = activeTests?.Count &amp;gt; decimal.Zero,&lt;br /&gt;                AbTestId = string.Join(&quot;,&quot;, testIds ?? new List&amp;lt;string&amp;gt;()),&lt;br /&gt;                AbTestVariant = string.Join(&quot;,&quot;, variantNames ?? new List&amp;lt;string&amp;gt;()),&lt;br /&gt;                PersonalizationRunning = visitorIds?.Count &amp;gt; decimal.Zero,&lt;br /&gt;                PersonalizationId = string.Join(&quot;,&quot;, visitorIds ?? new List&amp;lt;string&amp;gt;()),&lt;br /&gt;                PersonalizationType = string.Join(&quot;,&quot;, visitorNames ?? new List&amp;lt;string&amp;gt;())&lt;br /&gt;            };&lt;br /&gt;        }&lt;br /&gt;&lt;br /&gt;        private List&amp;lt;VisitorGroupViewModel&amp;gt; GetVisitorGroupsByCurrentUser(HttpContextBase httpContext)&lt;br /&gt;        {&lt;br /&gt;            var visitorGroupList = new List&amp;lt;VisitorGroupViewModel&amp;gt;();&lt;br /&gt;            var user = httpContext.User;&lt;br /&gt;            var visitorGroups = _visitorGroupRepository.List();&lt;br /&gt;&lt;br /&gt;            foreach (var visitorGroup in visitorGroups)&lt;br /&gt;            {&lt;br /&gt;                if (_visitorGroupRoleRepository.TryGetRole(visitorGroup.Name, out var virtualRoleObject))&lt;br /&gt;                {&lt;br /&gt;                    if (virtualRoleObject.IsMatch(user, httpContext))&lt;br /&gt;                    {&lt;br /&gt;                        var viewModel = new VisitorGroupViewModel&lt;br /&gt;                        {&lt;br /&gt;                            Id = visitorGroup.Id.ToString(),&lt;br /&gt;                            Name = visitorGroup.Name&lt;br /&gt;                        };&lt;br /&gt;&lt;br /&gt;                        visitorGroupList.Add(viewModel);&lt;br /&gt;                    }&lt;br /&gt;                }&lt;br /&gt;            }&lt;br /&gt;&lt;br /&gt;            return visitorGroupList;&lt;br /&gt;        }&lt;br /&gt;}&lt;/pre&gt;
&lt;h5&gt;Controller&lt;/h5&gt;
&lt;p&gt;Create an endpoint&amp;nbsp;&lt;em&gt;v1/google/analytics&lt;/em&gt; using a controller with the name of &lt;strong&gt;AnalyticsController&lt;/strong&gt; where you will inject the factory/service to load the data into the payload.&lt;/p&gt;
&lt;pre&gt;&amp;nbsp; [RoutePrefix(&quot;v1/google/analytics&quot;)]&lt;br /&gt;&amp;nbsp; public class AnalyticsController : Controller&lt;br /&gt;&amp;nbsp; {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; private readonly AnalyticsViewModelFactory _analyticsViewModelFactory;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; public AnalyticsController(AnalyticsViewModelFactory analyticsViewModelFactory)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; _analyticsViewModelFactory = analyticsViewModelFactory ?? throw new ArgumentNullException(nameof(analyticsViewModelFactory));&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;br /&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; [HttpGet]&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; public JsonResult GetAnalytics()&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; {&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; return this.JsonNet(_analyticsViewModelFactory.Create(this.HttpContext));&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; }&lt;br /&gt;&amp;nbsp; }&lt;/pre&gt;
&lt;h5&gt;Assign the payload result into Google Analytics&lt;/h5&gt;
&lt;p&gt;When you will hit the &lt;em&gt;v1/google/analytics&lt;/em&gt; endpoint then you get a result similar like below and then pass into the Google Analytics&amp;nbsp;script&amp;nbsp;&lt;em&gt;datalayer &lt;/em&gt;properties.&lt;/p&gt;
&lt;pre&gt;{&lt;br /&gt;&quot;abTestRunning&quot;: true,&lt;br /&gt;&quot;abTestId&quot;: &quot;a0a77ae9-31ec-4d74-8952-32a082535bb1,29de7423-f6ba-4212-a913-34e0517ffda3&quot;,&lt;br /&gt;&quot;abTestVariant&quot;: &quot;Cart A/B Test, AboutUs A/B Test&quot;,&lt;br /&gt;&quot;personalizationRunning&quot;: true,&lt;br /&gt;&quot;personalizationId&quot;: &quot;adb342d2-8ebb-4430-a305-e403c549452a&quot;,&lt;br /&gt;&quot;personalizationType&quot;: &quot;Time On Site Visitor Group&quot;&lt;br /&gt;}&lt;/pre&gt;
&lt;p&gt;Thanks for visiting my blog!&lt;/p&gt;
&lt;h5&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/h5&gt;</id><updated>2020-03-30T08:38:05.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>