Blog posts by Sanjay Katiyar2023-01-03T16:05:53.0000000Z/blogs/sanjay-katiyar/Optimizely WorldCustom promotion - Buy at least X items from catalog entries and get a discount on related catalog entries./blogs/sanjay-katiyar/dates/2022/12/buy-2x-products-from-a-category-and-get-x-product-discount-on-b-category-products-/2023-01-03T16:05:53.0000000Z<p><strong>Problem</strong>: Buy at least X items from catalog entries and get related catalog entries at a discount that satisfy the below formula.</p>
<p> or</p>
<p>Create a custom promotion to ‘Buy Products for Discount from Other Selections’ and apply the below formula to get a discount on other selections.</p>
<h3><strong><em>Formula = m x n, then get the discounts on n items.</em></strong></h3>
<p>m = Spend at least X items</p>
<p>n = Multiplier</p>
<p>e.g.</p>
<p>m = 2</p>
<p>n = 1, 2,3……</p>
<table>
<tbody>
<tr>
<td>CART</td>
<td></td>
</tr>
</tbody>
</table>
<table>
<tbody>
<tr>
<td>Main Product SKU Qty.</td>
<td>Other Selection SKU Qty.</td>
<td>Eligible Discount Qty.</td>
</tr>
<tr>
<td><strong>2 x 1</strong></td>
<td>1</td>
<td><strong>1</strong></td>
</tr>
<tr>
<td><strong>2 x 2</strong></td>
<td>2</td>
<td><strong>2</strong></td>
</tr>
<tr>
<td><strong>2 x 3</strong></td>
<td>4</td>
<td><strong>3</strong></td>
</tr>
<tr>
<td><strong>2 x 4</strong></td>
<td>5</td>
<td><strong>4</strong></td>
</tr>
<tr>
<td><strong>....</strong></td>
<td>....</td>
<td><strong>....</strong></td>
</tr>
<tr>
<td><strong>m x n</strong></td>
<td>....</td>
<td><strong>n</strong></td>
</tr>
</tbody>
</table>
<p>If you notice the above table ‘Other Selection SKU Qty.’ column, customer added more than the eligible discounted qty in the cart. So, we need to make sure the discount is only eligible for 'Eligible Discount Qty.' not for all ‘Other Selection SKU Qty.’.</p>
<p><strong>Solution:</strong></p>
<ol>
<li>Create a custom entry promotion</li>
</ol>
<pre class="language-csharp"><code> [ContentType(
GUID = "CB99C622-A170-4EF6-B28D-077B18BAFD81",
GroupName = "Custom - Promotions",
DisplayName = "Custom - Buy products for discount from other selection",
Description = "Buy at least X items from catalog entries and get related catalog entries at a discount e.g. (2 + 1), (4 + 2), (6 + 3) ... ",
Order = 30000)]
[PromotionSettings(FixedRedemptionsPerOrder = 1)]
[ImageUrl("~/Static/Img/CustomBuyQuantityGetItemDiscount.png")]
public class CustomBuyQuantityGetItemDiscount : EntryPromotion, IMonetaryDiscount
{
[Display(Order = 10)]
[PromotionRegion("Condition")]
public virtual PurchaseQuantity Condition { get; set; }
[Display(Order = 20)]
[PromotionRegion("Reward")]
public virtual DiscountItems DiscountTarget { get; set; }
[Display(Order = 30)]
[PromotionRegion("Discount")]
public virtual MonetaryReward Discount { get; set; }
}
</code></pre>
<p>2. Create custom discount processor and override following methods:</p>
<ul>
<li>GetPromotionItems
<ul>
<li>Return promotion conditions and reward items.</li>
</ul>
</li>
<li>RewardDescription
<ul>
<li>Read all discounted SKUs with the max eligible qty for discounts.</li>
<li>Create Redemption Description into GetRedemptions.</li>
</ul>
</li>
</ul>
<pre class="language-csharp"><code>public class CustomBuyQuantityGetItemDiscountProcessor : EntryPromotionProcessorBase<CustomBuyQuantityGetItemDiscount>
{
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 => !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<string>() { };
if (promotionData.ExcludedItems != null && promotionData.ExcludedItems.Count > 0)
{
foreach (ContentReference item in promotionData.ExcludedItems)
{
if (_contentLoader.TryGet(item, out NodeContent content))
{
excludedVariants.AddRange(
_contentLoader.GetDescendantsOfType<VariationContent>(content.ContentLink)
.Select(x => x.Code)
.ToList());
}
else if (_contentLoader.TryGet(item, out ProductContent productContent))
{
excludedVariants.AddRange(
_contentLoader.GetDescendantsOfType<VariationContent>(productContent.ContentLink)
.Select(x => x.Code)
.ToList());
}
else
{
excludedVariants.Add(_contentLoader.TryGet<VariationContent>(item)?.Code);
}
}
}
//Read all condition variants
var conditionSkus = new List<string>() { };
foreach (ContentReference item in promotionData.Condition.Items)
{
if (_contentLoader.TryGet(item, out NodeContent content))
{
conditionSkus.AddRange(
_contentLoader.GetDescendantsOfType<VariationContent>(content.ContentLink)
.Select(x => x.Code)
.ToList());
}
else if (_contentLoader.TryGet(item, out ProductContent productContent))
{
conditionSkus.AddRange(
_contentLoader.GetDescendantsOfType<VariationContent>(productContent.ContentLink)
.Select(x => x.Code)
.ToList());
}
else
{
conditionSkus.Add(_contentLoader.TryGet<VariationContent>(item)?.Code);
}
}
//Read all discounted variants
var discountSkus = new List<string>() { };
foreach (ContentReference item in promotionData.DiscountTarget.Items)
{
if (_contentLoader.TryGet(item, out NodeContent content))
{
discountSkus.AddRange(
_contentLoader.GetDescendantsOfType<VariationContent>(content.ContentLink)
.Select(x => x.Code)
.ToList());
}
else if (_contentLoader.TryGet(item, out ProductContent productContent))
{
discountSkus.AddRange(
_contentLoader.GetDescendantsOfType<VariationContent>(productContent.ContentLink)
.Select(x => x.Code)
.ToList());
}
else
{
discountSkus.Add(_contentLoader.TryGet<VariationContent>(item)?.Code);
}
}
//remove the discounted SKUs if exist in the excluded list.
if (excludedVariants.Count() > 0)
{
discountSkus = discountSkus.Where(x => !excludedVariants.Contains(x)).ToList();
conditionSkus = conditionSkus.Where(x => !excludedVariants.Contains(x)).ToList();
}
var totalPurchasedQty = allLineItems.Where(x => conditionSkus.Contains(x.Code)).Sum(x => x.Quantity);
var maxDiscountedQty = Math.Floor(totalPurchasedQty / promotionData.Condition.RequiredQuantity);
if (promotionData.DiscountTarget.MaxQuantity != null &&
maxDiscountedQty > promotionData.DiscountTarget.MaxQuantity)
{
maxDiscountedQty = (decimal)promotionData.DiscountTarget.MaxQuantity;
}
if (maxDiscountedQty == decimal.Zero)
{
return RewardDescription.CreateNotFulfilledDescription(
promotionData,
FulfillmentStatus.InvalidCoupon);
}
var discountedLineItems = allLineItems
.Where(x => discountSkus.Contains(x.Code))
.OrderByDescending(x => x.PlacedPrice)
.ThenBy(x => x.Quantity)
.ToList();
return
RewardDescription.CreateMoneyOrPercentageRewardDescription(
FulfillmentStatus.Fulfilled,
GetRedemptions(promotionData, context, discountedLineItems, maxDiscountedQty),
promotionData,
promotionData.Discount,
context.OrderGroup.Currency,
promotionData.Name);
}
private IEnumerable<RedemptionDescription> GetRedemptions(
CustomBuyQuantityGetItemDiscount promotionData,
PromotionProcessorContext context,
List<ILineItem> discountedLineItems,
decimal maxQty)
{
List<RedemptionDescription> redemptionDescriptionList = new List<RedemptionDescription>();
var applicableCodes = discountedLineItems.Select(x => x.Code);
decimal val2 = GetLineItems(context.OrderForm).Where(li => applicableCodes.Contains(li.Code)).Sum(li => li.Quantity);
decimal num = Math.Min(GetMaxRedemptions(promotionData.RedemptionLimits), val2);
for (int index = 0; index < num; ++index)
{
AffectedEntries entries = context.EntryPrices.ExtractEntries(applicableCodes, Math.Min(maxQty, val2), promotionData);
if (entries != null)
{
redemptionDescriptionList.Add(this.CreateRedemptionDescription(affectedEntries: entries));
}
}
return redemptionDescriptionList;
}
}
</code></pre>
<p>Wishing you a year full of blessing and filled with a new adventure.</p>
<p>Happy new year 2023!</p>
<p>Cheers🥂</p>Delete unused properties and content types in CMS 12/blogs/sanjay-katiyar/dates/2022/10/delete-unused-properties-and-content-types-in-cms-12/2022-10-08T03:22:33.0000000Z<p>The purpose of this blog is to delete unused properties, content references, and content types programmatically and keep clean content.</p>
<p><strong>Problem</strong>: 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 --> 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).</p>
<p>Then the question comes to mind how to delete it? So I tried the following solution and fixed it.</p>
<p><img src="/link/80aa19bf006d491caf7e754c575e93f5.aspx" /></p>
<p><strong></strong></p>
<p><strong>Solution</strong>: </p>
<p>We have two options two remove the missing content type and its references:</p>
<ol>
<li>Remove all references from each content type and then delete it from the Admin -> 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).</li>
<li>Write the code and clean up it programmatically.</li>
</ol>
<p>I am using the second (2) option to achieve this.</p>
<p>Where do you write the code? I will suggest in the <strong>Initialization Module</strong> or create a<strong> Schedule Job </strong>to delete the unused properties and content types. It’s totally up to you :)</p>
<p><em><span style="font-size: 12pt;">Delete the missing properties which are no longer exist in the code for Content Type (PageType/BlockType): (e.g. TeaserBlock -> Sub Title)</span></em></p>
<pre class="language-csharp"><code> private void DeleteUnUsedProperties()
{
var pageTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository<PageType>>();
var propertyDefinitionRepository = ServiceLocator.Current.GetInstance<IPropertyDefinitionRepository>();
foreach (var type in pageTypeRepository.List())
{
foreach (var property in type.PropertyDefinitions)
{
if (property != null && !property.ExistsOnModel)
{
propertyDefinitionRepository.Delete(property);
}
}
this.DeleteContentType(type);
}
var blockTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository<BlockType>>();
foreach (var type in blockTypeRepository.List())
{
foreach (var property in type.PropertyDefinitions)
{
if (property != null && !property.ExistsOnModel)
{
propertyDefinitionRepository.Delete(property);
}
}
this.DeleteContentType(type);
}
}</code></pre>
<p><em><span style="font-size: 12pt;">Delete the content references and missing content type (e.g. TeaserBlock)</span></em></p>
<pre class="language-csharp"><code> private void DeleteContentType(ContentType contentType)
{
if (contentType.ModelType != null)
{
return;
}
if (contentType.Saved != null &&
contentType.Created.HasValue &&
contentType.Created.Value.Year > 2021 &&
contentType.IsAvailable)
{
// Find and deletes the content based on type.
var contentModelUsage = ServiceLocator.Current.GetInstance<IContentModelUsage>();
var contentUsages = contentModelUsage.ListContentOfContentType(contentType);
var contentReferences = contentUsages
.Select(x => x.ContentLink.ToReferenceWithoutVersion())
.Distinct();
var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
foreach (var contentItem in contentReferences)
{
contentRepository.Delete(contentItem, true, EPiServer.Security.AccessLevel.NoAccess);
}
// Delete type of content.
var contentTypeRepository = ServiceLocator.Current.GetInstance<IContentTypeRepository>();
if (contentType.ID > 4)
{
contentTypeRepository.Delete(contentType.ID);
}
}
}</code></pre>
<p><strong>Note</strong>: CotentType.ID > 4 means this will exclude the system/predefined page types e.g. Root Page.</p>
<p>Please leave your feedback in the comment box.</p>
<p>Thanks for your visit!</p>Create a new Optimizely Commerce 14 Project/blogs/sanjay-katiyar/dates/2021/12/create-a-new-optimizely-commerce-14-project/2021-12-16T10:48:08.0000000Z<p>The purpose of blog to create a new Optimizely commerce project in version <strong>14.x</strong> 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.</p>
<p>Before to jump on steps make sure the <a href="/link/b351930dd81d451ba795ac042b56a63c.aspx">development environment</a> & <a href="/link/15a45be2c1664fde9b1c588592fa99f6.aspx">system requirement</a> is ready.</p>
<p><strong></strong></p>
<p><strong>Step 1:</strong></p>
<ul>
<li><strong>Install EPiServer templates:</strong></li>
</ul>
<pre class="language-csharp"><code>dotnet new -i EPiServer.Net.Templates --nuget-source https://nuget.optimizely.com/feed/packages.svc/ --force</code></pre>
<p><strong>--force:</strong> forces the modification and overwriting of required files for installation if required files exist.</p>
<ul>
<li> <strong>Install EPiServer CLI:</strong></li>
</ul>
<pre class="language-csharp"><code>dotnet tool install EPiServer.Net.Cli --global --add-source https://nuget.optimizely.com/feed/packages.svc/</code></pre>
<p><strong>Step 2:</strong></p>
<p>Open the Visual Studio and click on Create a new project link and search “episerver”, you will see EPiServer installed templates for both CMS and Commerce. We are going to create a commerce project so choose “Episerver Commerce Empty Project” and click on next.</p>
<p><img src="/link/3f2fc87e526f4cb6ad90fcfe07fabe03.aspx" width="752" height="271" /></p>
<p>Enter the project name e.g. QuickDemo and create.</p>
<p><strong><img src="/link/3c807a16ed7a4c18bd1574ec82163997.aspx" width="337" height="338" /></strong></p>
<p><strong>Step 3: </strong></p>
<p><strong>Create database with admin user.</strong></p>
<p>Open the package manager console in visual studio and select default project.</p>
<p><strong>OR</strong></p>
<p>If you are using other command prompt, then reach out the project location using <strong>cd</strong> command e.g., ‘C:\Optimizely\QuickDemo’ and type EPiServer cli commands as mentioned below.</p>
<ul>
<li><strong>CMS Database:</strong></li>
</ul>
<pre class="language-csharp"><code>dotnet-episerver create-cms-database QuickDemo.csproj -S [ServerName] -U [ServerLoginUserName] -P [ServerLoginPassword] -dn QuickDemoCms -du [DataBaseUserName] -dp [DataBasePassword]</code></pre>
<ul>
<li> <strong>Commerce Database:</strong></li>
</ul>
<pre class="language-csharp"><code>dotnet-episerver create-commerce-database QuickDemo.csproj -S [ServerName] -U [ServerLoginUserName] -P [ServerLoginPassword] -dn QuickDemoCommerce -du [DataBaseUserName] -dp [DataBasePassword]</code></pre>
<ul>
<li><strong>Add admin user</strong></li>
</ul>
<pre class="language-csharp"><code>dotnet-episerver add-admin-user QuickDemo.csproj -u admin -p Episerver123! -e admin@example.com -c EcfSqlConnection</code></pre>
<p><strong>Note:</strong> -c = connection string name is the case sensitive so make sure the name is same as in your appsetting.json</p>
<p><strong>-S:</strong> Database server name</p>
<p><strong>-U:</strong> Database server login username</p>
<p><strong>-P:</strong> Database server login password</p>
<p><strong>-dn:</strong> Database name e.g. epiCms</p>
<p><strong>-du:</strong> Database username e.g. sa</p>
<p><strong>-dp: </strong>Database password</p>
<p><strong>-u:</strong> Admin user username/loginname e.g. admin or email</p>
<p><strong>-p:</strong> Admin user password</p>
<p><strong>-e:</strong> Admin user email</p>
<p><strong>-c:</strong> connection string name e.g. EcfSqlConnection</p>
<p><strong></strong></p>
<p><strong></strong></p>
<p><strong>Step 4:</strong></p>
<p>Setup the launch browser for empty site in debug mode and press ctr+f5 or f5 (in windows) to run the site.</p>
<ul>
<li>For login use <strong>/util/login</strong></li>
<li>For CMS view type:<strong> /episerver/cms</strong></li>
</ul>
<p><strong><img src="/link/1ce516ddbb4d48d49b743e0f33ba86ce.aspx" width="625" height="344" /></strong></p>
<p><strong></strong></p>
<p><strong>Step 5: </strong></p>
<p><strong>Enable Commerce Tab: </strong>Once you login into the CMS area then make sure the <img src="/link/070fdf6e1ce64b989a9beba7a0862c1b.aspx" /> icon is visible for you if not exist then follow up the below steps and enable it</p>
<ul>
<li><strong>Add new Administrators group:</strong></li>
</ul>
<p>Admin --> Access Rights --> Administer Groups --> And create a new Administrators group</p>
<p><img src="/link/8e15dde184b94fc9a746f303442dc2b5.aspx" width="583" height="214" /></p>
<ul>
<li><strong>Assign the Administrators group to the created user:</strong></li>
</ul>
<p>Admin --> Access Rights --> Admin Users --> Click on the username</p>
<p><img src="/link/ce504e1b5301405b918be0c89928f7d5.aspx" width="495" height="446" /></p>
<ul>
<li>Now logout and login again then you will see the missing dotted icon on top navigation.</li>
</ul>
<p><img src="/link/2d8fde655c5746a088d826dc73c867e3.aspx" width="577" height="112" /></p>
<p><strong>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.</strong></p>
<p><strong>---</strong></p>
<p><strong>[Optional]</strong></p>
<p>During the commerce empty project setup I faced below errors which is highlighted with the solution.</p>
<p><strong>Error 1:</strong></p>
<ul>
<li>Are you getting “Something Went Wrong” error when entering in commerce area?</li>
</ul>
<p><img src="/link/53d7bb6e95b548acb89fa3e9a4af4ebf.aspx" width="519" height="259" /></p>
<p><strong> Solution:</strong></p>
<ul>
<li>Set the access rights for catalog root node<strong>.</strong></li>
</ul>
<pre class="language-csharp"><code>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);
}
}
}</code></pre>
<ul>
<li>Register the class into Startup.cs </li>
</ul>
<p>e.g. services.AddSingleton<EnableCatalogRoot>();</p>
<ul>
<li>Call the SetCatalogAccessRights() method into the SiteInitialization.</li>
</ul>
<p><img src="/link/18202b811ade427c9097a8333d914e1d.aspx" width="585" height="129" /></p>
<p>Now error is no more after the running the site.</p>
<p><strong>Error 2: “An error occurred while starting the application” <br /></strong></p>
<p><strong><img src="/link/0aefa5824e0342b38b8967a812d42c53.aspx" /></strong></p>
<p><strong>Solution:</strong></p>
<p>Upgrade or downgrade <strong>Configuration.ConfigurationManager</strong> according to your version 5.0.0.0 or 6.0.0.0 in my case I used 5.0.0.0 version.</p>
<p><strong>Merry Christmas!!!</strong></p>Optimizely Developers Meetup India - 26th July/blogs/sanjay-katiyar/dates/2021/7/optimizely-developers-meetup-india/2021-07-21T14:18:11.0000000Z<p><span style="font-size: 14pt;"><strong>Monday, July 26, 2021, 11:30 AM to 12:30 PM (IST) </strong></span></p>
<p>RSVP: <a href="https://www.meetup.com/Optimizely-formerlyEpiserver-Meetup-India/events/279586090/">https://www.meetup.com/Optimizely-formerlyEpiserver-Meetup-India/events/279586090/</a></p>
<p><strong>Agenda:</strong><span><br />Welcome and introductions.<br />Optimizely commerce walkthrough<br />Commerce 14.x highlights</span> </p>
<p><span>Your presence will be highly appreciated.</span></p>
<p> </p>Update GeoIP2 database automatically/blogs/sanjay-katiyar/dates/2021/5/update-geoip2-database-automatically/2021-05-04T06:49:16.0000000Z<p>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.</p>
<p><strong></strong></p>
<p><strong>About GeoIP2:</strong></p>
<p>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.</p>
<p>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.</p>
<p><strong> </strong></p>
<p><strong>Requirement:</strong></p>
<p>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 & Longitude, Metro Code etc.</p>
<p><strong></strong></p>
<p><strong>Solution: </strong></p>
<ol>
<li>Create the schedule job and download GeoLiteCity.dat.gz.</li>
<li>Uncompressed the file and copy the <strong>GeoLite2-City.mmdb</strong> database file on physical location which you have define in basePath.</li>
<li>Read the client IP Address.</li>
<li>Read the downloaded <strong>GeoLite2-City.mmdb</strong> database through <em>DatabaseReader</em> and search the IP address into city database and retrieve zip code/postal code.</li>
</ol>
<p> </p>
<p>Okay, so let's do some coding for achieving the above approaches:</p>
<p><strong>Step1: </strong>Create the schedule job.</p>
<pre class="language-csharp"><code>public class GeoIp2DataBaseUpdateJob : ScheduledJobBase
{
private bool _stopSignaled;
private const string GeoLiteCityFileDownloadUrl = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&suffix=tar.gz";
public GeoIp2DataBaseUpdateJob()
{
this.IsStoppable = true;
}
/// <summary>
/// Called when a user clicks on Stop for a manually started job, or when ASP.NET shuts down.
/// </summary>
public override void Stop()
{
base.Stop();
_stopSignaled = true;
}
/// <summary>
/// Called when a scheduled job executes
/// </summary>
/// <returns>A status message to be stored in the database log and visible from admin mode</returns>
public override string Execute()
{
// 1. Download file
var result = DownloadFile()
// 2. Unzip file
.Bind(UnzipFile)
// 3. Replace at physical location
.Bind(ReplaceCurrentFile)
.Match(
right => $"👍 GeoIP2 database updated successfully.",
left => $"👎 GeoIP2 database update failed.");
if (_stopSignaled)
{
return "Stop of job was called";
}
return result;
}
private static Either<Exception, string> DownloadFile()
{
var licenseKey = ConfigurationManager.AppSettings["Geolocation.LicenseKey"];
var uri = new Uri(GeoLiteCityFileDownloadUrl + $"&license_key={licenseKey}");
return Prelude.Try(
() =>
{
using (var client = new WebClient())
{
var tempDir = Path.GetDirectoryName(Path.GetTempPath());
var localFile = Path.Combine(tempDir, "MaxMind/GeoLite2-City.tar.gz");
var maxMindFolderPath = Path.GetDirectoryName(localFile);
if (!Directory.Exists(maxMindFolderPath) && !string.IsNullOrWhiteSpace(maxMindFolderPath))
{
Directory.CreateDirectory(maxMindFolderPath);
}
client.DownloadFile(uri, localFile);
return localFile;
}
})
.Succ(localFile => Prelude.Right<Exception, string>(localFile))
.Fail(ex => Prelude.Left<Exception, string>(ex));
}
private static Either<Exception, string> ReplaceCurrentFile(string unzippedFileName)
{
return Prelude.Try(
() =>
{
var maxMindDbFilePath = Path.Combine(EPiServerFrameworkSection.Instance.AppData.BasePath, "MaxMind/GeoLite2-City.mmdb");
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 => Prelude.Right<Exception, string>(fileName))
.Fail(ex => Prelude.Left<Exception, string>(ex));
}
private static Either<Exception, string> UnzipFile(string downloadedFileName)
{
return Prelude.Try(
() =>
{
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, "*.mmdb", SearchOption.AllDirectories)?.LastOrDefault();
//Delete .tar.gz file
if (File.Exists(downloadedFileName))
{
File.Delete(downloadedFileName);
}
return filePath;
})
.Succ(fileName => Prelude.Right<Exception, string>(fileName))
.Fail(ex => Prelude.Left<Exception, string>(ex));
}
}
</code></pre>
<p><strong></strong></p>
<p><strong>Step 2: </strong>Update the web.config settings<strong><br /></strong></p>
<ol>
<li>Set the database file path for geolocation provider if you are using for personalization.</li>
<li>Add the <em>basePath </em>for updating the file on a given physical location.</li>
<li>Set MaxMind license key</li>
</ol>
<pre class="language-markup"><code><episerver.framework>
...
<geolocation defaultprovider="maxmind">
<providers>
<add databasefilename="C:\Program Files (x86)\MaxMind\GeoLite2-City.mmdb" name="maxmind" type="EPiServer.Personalization.Providers.MaxMind.GeolocationProvider, EPiServer.ApplicationModules">
</add></providers>
</geolocation>
<appData basePath="C:\Program Files (x86)\MaxMind" />
</episerver.framework>
<appSettings>
...
<add key="Geolocation.LicenseKey" value="{YourLicenseKey}"
</appSettings></code></pre>
<p><strong>Step 3:</strong> Run the job and make sure the file is updating, if the code cause the error then update accordingly.</p>
<p><strong> Step4: </strong>Create the <em>GeoLocationUtility </em>class and read the database file for client IP Address.</p>
<pre class="language-csharp"><code> public class GeoLocationUtility
{
private static string _maxMindDatabaseFileName = "GeoLite2-City.mmdb";
public static GeoLocationViewModel GetGeoLocation(IPAddress address, NameValueCollection config)
{
string text = config["databaseFileName"];
if (!string.IsNullOrEmpty(text))
{
_maxMindDatabaseFileName = VirtualPathUtilityEx.RebasePhysicalPath(text);
config.Remove("databaseFileName");
}
if (string.IsNullOrWhiteSpace(_maxMindDatabaseFileName)
|| !File.Exists(_maxMindDatabaseFileName)
|| address.AddressFamily != AddressFamily.InterNetwork && 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;
}</code></pre>
<p><strong>Step 5: </strong>Finally, call the utility method where you want to get the zip code/postal code value such as:</p>
<pre class="language-csharp"><code> var maxMindDbFilePath = Path.Combine(EPiServerFrameworkSection.Instance.AppData.BasePath, "MaxMind/GeoLite2-City.mmdb");
var config = new NameValueCollection
{
{"databaseFileName", maxMindDbFilePath}
};
var postalCode= GeoLocationUtility.GetGeoLocation(ipAddress, config)?.PostalCode;</code></pre>
<p>I hope you found this informative and helpful, Please leave your valuable comments in the comment box.</p>
<p>Thanks for your visit!</p>Extends order status in Episerver Commerce/blogs/sanjay-katiyar/dates/2021/2/extends-order-status-in-episerver-commerce/2021-02-22T07:09:30.0000000Z<p>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.</p>
<p>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 ‘Open’ and display that order status into commerce area after the order submit or convert from cart to purchase order (_orderRepository.SaveAsPurchaseOrder(cart)).</p>
<p><strong>Note: </strong>The extendable order status is available in Episerver Commerce Version 13</p>
<p><strong><img src="/link/614f99ac5756479699256279b768207d.aspx" width="1016" height="352" /></strong></p>
<p><strong>Ticket Reference</strong>:</p>
<p>The same issue has been reported in this ticket.</p>
<p><a href="/link/fa763174b6114902847607de46244fd6.aspx">https://world.episerver.com/forum/developer-forum/Episerver-Commerce/Thread-Container/2020/1/changing-order-status-away-from-a-custom-order-status/</a></p>
<p>Okay, So let's solve the problem with help of below steps...</p>
<p><strong>Step 1</strong>: Create an order status custom helper class.</p>
<pre class="language-csharp"><code>public static class OrderStatusCustom
{
public static readonly OrderStatus Open = new OrderStatus(1001, "Open");
}</code></pre>
<p><strong>Step 2: </strong> Register the new order status into Episerver commerce initialization process.</p>
<pre class="language-csharp"><code> [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);
}
}
</code></pre>
<p><strong>Step 3</strong>: Set the custom order status for the purchase order and save it on the order submit action.</p>
<pre class="language-csharp"><code>var orderRepository = ServiceLocator.Current.GetInstance<IOrderRepository>();
var po = _orderRepository.Load<IPurchaseOrder>(orderLink.OrderGroupId);
po.OrderStatus = OrderStatusCustom.Open;
orderRepository.Save(purchaseOrder);</code></pre>
<p>From the step 1 to 3 we have changed the purchased order status from 'InProgress' (default) to 'Open'. Now times to display the custom order status in the commerce manager for the purchase order detail and list.</p>
<p><strong>Step 4:</strong> Add the order status string into the listed localization files in the format <strong>OrderStatus_</strong>[CustomOrderStatusName]</p>
<ul>
<li>App_GlobalResources\Orders\OrderStrings.resx </li>
<li>App_GlobalResources\SharedStrings.resx e.g.</li>
</ul>
<p>Key: OrderStatus_Open <br />Value: Open</p>
<p><strong>Step 5: </strong> 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.</p>
<p><img src="/link/e6a2f579aa5348efa37a2ecf6835ce28.aspx" /></p>
<ul>
<li>Include the <strong>/Apps/Order/CustomPrimitives/OrderStatusTitle.ascx</strong> & <strong>/Apps/Order/GridTemplatesOrderStatusTemplate.ascx</strong> files into the CommerceManager project.</li>
<li>Create <strong>OrderStatusTitle.cs </strong>and <strong>GridTemplatesOrderStatusTemplate.cs</strong> file and replace the code.</li>
</ul>
<p><strong>OrderStatusTitle.cs </strong></p>
<pre class="language-markup"><code>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("OrderStrings", typeof(OrderStatus).Name + "_" + ((OrderGroup)orderGroup).Status);
if (string.IsNullOrEmpty(status))
{
status = (string)this.GetGlobalResourceObject("OrderStrings", typeof(OrderStatus).Name + "_" + OrderStatusManager.GetOrderGroupStatus(orderGroup));
}
this.lblOrderStatus.Text = status;
}
public void SaveControlValues(object Sender)
{
}
}</code></pre>
<p><strong>GridTemplatesOrderStatusTemplate.cs</strong></p>
<pre class="language-csharp"><code>public class OrderStatusTemplate : Mediachase.Commerce.Manager.Apps.Order.GridTemplates.OrderStatusTemplate
{
protected void Page_Load(object sender, EventArgs e)
{
string str1 = this.DataItem is DataRowView ? "" : "[undefined]";
string str2 = string.Empty;
IOrderGroup dataItem = this.DataItem as IOrderGroup;
if (dataItem != null)
{
str1 = (string)this.GetGlobalResourceObject("OrderStrings", typeof(OrderStatus).Name + "_" + ((OrderGroup)dataItem).Status);
if (string.IsNullOrEmpty(str1))
{
str1 = (string)this.GetGlobalResourceObject("OrderStrings", typeof(OrderStatus).Name + "_" + OrderStatusManager.GetOrderGroupStatus(dataItem));
}
List<string> source = new List<string>();
if (dataItem is IPurchaseOrder purchaseOrder)
{
if (purchaseOrder.HasAwaitingStockReturns())
source.Add((string)this.GetGlobalResourceObject("OrderStrings", "OrderStatus_HaveAwaitingStockReturns"));
if (purchaseOrder.HasAwaitingReturnCompletable())
source.Add((string)this.GetGlobalResourceObject("OrderStrings", "OrderStatus_HaveAwaitingReturnCompletable"));
if (source.Count<string>() > 0)
str2 = "(" + string.Join(",", source.ToArray()) + ")";
}
}
this.label1.Text = str1;
if (string.IsNullOrEmpty(str2))
return;
this.divAdditionalStatus.Visible = true;
this.label2.Text = str2;
}
}</code></pre>
<ul>
<li>Now open the OrderStatusTitle.ascx file and change the 'XXXXXXX' <strong>Inherits</strong> from the qualified namespace of commerce project e.g. QuickSilver.Commerce.Apps.Order.CustomPrimitives.OrderStatusTitle</li>
</ul>
<p> <code><%@ Control Language="C#" AutoEventWireup="true" CodeBehind="OrderStatusTitle.ascx.cs" Inherits="XXXXXXX.Apps.Order.CustomPrimitives.OrderStatusTitle" %></code></p>
<ul>
<li>Repeat the same process for GridTemplatesOrderStatusTemplate.acsx</li>
</ul>
<p><strong></strong></p>
<p><strong>RESULT :</strong></p>
<p>Purchase order List:</p>
<p><img src="/link/1653f96b80c6484192db435877eab02e.aspx" width="1001" height="173" /></p>
<p>Purchase order Detail:</p>
<p><img src="/link/c3f2635f2270473882255a3d196c2b8a.aspx" width="992" height="454" /></p>
<p>Enjoy the coding and share your thoughts 😊</p>
<p>Thanks for your visit!</p>Multilingual cart validation message /blogs/sanjay-katiyar/dates/2021/1/validates-a-cart-in-multi-language/2021-02-01T05:49:54.0000000Z<p>The purpose of this blog to display the cart validation messages market's language specific and make it user-friendly to the customer for the better understanding. </p>
<p><strong>About the Market in Episerver Commerce?</strong><br />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 <code>Mediachase.Commerce</code> or <code>Mediachase.Commerce.Markets</code> namespaces.</p>
<p><strong>Problem:</strong><br />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.</p>
<p>e.g.</p>
<ul>
<li>United State (en) : Display the cart validation message in <strong>English </strong>language.</li>
<li>France (fr) : Display the cart validation message in <strong>French </strong>language. </li>
</ul>
<p><strong></strong></p>
<p><strong>Solution:</strong></p>
<p>Episerver provides a class method <code>OrderValidationService.ValidateOrder(cart)</code> to validate your cart before to save, using <code>IOrderRepository.Save(cart)</code> 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.</p>
<p>The <code>ValidateOrder(cart)</code> method returns <code>IDictionary<ILineItem, IList<ValidationIssue>> </code>validation issue per ILineItem but <code>ValidationIssue</code> is an enum type that returns validation message in below formats which are not user-friendly and market language specific.</p>
<ul>
<li>CannotProcessDueToMissingOrderStatus</li>
<li>RemovedDueToCodeMissing</li>
<li>RemovedDueToNotAvailableInMarket</li>
<li>RemovedDueToUnavailableCatalog</li>
<li>...</li>
</ul>
<p>So let’s make cart validation message user-friendly in the simple way.</p>
<p><strong>Step 1:</strong> Create an XML file language specific and place the all possible cart validation message like below and placed into the <strong>lang </strong>folder under the site root.</p>
<pre class="language-markup"><code><?xml version="1.0" encoding="utf-8" standalone="yes"?>
<languages>
<language name="English" id="en">
<cart>
<validation>
<CannotProcessDueToMissingOrderStatus>Cannot process due to missing order status.</CannotProcessDueToMissingOrderStatus>
<RemovedDueToCodeMissing>The catalog entry code that maps to the line item has been removed or changed.</RemovedDueToCodeMissing>
<RemovedDueToNotAvailableInMarket>Item has been removed from the cart because it is not available in your market.</RemovedDueToNotAvailableInMarket>
<RemovedDueToUnavailableCatalog>Item has been removed from the cart because the catalog of this entry is not available.</RemovedDueToUnavailableCatalog>
<RemovedDueToUnavailableItem>Item has been removed from the cart because it is not available at this time.</RemovedDueToUnavailableItem>
<RemovedDueToInsufficientQuantityInInventory>Item has been removed from the cart because there is not enough available quantity.</RemovedDueToInsufficientQuantityInInventory>
<RemovedDueToInactiveWarehouse>Item has been removed from the cart because the selected warehouse is inactive.</RemovedDueToInactiveWarehouse>
<RemovedDueToMissingInventoryInformation>Item has been removed due to missing inventory information.</RemovedDueToMissingInventoryInformation>
<RemovedDueToInvalidPrice>Item has been removed due to an invalid price.</RemovedDueToInvalidPrice>
<RemovedDueToInvalidMaxQuantitySetting>Item has been removed due to an invalid setting for maximum quantity.</RemovedDueToInvalidMaxQuantitySetting>
<AdjustedQuantityByMinQuantity>Item quantity has been adjusted due to the minimum quantity threshold.</AdjustedQuantityByMinQuantity>
<AdjustedQuantityByMaxQuantity>Item quantity has been adjusted due to the maximum quantity threshold.</AdjustedQuantityByMaxQuantity>
<AdjustedQuantityByBackorderQuantity>Item quantity has been adjusted due to backorder quantity threshold.</AdjustedQuantityByBackorderQuantity>
<AdjustedQuantityByPreorderQuantity>Item quantity has been adjusted due to the preorder quantity threshold.</AdjustedQuantityByPreorderQuantity>
<AdjustedQuantityByAvailableQuantity>Item quantity has been adjusted due to the available quantity threshold.</AdjustedQuantityByAvailableQuantity>
<PlacedPricedChanged>This item's price has changed since it was added to your cart.</PlacedPricedChanged>
<RemovedGiftDueToInsufficientQuantityInInventory>Gift item has been removed from the cart because there is not enough available quantity.</RemovedGiftDueToInsufficientQuantityInInventory>
<RejectedInventoryRequestDueToInsufficientQuantity>The inventory request for item has been rejected because there is not enough available quantity.</RejectedInventoryRequestDueToInsufficientQuantity>
</validation>
</cart>
</language>
<language name="French" id="fr">
<cart>
<validation>
<CannotProcessDueToMissingOrderStatus>Il ne peut pas être traité en raison d'un statut de commande manquant.</CannotProcessDueToMissingOrderStatus>
<RemovedDueToCodeMissing>Le code d'entrée de catalogue qui correspond à l'élément de campagne a été supprimé ou modifié.</RemovedDueToCodeMissing>
<RemovedDueToNotAvailableInMarket>L'article a été retiré du panier car il n'est pas disponible sur votre marché.</RemovedDueToNotAvailableInMarket>
<RemovedDueToUnavailableCatalog>L'article a été retiré du panier car le catalogue de cette entrée n'est pas disponible.</RemovedDueToUnavailableCatalog>
<RemovedDueToUnavailableItem>L'article a été retiré du panier car il n'est pas disponible pour le moment.</RemovedDueToUnavailableItem>
<RemovedDueToInsufficientQuantityInInventory>L'article a été retiré du panier car la quantité disponible est insuffisante.</RemovedDueToInsufficientQuantityInInventory>
<RemovedDueToInactiveWarehouse>L'article a été retiré du panier car l'entrepôt sélectionné est inactif.</RemovedDueToInactiveWarehouse>
<RemovedDueToMissingInventoryInformation>L'article a été supprimé en raison d'informations d'inventaire manquantes.</RemovedDueToMissingInventoryInformation>
<RemovedDueToInvalidPrice>L'article a été supprimé en raison d'un prix non valide.</RemovedDueToInvalidPrice>
<RemovedDueToInvalidMaxQuantitySetting>L'article a été supprimé en raison d'un paramètre non valide pour la quantité maximale.</RemovedDueToInvalidMaxQuantitySetting>
<AdjustedQuantityByMinQuantity>La quantité d'articles a été ajustée en raison du seuil de quantité minimale.</AdjustedQuantityByMinQuantity>
<AdjustedQuantityByMaxQuantity>La quantité d'articles a été ajustée en raison du seuil de quantité maximale.</AdjustedQuantityByMaxQuantity>
<AdjustedQuantityByBackorderQuantity>La quantité d'articles a été ajustée en raison du seuil de quantité de commandes en souffrance.</AdjustedQuantityByBackorderQuantity>
<AdjustedQuantityByPreorderQuantity>La quantité d'articles a été ajustée en raison du seuil de quantité de précommande.</AdjustedQuantityByPreorderQuantity>
<AdjustedQuantityByAvailableQuantity>La quantité d'articles a été ajustée en raison du seuil de quantité disponible.</AdjustedQuantityByAvailableQuantity>
<PlacedPricedChanged>Le prix de cet article a changé depuis qu'il a été ajouté à vos favoris.</PlacedPricedChanged>
<RemovedGiftDueToInsufficientQuantityInInventory>L'article cadeau a été retiré du panier car la quantité disponible est insuffisante.</RemovedGiftDueToInsufficientQuantityInInventory>
<RejectedInventoryRequestDueToInsufficientQuantity>La demande d'inventaire pour l'article a été rejetée car la quantité disponible n'est pas suffisante.</RejectedInventoryRequestDueToInsufficientQuantity>
</validation>
</cart>
</language>
</languages></code></pre>
<p><strong>Step 2: </strong>Create a model class that will hold the error message and variant code.<strong><br /></strong></p>
<pre class="language-csharp"><code>public class CartValidationIssue
{
public string Message { get; set; }
public string Code{ get; set; }
public bool IsBlank => string.IsNullOrWhiteSpace(this.Message);
public static CartValidationIssue Make(string message, string code)
{
return new CartValidationIssue
{
Message = message,
Code = code,
};
}
}
</code></pre>
<p><strong>Step 3: </strong>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.</p>
<ol>
<li>Use <code>ICurrentMarket</code> interface and get the current market culture</li>
<li>Make sure you have selected correct default language for the current market in commerce manager for e.g. France choose default language <em>francais</em></li>
</ol>
<p> <img src="/link/1fa2f9c7ab8a4d1fa01689b17e515a0d.aspx" width="556" height="222" /></p>
<pre class="language-csharp"><code>public List<CartValidationIssue> ValidateCart(ICart cart)
{
var validationResult = _orderValidationService.ValidateOrder(cart);
var errors =
validationResult
?.Select(lineItemIssueEntry => new
{
LineItemIssues =
lineItemIssueEntry.Value
.Select(validationIssue => new
{
ValidationIssueMessage = this.GetCartValidationMessage(validationIssue),
LineItemCode = lineItemIssueEntry.Key.Code,
})
.ToList(),
})
.SelectMany(lineItemIssueGroup => lineItemIssueGroup.LineItemIssues)
.Select(x => CartValidationIssue.Make(x.ValidationIssueMessage, x.LineItemCode))
.Where(x => !x.IsBlank)
.ToList() ?? new List<CartValidationIssue>();
return errors;
}</code></pre>
<pre class="language-csharp"><code> 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("/cart/validation/CannotProcessDueToMissingOrderStatus", "It cannot process due to missing order status.", cultureInfo);
case ValidationIssue.RemovedDueToCodeMissing:
return LocalizationService.Current.GetStringByCulture("/cart/validation/RemovedDueToCodeMissing", "The catalog entry code that maps to the line item has been removed or changed.", cultureInfo);
case ValidationIssue.RemovedDueToNotAvailableInMarket:
return LocalizationService.Current.GetStringByCulture("/cart/validation/RemovedDueToNotAvailableInMarket", "The catalog entry code that maps to the line item has been removed or changed.", cultureInfo);
case ValidationIssue.RemovedDueToUnavailableCatalog:
return LocalizationService.Current.GetStringByCulture("/cart/validation/RemovedDueToUnavailableCatalog", "The catalog entry code that maps to the line item has been removed or changed.", cultureInfo);
case ValidationIssue.RemovedDueToUnavailableItem:
return LocalizationService.Current.GetStringByCulture("/cart/validation/RemovedDueToUnavailableItem", "Item has been removed from the cart because it is not available at this time.", cultureInfo);
case ValidationIssue.RemovedDueToInsufficientQuantityInInventory:
return LocalizationService.Current.GetStringByCulture("/cart/validation/RemovedDueToInsufficientQuantityInInventory", "Item has been removed from the cart because there is not enough available quantity.", cultureInfo);
case ValidationIssue.RemovedDueToInactiveWarehouse:
return LocalizationService.Current.GetStringByCulture("/cart/validation/RemovedDueToInactiveWarehouse", "Item has been removed from the cart because the selected warehouse is inactive.", cultureInfo);
case ValidationIssue.RemovedDueToMissingInventoryInformation:
return LocalizationService.Current.GetStringByCulture("/cart/validation/RemovedDueToMissingInventoryInformation", "Item has been removed due to missing inventory information.", cultureInfo);
case ValidationIssue.RemovedDueToInvalidPrice:
return LocalizationService.Current.GetStringByCulture("/cart/validation/RemovedDueToInvalidPrice", "Item has been removed due to an invalid price.", cultureInfo);
case ValidationIssue.RemovedDueToInvalidMaxQuantitySetting:
return LocalizationService.Current.GetStringByCulture("/cart/validation/RemovedDueToInvalidMaxQuantitySetting", "Item has been removed due to an invalid setting for maximum quantity.", cultureInfo);
case ValidationIssue.AdjustedQuantityByMinQuantity:
return LocalizationService.Current.GetStringByCulture("/cart/validation/AdjustedQuantityByMinQuantity", "Item quantity has been adjusted due to the minimum quantity threshold", cultureInfo);
case ValidationIssue.AdjustedQuantityByMaxQuantity:
return LocalizationService.Current.GetStringByCulture("/cart/validation/AdjustedQuantityByMaxQuantity", "Item quantity has been adjusted due to the maximum quantity threshold.", cultureInfo);
case ValidationIssue.AdjustedQuantityByBackorderQuantity:
return LocalizationService.Current.GetStringByCulture("/cart/validation/AdjustedQuantityByBackorderQuantity", "Item quantity has been adjusted due to backorder quantity threshold.", cultureInfo);
case ValidationIssue.AdjustedQuantityByPreorderQuantity:
return LocalizationService.Current.GetStringByCulture("/cart/validation/AdjustedQuantityByPreorderQuantity", "Item quantity has been adjusted due to the preorder quantity threshold.", cultureInfo);
case ValidationIssue.AdjustedQuantityByAvailableQuantity:
return LocalizationService.Current.GetStringByCulture("/cart/validation/AdjustedQuantityByAvailableQuantity", "Item quantity has been adjusted due to the available quantity threshold.", cultureInfo);
case ValidationIssue.PlacedPricedChanged:
return LocalizationService.Current.GetStringByCulture("/cart/validation/PlacedPricedChanged", "This item's price has changed since it was added to your favorites.", cultureInfo);
case ValidationIssue.RemovedGiftDueToInsufficientQuantityInInventory:
return LocalizationService.Current.GetStringByCulture("/cart/validation/RemovedGiftDueToInsufficientQuantityInInventory", "Gift item has been removed from the cart because there is not enough available quantity.", cultureInfo);
case ValidationIssue.RejectedInventoryRequestDueToInsufficientQuantity:
return LocalizationService.Current.GetStringByCulture("/cart/validation/RejectedInventoryRequestDueToInsufficientQuantity", "The inventory request for item has been rejected because there is not enough available quantity.", cultureInfo);
}
}</code></pre>
<p><strong>Result:</strong></p>
<p><strong>e.g. </strong>The validation message display for France(fr) market in the French language.</p>
<p><strong><img src="/link/a09257ddf81f44d08058111b8f53f13c.aspx" width="668" height="307" /></strong></p>
<p><strong></strong></p>
<p>Enjoy the coding and share your thoughts 😊</p>
<p>Thanks for your visit!</p>Customize order management in commerce manager/blogs/sanjay-katiyar/dates/2020/10/customize-order-management-in-commerce-manager/2020-10-07T13:51:37.0000000Z<p>The purpose of the blog to customize the order management UI in the commerce manager and handle out-of-box functionality. </p>
<p>In this blog I am going to cover two scenarios:</p>
<ol>
<li>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.<img src="/link/e369a023eee4410ca7c98eed7dd61b0e.aspx" /></li>
<li>Add a complete order button within the order summary section and trigger the button event.</li>
</ol>
<p> <img src="/link/86b9c45f55824a5e9c160d678c7451b0.aspx" width="653" height="265" /></p>
<p>The customization is based on the <strong>QuickSilver</strong> solution but you can try the recommended file and code changes in your solution.</p>
<p><img src="/link/5bef62b679e04d6bb48ae82a57c3dcb6.aspx" width="349" height="535" /></p>
<p>So, Let’s get start coding fun 😊</p>
<h4></h4>
<h3>- 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.</h3>
<ul>
<li>Goto the EPiServer.Reference.Commerce.Manager site and expand the all folder as above screen and visit the folder.</li>
</ul>
<p><em> Folder Path:</em> ..\EPiServer.Reference.Commerce.Manager\Apps\Order\CustomPrimitives</p>
<ul>
<li>Include the OrderCustomer.ascx and add a new class with the same name e.g. ‘OrderCustomer.cs’.</li>
</ul>
<p><img src="/link/8832d4c684ab4fd1a680dcfafa26df12.aspx" width="411" height="416" /></p>
<ul>
<li>Open OrderCustomer.ascx and copy the highlighted <strong>Inherits</strong> tag value (<em>Mediachase.Commerce.Manager.Apps.Order.CustomPrimitives.OrderCustomer</em>)and create the OrderCustomer.cs class and derived from the inherits value.<img src="/link/a6b02b07da2045a59d26d09da1426e27.aspx" /></li>
<li>Override the OnPreRender(EventArgs e) method and fetch the purchase order shipping address detail using the order helper class
<pre class="language-csharp"><code>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 > 0
? OrderHelper.GetPurchaseOrderById(this.OrderGroupId)
: null;
var address = purchaseOrder?.GetFirstShipment()?.ShippingAddress;
if (address != null)
{
this.lblCustomerName.Text = $@"{address.FirstName} {address.LastName}";
}
}
}
}
}
</code></pre>
</li>
</ul>
<ul>
<li>And in the last re-place the <strong><em>Inherits</em></strong> attribute value from your created class including full qualified namespace + class name.<img src="/link/a67714cd51e348c58b933b2ed1462214.aspx" /> </li>
</ul>
<p><strong>Result (1):</strong> Now refresh the purchase order and see the changes.</p>
<p><img src="/link/36a49e0d58fd45f8b5d88d74d2ade13b.aspx" /></p>
<h3>- Add a complete order button within the order summary section and trigger the button event:</h3>
<ul>
<li>Goto the EPiServer.Reference.Commerce.Manager site and expand all folder and open the folder.\</li>
</ul>
<p><em> Folder path:</em> …\EPiServer.Reference.Commerce.Manager\Apps\Order\Config\View\Forms.</p>
<ul>
<li>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 ‘btn_CompleteOrderBtn’ and permissions.<img src="/link/f93ed9fef2de4074bdd01d4a3f3e288a.aspx" width="1046" height="318" /></li>
</ul>
<p>Create the CompletePurchaseOrderHandler class and inherit it from the TransactionCommandHandler and override the DoCommand method.</p>
<p><img src="/link/64da555f1fad424c800c7eab3c8bd8d1.aspx" width="440" height="366" /></p>
<p>You can choose a transaction command handler on basis of the requirements e.g. purchase order, payment plan, and return form handler, etc...</p>
<ul>
<li>Commerce.Manager.Order.CommandHandlers.PurchaseOrderHandlers</li>
<li>Commerce.Manager.Order.CommandHandlers.PaymentPlanHandlers</li>
<li>Commerce.Manager.Order.CommandHandlers.ReturnFormHandlers</li>
</ul>
<p>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.</p>
<pre class="language-csharp"><code>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) ;
}
}
}</code></pre>
<ul>
<li>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.<img src="/link/5912abba34e9464b877fcfd828f9dee6.aspx" /></li>
</ul>
<p><strong>Result (2):</strong> 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 ‘Complete Order’ and when the user clicks on the button then the order will be mark as complete.</p>
<p><img src="/link/11148cc0dd6447d4be2ae846de7d0422.aspx" width="925" height="383" /></p>
<p>Enjoy the coding and share your thoughts 😊</p>Exclusion of the partial routed folder/category from the Geta.Seo.Sitemaps/blogs/sanjay-katiyar/dates/2020/8/geta-seo-sitemap-customization/2020-08-02T19:57:19.0000000Z<p>Purpose of the blog to exclude <strong>routed </strong>commerce catalog folder/category content Urls from the Geta.Seo.Sitemaps sitemap.xml before to it generates. This is out of box functionality in the current Geta.Seo.Sitemaps version thus we have customized it using the`CommerceSitemapXmlGenerator` or `CommerceAndStandardSitemapXmlGenerator` class in my current project for the solution.</p>
<p><strong>Introduction: </strong><span>This tool allows you to generate XML sitemaps for search engines to better index your EPiServer sites with </span><span>some additional specific features.</span> </p>
<ul>
<li>sitemap generation as a scheduled job</li>
<li>filtering pages by virtual directories</li>
<li>ability to include pages that are in a different branch than the one of the start page</li>
<li>ability to generate sitemaps for mobile pages</li>
<li>it also supports multi-site and multi-language environments</li>
</ul>
<p> </p>
<table>
<tbody>
<tr>
<td><img src="/link/278b1fe0add64c2d820a201defa9dcf0.aspx" width="317" height="239" /></td>
<td><img src="/link/b16fb2e5f2814c5db139cf11faccf6a8.aspx" width="783" height="385" /></td>
</tr>
</tbody>
</table>
<p><strong>Problem</strong>: The Geta.Seo.Sitemaps is not able to <strong>avoid</strong> the 'Services' folder Urls from the sitemap.xml because the folder is partially <strong>routed</strong> using the IPartialRouter interface. And in the URLs, the folder name does not exist like <a href="https://www.xyz.com/services/laundry/dry-clean">https://www.xyz.com/services/laundry/dry-clean</a>, <a href="https://www.xyz.com/service/laundary/normal-clean">https://www.xyz.com/service/laundary/normal-clean</a></p>
<p>However, the ServiceFolderPage is already inherited from <strong>IExcludeFromSitemap </strong>as below, but still not able to avoid/exclude content from the sitemap.xml.</p>
<pre class="language-csharp"><code>public class ServiceFolderPage : NodeContent, IExcludeFromSitemap
{
}</code></pre>
<p><strong>Solution</strong>: Avoid Urls e.g. <a href="https://www.xyz.com/dry-clean">https://www.xyz.com/dry-clean</a>, <a href="https://www.xyz.com/normal-clean">https://www.xyz.com/normal-clean</a></p>
<p>from the generated sitemap.xml because these are services folder content URLs.</p>
<p>1. Create a utility class like CustomCatalogUrlFilter and pass the current language content and avoid folders list into the <strong>IsUrlFiltered </strong>method.</p>
<pre class="language-csharp"><code> public class CustomCatalogUrlFilter
{
private readonly IContentLoader _contentLoader;
public CustomCatalogUrlFilter(IContentLoader contentLoader)
{
_contentLoader = contentLoader ?? throw new ArgumentNullException(nameof(contentLoader));
}
/// <summary>
/// Gets whether the current content should be filtered out of the Sitemap.
/// </summary>
public bool IsUrlFiltered(IContent page, IList<string> 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<IRoutable>()
.Select(x => x.RouteSegment)
.Where(x => 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("/", ancestorsAndSelfRouteSegments).ToUpperInvariant();
// Check to see whether any path to avoid exists within the current page's path.
foreach (string avoidPathUpper in avoidPaths)
{
if (string.IsNullOrWhiteSpace(avoidPathUpper))
continue;
// If the page'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;
}
}</code></pre>
<p>2. Create a custom sitemap XML generator class and derived from the`CommerceSitemapXmlGenerator` or `CommerceAndStandardSitemapXmlGenerator` and override `AddFilteredContentElement` method.</p>
<pre class="language-csharp"><code> 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<XElement> 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's LanguageBranch.
if (this.HostLanguageBranch != null && localizableContent.Language.Name.Equals(this.HostLanguageBranch, StringComparison.InvariantCultureIgnoreCase))
{
url = url.Replace(string.Format("/{0}/", this.HostLanguageBranch), "/");
}
}
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());
}
}</code></pre>
<p>Note: I am assuming the routing is already done for the folder/category content which you want to exclude from the sitemap.xml.</p>
<p>Enjoy!</p>Episerver A/B testing and visitor group (personalization) metadata into an analytics payload for consumption with the Google Analytics/blogs/sanjay-katiyar/dates/2020/3/episerver-ab-testing-and-visitor-group-personalization-metadata-into-an-analytics-payload-for-consumption-with-the-google-analytics/2020-03-30T08:38:05.0000000Z<p>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.</p>
<h4></h4>
<h3>A/B TESTING</h3>
<p>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.</p>
<p>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 <a href="http://nuget.episerver.com/feed/packages.svc/">http://nuget.episerver.com/feed/packages.svc/</a> in Nuget package manager before download. The name of NuGet package that installs under the <em>~/module</em> folder is <em>EPiServer.Marketing.Testing</em> after installation you need to update Episerver database schema.</p>
<p><strong>Episerver CMS Conversions Goals:</strong></p>
<ol>
<li>Landing page: The goal for the visitor to navigate the specify page and only click is counted as a conversion.</li>
<li>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).</li>
<li>Time on Page: Visitors spend some time on the page for specifying numbers second.</li>
</ol>
<p><strong>Episerver Commerce Conversions Goals:</strong></p>
<ol>
<li>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.</li>
<li>Purchase product: If a site visitor buys the added products on the cart, then it is counted as a conversion.</li>
<li>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 “convert” many times in the test duration. On Episerver Commerce websites using different currencies, the test converts all carts to the same currency.</li>
</ol>
<p><strong>Note:</strong></p>
<ul>
<li>Developers can create own custom conversions which known as KPI (Key performance indicator)</li>
<li>For the Commerce-related conversion goals, you required Episerver Commerce and then you can create commerce-related visitor groups criteria for personalization</li>
</ul>
<h3>PERSONALIZATION</h3>
<p>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.</p>
<p>List of <strong>CMS</strong> and <strong>Commerce</strong> visitor group criteria, you can see the difference in both.</p>
<p><img src="/link/bc54870ebf1b453081f56996bae0ab02.aspx" width="793" height="567" /></p>
<p>Let's create visitor groups (personalization) for the CMS and Commerce following the below steps.</p>
<p><strong>Episerver CMS visitor group</strong><br /><strong></strong></p>
<ul>
<li>Go to visitor group area and click to <strong>create</strong> button</li>
<li>Tap on <strong><em>Time and Place criteria</em></strong> options and drag `Time on Site` from the right section in `Drop new creation here` in the left section.</li>
<li>Enter specific time in seconds e.g. 10</li>
<li>Enter the visitor group name e.g. Time On-Site Visitor Group</li>
<li>Click to <strong>Save</strong>.</li>
</ul>
<p><img src="/link/a6ab301b3794464d8252db15f4f8bd05.aspx" width="798" height="304" /></p>
<h4>Episerver Commerce visitor group</h4>
<ul>
<li>Go to visitor group area and click to <strong>create</strong> button</li>
<li>Tap on <strong><em>Commerce criteria</em></strong> options and `Product in Cart or Wish List` from the right section in `Drop new creation here` in the left section.</li>
<li>Enter the specific product codes. e.g. 123456</li>
<li>Enter visitor group name e.g. Add to cart visitor group</li>
<li>Click to <strong>Save</strong>.</li>
</ul>
<p><img src="/link/ddf28d1fd211485695171200277f5465.aspx" width="816" height="362" /></p>
<p><span>The below screenshot is an example of the cart page AB testing where you can see CMS and Commerce related conversion goals with personalization.</span></p>
<p><span><img src="/link/cbd1e34ec6bf43fbbe066bd339ebb35b.aspx" width="818" height="596" /></span></p>
<p><span><img src="/link/6f50a0380c004467972e95f8f09c38a3.aspx" width="814" height="531" /></span></p>
<h3>CODE</h3>
<p>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. </p>
<ul>
<li>View Model</li>
<li>Factory or Service</li>
<li>Controller</li>
<li>Assign the payload result into Google Analytics</li>
</ul>
<h5>View Model</h5>
<p>Create two view models with the name <em>AnalyticsViewModel </em>and <em>VisitorGroupViewModel.</em></p>
<p>The <strong>AnalyticsViewModel </strong>view model is the payload response view model that returns the real-time A/B testing and personalization details into the response of payload.</p>
<pre>public class AnalyticsViewModel <br />{<br /><br /> public bool AbTestRunning { get; set; }<br /><br /> public string AbTestId { get; set; }<br /><br /> public string AbTestVariant { get; set; }<br /><br /> public bool PersonalizationRunning { get; set; }<br /><br /> public string PersonalizationId { get; set; }<br /><br /> public string PersonalizationType { get; set; }<br /><br />}<br /><br /></pre>
<p>The <strong>VisitorGroupViewModel</strong> view model that helps to get the visitor group details into the created factory class.</p>
<pre>public class VisitorGroupViewModel<br />{<br /> public string Id { get; set; }<br /><br /> public string Name { get; set; }<br />}</pre>
<h5>Factory or Service</h5>
<p>Create a factory class with name <strong>AnalyticsViewModelFactory </strong>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.</p>
<ul>
<li>IVisitorGroupRepository</li>
<li>IVisitorGroupRoleRepository</li>
<li>ITestManager</li>
</ul>
<pre>public class AnalyticsViewModelFactory<br />{<br /> private readonly IVisitorGroupRepository _visitorGroupRepository;<br /> private readonly IVisitorGroupRoleRepository _visitorGroupRoleRepository;<br /> private readonly ITestManager _testManager;<br /><br /> public AnalyticsViewModelFactory(<br /> IVisitorGroupRepository visitorGroupRepository,<br /> IVisitorGroupRoleRepository visitorGroupRoleRepository,<br /> ITestManager testManager)<br /> {<br /> _visitorGroupRepository = visitorGroupRepository ?? throw new ArgumentNullException(nameof(visitorGroupRepository));<br /> _visitorGroupRoleRepository = visitorGroupRoleRepository ?? throw new ArgumentNullException(nameof(visitorGroupRoleRepository));<br /> _testManager = testManager ?? throw new ArgumentNullException(nameof(testManager));<br /> }<br /><br /> public AnalyticsViewModel Create(HttpContextBase httpContext)<br /> {<br /> var visitorGroups = this.GetVisitorGroupsByCurrentUser(httpContext);<br /> var visitorIds = visitorGroups?.Select(x => x.Id).ToList();<br /> var visitorNames = visitorGroups?.Select(x => x.Name).ToList();<br /><br /> var activeTests = _testManager?.GetActiveTests();<br /> var variantNames = activeTests?.Select(x => x.Title).ToList();<br /> var testIds = activeTests?.Select(x => x?.Id.ToString()).ToList();<br /><br /> return new AnalyticsViewModel<br /> {<br /> AbTestRunning = activeTests?.Count > decimal.Zero,<br /> AbTestId = string.Join(",", testIds ?? new List<string>()),<br /> AbTestVariant = string.Join(",", variantNames ?? new List<string>()),<br /> PersonalizationRunning = visitorIds?.Count > decimal.Zero,<br /> PersonalizationId = string.Join(",", visitorIds ?? new List<string>()),<br /> PersonalizationType = string.Join(",", visitorNames ?? new List<string>())<br /> };<br /> }<br /><br /> private List<VisitorGroupViewModel> GetVisitorGroupsByCurrentUser(HttpContextBase httpContext)<br /> {<br /> var visitorGroupList = new List<VisitorGroupViewModel>();<br /> var user = httpContext.User;<br /> var visitorGroups = _visitorGroupRepository.List();<br /><br /> foreach (var visitorGroup in visitorGroups)<br /> {<br /> if (_visitorGroupRoleRepository.TryGetRole(visitorGroup.Name, out var virtualRoleObject))<br /> {<br /> if (virtualRoleObject.IsMatch(user, httpContext))<br /> {<br /> var viewModel = new VisitorGroupViewModel<br /> {<br /> Id = visitorGroup.Id.ToString(),<br /> Name = visitorGroup.Name<br /> };<br /><br /> visitorGroupList.Add(viewModel);<br /> }<br /> }<br /> }<br /><br /> return visitorGroupList;<br /> }<br />}</pre>
<h5>Controller</h5>
<p>Create an endpoint <em>v1/google/analytics</em> using a controller with the name of <strong>AnalyticsController</strong> where you will inject the factory/service to load the data into the payload.</p>
<pre> [RoutePrefix("v1/google/analytics")]<br /> public class AnalyticsController : Controller<br /> {<br /> private readonly AnalyticsViewModelFactory _analyticsViewModelFactory;<br /> public AnalyticsController(AnalyticsViewModelFactory analyticsViewModelFactory)<br /> {<br /> _analyticsViewModelFactory = analyticsViewModelFactory ?? throw new ArgumentNullException(nameof(analyticsViewModelFactory));<br /> }<br /><br /> [HttpGet]<br /> public JsonResult GetAnalytics()<br /> {<br /> return this.JsonNet(_analyticsViewModelFactory.Create(this.HttpContext));<br /> }<br /> }</pre>
<h5>Assign the payload result into Google Analytics</h5>
<p>When you will hit the <em>v1/google/analytics</em> endpoint then you get a result similar like below and then pass into the Google Analytics script <em>datalayer </em>properties.</p>
<pre>{<br />"abTestRunning": true,<br />"abTestId": "a0a77ae9-31ec-4d74-8952-32a082535bb1,29de7423-f6ba-4212-a913-34e0517ffda3",<br />"abTestVariant": "Cart A/B Test, AboutUs A/B Test",<br />"personalizationRunning": true,<br />"personalizationId": "adb342d2-8ebb-4430-a305-e403c549452a",<br />"personalizationType": "Time On Site Visitor Group"<br />}</pre>
<p>Thanks for visiting my blog!</p>
<h5><code></code><code></code></h5>