Extending JavaScript: Add MSRPs to your Products

Purchasing incentives come in many forms, and the typical ones tend to be things like promotional discount codes, free or discounted shipping, multi-buy discounts. I've talked before how you can make subtle design changes to the product detail page to encourage users to purchase, but there are other ways to do this: prices.

In particular, one neat thing you can do is show a competing or secondary price that shows just how much of a deal your actual sale price is. Commonly in commerce, a good candidate is the manufacturer's suggested retail price (MSRP), also known as the recommended retail price (RRP).

The idea is that your customers see this as you discounting your products, or otherwise selling them for less than they'd pay elsewhere, so it's a good idea to buy them from you. Typically the MSRP has some sort of styling on it, such as strikethrough, to make it clear that this price doesn't apply — see below.

In this article, I'm going to tell you how you can add this to your product detail and search results pages on your SuiteCommerce Advanced site. One of the primary things we're going to look at is extending the SCA JavaScript, which can be done in many different ways. This involves:

  1. Preparing your product data to have a retail price, if they don't already
  2. Configuring your site's field sets to expose the retail price to the search API
  3. Adding a function to the ItemsKeyMapping module to standardize the price's exposure from the search API to frontend modules
  4. Adding a function to the ItemDetails model to expose this data to the view
  5. Adding properties to the ItemViews context so it can be included in the template
  6. Adding elements to the ItemViews template for prices, to display the MSRP on the frontend
  7. Add styling to the elements, so they stand out (or don't) as intended

Before You Start: Prepare Your Module

As always, before we implement custom functionality, we should prepare a workspace for all the changes — never modify source files directly. In the directory where you keep your site's customizations create the following structure:

  • MsrpDisplay@1.0.0
    • JavaScript
    • Sass
    • Templates

As usual, you'll need to create an ns.package.json file in the top level of the module's directory. After that, register the module in distro.json, making sure to include the JavaScript and Sass as dependencies for the shopping, my account and checkout applications.

Finally, we need to create an entry point file. Create MsrpDisplay.js in the JavaScript folder and, for the timebeing, put the following in it:

define('MsrpDisplay'
, [

  ]
, function
  (

  )
{
  'use strict';

  return {
    mountToApp: function(application)
    {
      console.log('MsrpDisplay loaded!')
    }
  }
});

If you run gulp local and visit your local site, you should see the message appear in the browser console.

Prepare Your Data

There isn't advice to give here except that you'll need to provide MSRPs for each of your products that you want to feature.

To change a product's pricing, you can type its name into the search box in the NetSuite backend. Alternatively, you can go to Lists > Web Site > Items to see all of your items listed. You'll need at least one product to test this functionality, naturally though, I would advise a handful.

When on the Inventory Item page, go to the Sales / Pricing tab and edit the Retail price level. If you don't have a price level for this, search for "creating price levels" in the help center. Make sure you include a 'formatted' version of the price, ie, one that includes that appropriate currency symbol.

Modify Your Field Sets

Once the data has been added to the products, you need to modify your field sets so that it is included in the data that is exposed to the search API.

  1. Go to Setup > SuiteCommerce Advanced > Set Up Web Site and edit the site record for your site
  2. On the following page, go to the Field Sets tab; here you'll need to edit the fields included in the sets for item details (often called details) and search results (often called search)
  3. For each of the sets, add the fields for the retail price and and formatted retail price

Here's what it looks like on my site:

NOTE — on my site, the price level for MSRPs has the pricelevel1 ID. If your site has a different ID, you'll need to change it in the code that follows.

After that, save your changes.

Item Key Mapping

That's all for setup, now we can do some coding. To start, take a look at the ItemsKeyMapping module.

This module maps what is returned from the search API to what you might call 'standard' definitions. These semantics are used throughout SCA, making it easier to name details of an item regardless of how they are generated. What the internal ID? One name for that. What the URL? One name for that, and so on. This also means that you can change how these details are generated without having to track down each place it's used and change it there.

This file feeds into ItemDetails.Model.js, which has some interesting code:

,   getKeyMapping: function()
    {
        if (!this._keyMapping)
        {
            this._keyMapping = _.defaults(Configuration.itemKeyMapping || {}, ItemsKeyMapping.getKeyMapping(Configuration));
        }
        return this._keyMapping;
    }

This is the function that loads the key mapping, but it also has an interesting part to it: when setting the mapping we use _.defaults, which is an Underscore method. What it allows us to do is a clever merging of two mapping files: it takes the first one, which is the Configuration.itemKeyMapping object and uses them as defaults; then it takes the second one, ItemsKeyMapping.getKeyMapping and merges in any values that haven't already been set. Therefore, if we want to add in some of our own methods, we simply we need to add them to the first object.

So, go back to MsrpDisplay.js and replace it with the following code:

define('MsrpDisplay'
, [
    'Backbone'
  , 'SC.Configuration'
  , 'underscore'
  ]
, function
  (
    Backbone
  , Configuration
  , _
  )
{
  'use strict';

  return {
    mountToApp: function(application)
    {
      Configuration.itemKeyMapping = Configuration.itemKeyMapping || {};
      _.extend(Configuration.itemKeyMapping,
      {
        _retailPrice: function _retailPrice(item)
        {
          return item.get('pricelevel1');
        }
      , _retailPriceFormatted: function _retailPriceFormatted(item)
        {
          return item.get('pricelevel1_formatted');
        }
      });
    }
  }
});

It really is quite simple: following the best practices, we are testing to see if Configuration.itemKeyMapping exists, creating it if not, and then using the _.extend Underscore method to add additional methods to it. The methods themselves are pretty simple: take the item and get the specified field value from the API.

The Model

The model is the part of a Backbone application that handles the data. Now that we have a standard way of extracting the retail prices from what is returned from the search API, we need to send this data to the view. We're going to do this by adding a method that gets the retail price. To do this, we're going to use prototyping.

Prototyping is something that's built into JavaScript. It is a simple way to add new properties or methods to existing objects. What we need is a method that gets the retail price.

In MsrpDisplay.js add ItemDetails.Model as a dependency, and then add the following code to the mountToApp property:

ItemDetailsModel.prototype.getRetailPrice = function getRetailPrice()
{
  var price_retail = this.get('_retailPrice')
  , price_retail_formatted = this.get('_retailPriceFormatted');

  return {
    unformatted: price_retail
  , formatted: price_retail_formatted
  };
}

NOTE — as we're only adding one function, we can put this into the entry point file along with our other code. If we were extending the model a lot more, we might consider using a separate file for the model.

So we're prototyping the model, adding on a new method. This method has two properties: one for the retail price and one for the formatted version. You'll notice that the get parts call on the methods we created earlier on. We then return the values in an object.

The View

The next part is to surface the values to the view. As you'll remember, the view is the thing that connects the HTML of the frontend with the data and logic of the site. You'll also remember that you surface the values to the template via the context object, which is what we now need to extend.

Add ItemViews.Price.View as a dependency and then add the following code to mountToApp:

ItemViewsPriceView.prototype.getContext = _.wrap(ItemViewsPriceView.prototype.getContext, function(fn)
{
  var context = fn.apply(this, _.toArray(arguments).slice(1));

  context.priceRetail = this.model.getRetailPrice().unformatted;
  context.priceRetailFormatted = this.model.getRetailPrice().formatted;

  return context;
});

This is a third way that you can extend JavaScript in SCA. While we are prototyping it like we did before, we are using the _.wrap Underscore method, which lets us modify a function before it runs. The getContext function is what generates the context object, which is why we're modifying it.

You can see now how we're passing the data:

  1. Put data into NetSuite
  2. Expose data to search API
  3. Add fields to the layer used to access the search API
  4. Access those fields in the model
  5. Pass those fields over to the view
  6. Add those fields to the view's context, so it can be access by the template

Which means there are only two bits left: updating the template and styling it.

The Template

Since there is no mechanism for overriding a specific part of a template, you will need to override the entire file.

Create item_views_price.tpl in the Templates folder and copy and paste the contents of the original template into the new one. From here, we need to make some modifications to add in the MSRP.

The template contains two main conditionals: show content if the price comes in a range, or if it doesn't. Price ranges are used when the product has varieties (ie is a matrix item) and there are price differences amongst those varities. MSRPs are applicable to both and, as we want it to show in both cases, we will need to insert code in each of the conditional blocks. Insert the following in each of the blocks:

{{#if priceRetailFormatted}}
    <small class="item-views-price-msrp">
        {{translate 'MSRP'}}: {{priceRetailFormatted}}
    </small>
{{/if}}

If priceRetailFormatted is available, then trigger some HTML that will generate it in the template. Simple.

One small bit of admin here: we need to add in the override to ns.package.json so the application knows to replace the old template with this one. Open it up and replace it with the following:

{
  "gulp": {
    "javascript": [
      "JavaScript/*.js"
    ],
    "templates": [
      "Templates/*.tpl"
    ],
    "sass": [
        "Sass/*.scss"
    ]
  },
  "overrides": {
      "suitecommerce/ItemViews@1.1.0/Templates/item_views_price.tpl": "Templates/item_views_price.tpl"
  }
}

So the final bit from here is to style it.

The Sass

It may not surprise you that, by default, we already include some styling for these sorts of prices. For example, we have the item-views-price-old class, so you could use this class if you wanted. However, we're not lazy are we? No. We're going to add in our own Sass.

Create _msrp-display.scss in the Sass folder and in it put the following:

.item-views-price-msrp {
    @extend .old-price
}

OK, we're being a little bit lazy, but it's the good kind of lazy — it's reusing the base class. You can find .old-price in BaseSassStyles > Sass > atoms > _typography.scss. In short, it makes the font size about 30% smaller, uses the base color, lightens it a bit, and then strikes it through. This ensures that MSRP doesn't stand out, but is still apparent.

Test and Deploy

Everything is in place! Time to test!

Start your local server (if it's already running, you'll need to restart). Do a search for the products you added MSRPs to and you should see the MSRPs appear next to the main prices in the search results. Visit the product detail page and you'll see the same thing too.

And that's it. Well done, you just added MSRPs to your site!

Summary

I hope you found this tutorial useful. I like it because it teaches you how simple it is to get standard field data from the backend to the frontend. It also teaches a few ways we have for extending JavaScript within SCA.

  1. _.extend — we use this Underscore method to create a new object out of an existing object, with additional properties. This works in this particular situation because of the code we, NetSuite, pre-emptively put in place for such a situation, which makes use of the _.defaults method.
  2. .prototype — this standard JavaScript operation allows us to add new properties to an object. You can see models as one big object with functions as properties that, well, do stuff. In most cases these functions take some data, do something to do it, and then return it.
  3. .prototype and _.wrap — the _.wrap Underscore method allows us to take an existing function, add in our code, and then execute it. This is necessary as the getContext property is a function (ie a method), and if we want to add to it then we need to take the original code and splice it with our new code.

I think it's also useful to poke around in the files that these MsrpDisplay.js is modifying, because it'll give you a better sense about how they work. This will then give you a better grounding in the future when you come to make other changes to your site.

Further Reading