Pages - Menu

nopCommerce - Multi Store / Multi Currency / Multi Price for each product

Scope

We have had a few sites went live with nopCommerce. I am in the process to replicate our existing Australia sites to New Zealands sites.
  • Both sites to sell the same products, with stocks coming from the same location.
  • Products dispatch from the same warehouse.
  • Customizable Price, Old Price and Special Price per product per store and not using any currency converter.
  • etc.

What I am trying to achieve is also described here and here by other people. Or as a workitem in codeplex. No viable solution at time of writing this article.

This is base on nopCommerce v3.3.

Business Case

nopCommerce out of the box supports multiple currencies by using a currency converter.



However, this is only practical if your company is 1 entity and want to accept orders from around the world. Currency converter provides an estimate for users on how much they will be spending, but the transaction is still processed in my primary currency - AU dollars in our case.

Example of Currency Converter from my previous project - Bardot

In our business case, we own both of the AU and NZ companies as separate business entities. Our NZ company have their own price and we would like to accept local NZ credit cards in NZ currency. In this scenario, currency converter is not a feasible solution.

Technical Overview

  • Create a new ProductPrices table to store prices for different stores
  • Create an admin screen to handle Product Prices
  • Create a GetCustomPrice() to retrieve price from ProductPrices if store id match
  • Replace any existing controllers or services that use Product.Price to use GetCPrice()

Prerequisite

Code

ProductCustomPrice


We created a new SQL table to store our custom price. 


In our code, we need to create a new domain object for ProductCustomPrice and a mapping file. This is already demonstrated in previous posts in prerequisite.

PriceCalculationService


We need to override the PriceCalculationService.GetFinalPrice() in the Product Price region to use our custom price. We will need a small refactor work in core.

// decimal result = product.Price;
decimal result = GetProductInitPrice(product);

We will write a method to retrieve the custom price from our database.

public virtual ProductCustomPrice GetProductCustomPriceByProductId(int productId)
{
    if (productId == 0)
        return null;
            
    var currentStoreId = _storeContext.CurrentStore.Id;
    var query = _productCustomPriceRepository.Table
                    .Where(p => p.StoreId == currentStoreId 
                            && p.ProductId == productId);

    return query.FirstOrDefault();
}

We will override the method in our new CustomPriceCalculationService class. It will pick up custom price if available, otherwise default.

protected override decimal GetProductInitPrice(Product product)
{
    var customPrice = GetProductCustomPriceByProductId(product.Id);

    if (customPrice != null)
        return customPrice.Price;

    return base.GetProductInitPrice(product);
}

CatalogController


We then need to do the same for OldPrice, SpecialPrice, SpecialPriceStartDateTimeUtc and SpecialPriceEndDateTimeUtc. I will leave this fun exercise for my readers. As a hint, a good starting point is to refactor the CatalogController like what we did for Price.

// decimal oldPriceBase = _taxService.GetProductPrice(product, product.OldPrice, out taxRate);
decimal oldPriceBase = _taxService.GetProductPrice(product, _priceCalculationService.GetProductInitOldPrice(product), out taxRate);

We now able show different Price and SpecialPrice for multi stores.


I built an admin screen base on the TierPrice screen and ProductCustomPrice() CRUD base on Nop.Admin.Controllers.ProductController.TierPriceList().

For example, the Reebok Core Price is $169.99 and Special Price is $118.99.

By introducing Per Store Custom Price, the Reebok NZ Local store would have a customized price of $200, an old price of $180 and a special price of $130.






Other Solutions that didn't work

Tier Price

Initially I was trying to use store specific tier price with 1 unit, and in the hope that the price will be overridden by the tier price. It didn't work in 2 aspects.

Firstly, the way how tier price works is to give a better discount for the customer if they are buying multiples. The logic in nopCommerce will check if the tier price is cheaper than the original and only apply if cheaper. Unfortunately the foreign currency is not always cheaper than the primary.

Secondly, I cannot get store specific for OldPrice or SpecialPrice.

Conclusion

This is not the best solution but the one of the possible solution. If I was a core nopCommerce developer, I would have changed the domain object to work with the prices.

I am very lucky that Australia and New Zealand use the same currency symbol. For wider audience sake, I think the solution needs to be extended to cover other area as well.

Setting up nopCommerce in Azure

I am preparing a migration plan for our nopCommerce sites from our data center to cloud. Since nopCommerce is available in the Azure Gallery, I thought I would just spin one up and see how it goes.

Installation

Gallery

From gallery, I chose nopCommerce. Note that they only have the latest version.



Database

After the instance is up, I have to go thru the standard installation guide for nopCommerce. Fairly straight forward non-cloud stuff.

So far so good, in fact I don't anticipate any issues at this stage.

Store Front

And we have an e-commerce store setup on Azure comfortably easy.



Setup

Plugins

I thought there will be some resistance with the plugins folder permissions, but in fact not. I installed a commercial pack of plugins and everything work straight out of the box. ;)


Email

Unfortunately email services wouldn't work just out of the box.

Since there is no SMTP server setup in the cloud, we will need to use 3rd party service to help us sending out emails. One of the popular one you can find on the Azure Marketplace is SendGrid.

I have my connection string set as follow in nopCommerce, and email sent out and received fine.




Payment Gateway

For our test, I have setup the Payments.PayPalDirect plugin.


After ordering some sample products from our website, the fund would go thru to PayPal Sandbox.



XML Drop

In some of the old fashion system, we are still relying on an older technology that I called Xml Drop or Xml Dump.

Basically what it does is in our site, we will generate an xml file that contains some information and the 3rd party integration system will have a collector to pick up those xml files and perform their other actions. These xml files are not exposed publicly via web, but in a form of secure ftp or a share drive via vpn with authentication and authorization.

Unfortunately I cannot find such products on Azure would do that. The closest item that I found is the Azure Storage but I cannot replace Xml Drop directly.

A better solution is to replace the Xml Drop by using Web Api. If the 3rd party system is not capable to call an api, we will then need to use Queue or ServiceBus.

Conclusion

At first, I anticipated I would get much resistance at the integration points or folder permission as it is a managed IIS instance, but it turns out it was pretty straight forward. 

Using Message Queue with nopCommerce

Introduction

We have a few issues in our IPC process that some of our IPC calls are dropped out. I am looking for a more robust way to handle the calls. One thing I am looking at is the Microsoft Message Queue.

Installing

In Windows 8, MSMQ is part of the Windows Features. Installing is fairly straight forward.



Setup

By using the Management Console, I have created a new private queue called Test.


We will give IIS_IUSRS permissions to access the queue.


nopCommerce

I have skipped some of the steps on about extending the class. They were previously discussed in nopCommerce - Custom Service inherit from Core Service by Dependency Injection and new interface.

MessageQueueService

We will create a very simple class to handle MessageQueue operations. I would recommend something more robust and reliable for real life application.

using System.Messaging;

public interface IMessageQueueService
{
    void SendQueue(QueuedEmail queuedEmail);
}

public class MessageQueueService : IMessageQueueService
{
    public override void SendQueue(QueuedEmail queuedEmail)
    {
        if (queuedEmail == null)
            throw new ArgumentNullException("queuedEmail");

        using (MessageQueue messageQueue = new MessageQueue(@".\private$\Test"))
        {
            using (var transaction = new MessageQueueTransaction())
            {
                transaction.Begin();

                messageQueue.Send(queuedEmail, queuedEmail.Subject, transaction);

                transaction.Commit();
            }
        }
    }
}

DependencyRegistrar.cs


builder.RegisterType<MessageQueueService>().As<IMessageQueueService>();

WorkflowMessageService.cs


To use MessageQueue when sending out emails, we will need to extend the WorkflowMessageService class to consume our MessageQueueService. We need create a property for IoC, change our ctor, and override the SendNotification Method.

// Properties
protected readonly IMessageQueueService _messageQueueService;

Inside our ctor
_messageQueueService = messageQueueService;

In our override SendNotification(), we will call our queue service instead
// _queuedEmailService.InsertQueuedEmail(email);
_messageQueueService.SendQueue(email);

Queue

Do something on the site that will trigger an email. If everything is setup correctly, you should see a message added to the queue.


Conclusion

Although we have only done it for emails, but we can easily use it for any IPC calls. The sky is the limit.

By using a mean of MSMQ as a middle layer, we decouple our system from 3rd party integration points. Thus, conform with the ultimate rule of loose couple and high cohesion in OO design.

This concludes the most basic setup for using MSMQ in nopCommerce. The next part is for our 3rd party system to read from the queue - which is their problem :)