Get Started with Scriptable Cart

We know that there's a lot you can do to the frontend of a SuiteCommerce Advanced site so that it fits how you want it presented to the world. We also know that with SuiteScript you can add in a lot of customizations to service files so that records are pulled and updated, and that you can run JavaScript on the server. However, there's another part to this equation outside of the usual SCA arena: scriptable cart.

Scriptable cart is functionality that is server-side JavaScript and SuiteScript that runs when a shopper is checking out. It helps plug gaps that would otherwise be difficult to do in normal SCA code by running entirely on the server, with no obvious connections to the frontend. This mainly because scriptable cart is not anything new: we've had this functionality on the NetSuite platform since the days of SiteBuilder, and with the burgeoning power afforded by SCA, it's easy to understand why it may have fallen to the wayside.

To us, and to many of you, it still remains a powerful tool that you can use. So why might you use it? Examples include:

  • Connecting to a third-party tax calculator
  • Changing price levels of items in a way native functionality cannot
  • Creating buy one / get one type offers in a way the promotions engine cannot
  • Paying using stored loyalty points
  • Accepting charitable donations

In fact, there are a load of other scenarios where you may want to do something specific, but the fundamental thing that underpins it all is a desire to modify the behavior of the sales order form.

In this article, we're going to go through the basics of scriptable cart so that we can get a grasp of what it is and when it's appropriate to use. We're also going to talk about best practices to ensure that you're doing the right thing with data and that it performs well. Finally, we'll go through an example to teach some of these principles so that you get an idea of how to do it yourself.

What is Scriptable Cart?

At its most basic, scriptable cart is a single SuiteScript file that you deploy to your NetSuite instance that executes when a sales order is being created. This can be either on the web store or in the backend UI.

It works by listening to events in the client and then performing actions when they're triggered. Generally speaking, those events are grouped as follows:

  • pageInit — after the user logs into the web store
  • fieldChanged — after a field is modified (whether by the customer or a script); eg, an address line
  • postSourcing — after a field is modified that sources information from another field or list; this is similar to the field change event, except it's only called after all sourcing is finished (ie if you have slaved/cascaded field changes)
  • validateLine — before an item is added to or changed in the cart; eg, the user tries to increase the quantity of an item but is prevented because an artificial limit we scripted using this even
  • recalc — after the contents of the shopping cart is changed (it iterates over every line); eg an item is added or removed, or a quantity is changed
  • saveRecord — after the sales order form submit call is made but before it is actually saved

Note that you do not need to use these specific names for the functions we create, rather that these are what we use in the backend to refer to them. Thus, when you create your own scripts, you don't have to use these names but we consider it good practice to do it in some form. For example, if your script executes some code after the shopper attempts to add something to the cart, you could call it validateLineOnItemAdd() or customValidateLine. Doing this will help you (and anyone else) in the future when it comes to code maintenance.

Why Implement a Scriptable Cart (Rather Than an SCA Service File?)

I brought this up a little bit before, but the gist is that you use scriptable cart when you can't accomplish what you need purely through SCA code. This could be because you want to run the code in both the web store and the UI or because you need to modify a line item, which is not something you can do in SCA. For example, if your solution requires you to modify an item rate, then you can't do this through the commerce API: this API only lets you pull item data, not modify it.

Where Can Scriptable Cart Run?

The script you write can run during checkout on a SuiteCommerce Advanced or Sitebuilder site. However, you can also write the script so that it runs when a staff member is placing an order on behalf of a customer in the backend UI.

Typically, however, you choose to execute the script in only one of these two places. There are good reasons for this:

  • Permissions — a UI script runs with the permissions of an administrator, whereas a scriptable cart runs as either customer center or shopper
  • DOM — quite often a client script may access or modify the DOM, but this is not possible in scriptable cart
  • Alerts — in scriptable cart you cannot trigger alerts
  • Performance — in scriptable cart the functions are called a lot more often

Consider it advice, rather than discouragement. There are things we can do to workaround or mitigate these issues.

How Do SCA and Scriptable Cart Communicate?

It is not uncommon to want to send some information to the scriptable cart from SCA, so that you can trigger some function to run on it. To accomplish this, we will typically use a custom transaction body or column field — you set the information you need in SCA and then call it in the scriptable cart.

Basic Process for Setting Up Scriptable Cart

OK, so you know you want to add a scriptable cart to your site — here are the summarized steps for implementing it:

  1. Enable features on your site
  2. Set up any required custom records, columns, fields, etc, you may need for your script
  3. Write the script
  4. Upload the script
  5. Create a script record
  6. Deploy the script

In this article, we're going to go through this process to implement our own scriptable cart. Let's run through those steps now.

Enable Features

Before we do anything, we need to make sure that your site is set up to use scriptable cart. There are two features you need to enable.

Go to Setup > Company > Enable Features and then check Web Presence > Advanced Site Customization and SuiteCloud > Client SuiteScript.

That makes scripting available throughout your entire NetSuite instance. Next, we need to enable it for the web store specifically.

Go to Setup > SuiteCommerce Advanced > Set Up Web Site > Setup and check Scriptable Cart and Checkout.

Scriptable Cart Skeleton

Let us start with something basic so that we at least know that the functionality is working: let us say hello to the world.

While you do not deploy the script like you do SCA modules (ie via Gulp), we can still manage our code this way — we just won't update the rest of the application code (eg distro.json). Keeping it all one place can be useful for context, as well as for things such as if you use version control.

Write a Basic Script

In your Modules directory, create a folder called scriptablecart. In it, create examplescriptable.js with the following:

function debug (message)
{
  nlapiLogExecution('DEBUG', 'Example Scriptable Cart', message);
}

function customRecalc ()
{
  debug('recalc called!')
}

We created a utility function that sends log messages to the backend. Note that unlike the SCA application, we don't have aliases set up for the standard console command, so we have to use the proper NetSuite method.

Then we created our recalc function that, at the moment, simply sends a log message saying that it's alive.

Upload the Script (Create a Script Record)

As we don't deploy the scripts using Gulp, we need to do it the old-fashioned way:

  1. Go to Customization > Scripting > New
  2. Next to the Script File dropdown, click the + (new) button, and in the new window that opens, select the following options:
    • Attach From: Computer
    • Select File: (select the script we just wrote)
  3. Click Create Script Record
  4. In the list of types, click Client
  5. On the next screen, enter the following details:
    • Name: Example Scriptable Cart
    • ID: _example_scriptable_cart
    • Scripts > Recalc Function: customRecalc
    • Deployments:
      • Applies To: Sales Order
      • Status: Released
    • Don't forget to click Add
  6. Click Save

Test the Script

As mentioned above, the recalc function is called whenever the contents of the chart changes. So, to test this out, go to your web store and find an item and add it to your cart. If you already have an item in your cart, adjust its quantity.

When that's done, we can check the script execution log and see if it's logged our message. However, you don't check in the usual place for SCA (the SSP log) as, technically, it is not the SCA SSP that is executing the code: it's in the wider context of the application itself. Thus, we can check the logs for it two places:

  1. Customization > Scripting > Scripts > View (Example Scriptable Cart) > Execution Log
  2. Customization > Scripting > Script Execution Log

The first is the execution log specifically for the script you're viewing, whereas the second is the execution log for every script on your instance. Which one you look at is up to you, for the sake of ease it's probably easier to look at the first as it's specific to the script file.

Working with validateLine

Let's move on. We worked with recalc and now let's take a look at another essential function: validateLine. This event occurs whenever a line in a transaction changes: this could be because the shopper added an item, changed its quantity, or removed it.

In all cases, this function runs before the change is made. This means that it is the perfect mechanism for a wide range of preventative actions. For example, you could write a function to prevent the shopper from adding a particular item to their cart or increasing the quantity over a certain amount.

Because this function performs this check, you must either return true or false (returning nothing is interpreted as false). Returning true allows the change to happen; false blocks it.

In your script file, add the following function:

function customValidateLine (type)
{
  if (type = 'item')
  {
    var itemId = nlapiGetCurrentLineItemValue('item', 'item');
    debug('Line validated: ' + itemId);
  }

  return true
}

If you don't know how to edit a script in NetSuite after you've added it, the quickest way is to type the name of the file into the search box. On the file page, you can then edit it by clicking the Edit button underneath where it says Media Item.

Don't forget that as we've now added another function, we need to associate our custom version with the NetSuite version. We do this in the script's record.

Go to Customization > Scripting > Scripts and then select the script. In the Scripts tab, enter customValidateLine to the validateLine field and save the form.

If you refresh your site and then attempt to modify the contents of the cart, the changes will go through. Then, if you head over to the execution log, you will see the item IDs of the items.

You'll notice that our function accepts parameters, I'll get to this in a minute but I should note that this is not the same as the parameters you see in the script record. The parameters in the record could more accurately be described as custom fields that you pass the function. You would typically use them to make part of the script configurable (ie you want to pass it an external value) or if there's a value the user needs to input that affects how the code should behave. For more information, see our docs on why you might use script parameters, but note that, at the time of writing, there are currently issues affecting their operation.

pageInit and Checking Context

Another useful function is pageInit. In simplest terms, this is called when the transaction form finishes loading or if it's been reset. On the frontend, this can happen at numerous points:

  • When the user goes to the login page
  • The first time a guest adds something to their cart

They're perhaps not good descriptions; instead, think of it this way: the system is being asked to check if they have a transaction record and if they don't, one is initialized.

Don't forget that these functions are also called on the backend. This means that this function will be called when an employee goes to create a new sales order or when an existing one is edited.

With that information, now is a good time to introduce a useful tool for you: a context checker. Remember, as these functions are called on both the front- and backends, you may want to put in conditional checks that cause only certain parts of your code to run at certain times. This isn't just a design choice, it can be important because of the differences between these two environments (eg with permissions).

Add another utility function to our script:

function getContext ()
{
  return nlapiGetContext().getExecutionContext();
}

So we can call on this now to get back information on where the script is being run. Shall we combine this with pageInit? Add:

function customPageInit ()
{
  if (getContext() === 'webstore')
  {
    debug('You\'re in the frontend!');
  }
  else if (getContext() === 'userinterface')
  {
    debug('You\'re in the backend!');
  }
}

Don't forget you have to manually edit the file in the backend to add your changes.

So let's trigger this. On the frontend of your site, sign out, and then add an item to your cart. Visit the execution log and you should see the messages we added before, as well as our new one. Now, in the backend, go to create a new sales order. After the page has finished loading, refresh the execution log and you'll see the other message in our conditional.

Check for Guest (Logged Out) Users

Another important thing to keep in mind is if your scripts attempt to pull data from the user's record then you'll first need to check that they're actually logged in. We can check for this.

In your customPageInit function, add the following:

var userId = nlapiGetFieldValue('entity');
if (parseInt(userId) === 0)
{
  debug('Guest user');
}
else
{
  debug('User id: ' + userId);
}

If there is no value for the entity field then 0 is returned, thus we can use this to check the user's status.

saveRecord and Updating a Custom Transaction Body Field

A common use case for scriptable cart is updating something in the sales order, whether a body field or column, or some other custom record. This typically happens after the order has been successfully placed, so the best function for this is saveRecord.

Like validateLine, this function can be used as a blocker. Thus the user can get all the way to submitting the sales order and then you can run some business logic to check whether it should actually go through. Commonly, this is used to add in additional validation (which then returns false if they fail the test).

Let's add a custom saveRecord function that updates a custom transaction body field to the order the customer places.

In the backend, go to Customization > Lists, Records & Fields > Transaction Body Fields > New. Set it up as follows:

  • Label: Used Scriptable Cart?
  • ID: _used_scriptable_cart
  • Type: Free-Form Text
  • Applies To > Sale: (checked)
  • All other fields can be left to default values

Back in your script file, add in:

function customSaveRecord ()
{
  nlapiSetFieldValue('custbody_used_scriptable_cart', 'T');
  return true
}

Update the backend copy of the script as well as map the saveRecord function to our custom version of it.

When you've done that, go through your site and place an order. When you've done that, look the order up in the backend. In the Custom tab, you should see the custom field has the value we told it to set!

Best Practices

We've covered a great deal of the basics of scriptable cart that you have enough to get started. It should be obvious that it presents a different challenge to typical SCA development: you'll have to rely a lot on your knowledge of (and our resources on) SuiteScript. This is, therefore, not something that a beginner should attempt on a live site without training or experience.

If you're up for the challenge, then the next step is to learn a number of things that will make your code better, faster and more efficient.

Always Include a Custom Recalc Function

If your script is relatively small and only makes a line change, the temptation is only to run checks on modification of that line. However, we have found that a number of issues with scriptable carts have arisen because there were no checks performed during recalculation. Accessing only a line during checkout will likely cause you problems.

Build and Use a Cache Object

Calling records is expensive in terms of performance. You should always try to find ways to minimize them so that your script is as close to pure JavaScript and SuiteScript as possible. One thing that we like to do is build up cache objects after we make calls that we work with those objects 'locally', rather than repeatedly calling the server for it repeatedly.

For example, let's say you that you need to access item pricing information, did you know you can save it as a session object?

var context = nlapiGetContext();
var ITEM_PRICING = context.getSessionObject('ITEM_PRICING');
context.setSessionObject('ITEM_PRICING', JSON.stringify(ITEM_PRICING));

Now, whenever you need pricing information you can call your local variable. This works with records, searches and Suitelets too.

Use a Global Variable to Prevent Recursion

It is entirely possible that no matter how well you write your code, you may end up processing the same records, lines, and fields multiple times at once. This, obviously, is sub-optimal.

Recursion will typically happen when you write some code in a recalc function, which then triggers a line change event, which then requires a recalc event... and so on. An example of this might be that you change an item rate in a recalc function. As you are changing a field, it will trigger a fieldChange event. As the field has changed, it will trigger a recalc to calculate the sub-total.

If we're working with something, even if it's just running through some code, we want to 'lock' it so that we process it serially and not get caught up in a loop. To do this, we can use flags that cause functions to short-circuit and break the loop.

So, for example, we could write a custom recalc function like this:

// Create a variable to be used as a boolean flag
var IS_PROCESSING = false;

function customRecalc (type, action)
{
  // As is good practice, run a check to see if we're in the webstore. If we're not, the function short-circuits and doesn't run any additional code
  if (nlapiGetContext().getExecutionContext() !== 'webstore') {return true}

  // We check what type and action we're dealing with
  if (type === 'item' && (action == 'commit' || action == 'remove'))
  {
    // We check to see if we're already processing some code; if we are, we short-circuit and don't run any more code
    if (IS_PROCESSING) {return true}
    
    // If we're not, then we can run the code related to this function
    try
    {
      // We're now processing code, so we flag it to true for the timebeing. Thus, if another function (or this function) is called again, it won't process until this one is done.
      IS_PROCESSING = true;

      // Now we can run the function we want to run
      theFunctionWeWantToRun();
    }
    
    // Error handling
    catch (e)
    {
      nlapiLogExecution('ERROR', 'Error', e);
    }

    // The last thing we do is set the variable back to false
    finally
    {
      IS_PROCESSING = false;
    }
  }
}

I put in code comments to give you an idea of what's going on. The core idea is that you set it to false until you're processing code, and then you set it to true. Then, throughout your code (not just the custom recalc function), you put in checks for this variable.

In some legacy code, it is not uncommon to find the same logic but instead of a global variable, a transaction body field is used (but the value isn't stored).

fieldChanged and postSourcing Functions

fieldChanged events occur whenever a field is changed by the user or by a client side API call. For example, if you have some special rules for when shipping should not be charged, and this cannot be accomplished with the standard shipping items or promo codes. In this case we need to listen to the shippingcost field change and if the conditions are met set it to 0.

postSourcing is similar, except that it listens to changes to fields which have their values sourced from sublists; for example, if you're listening to shippingtaxcode then you'll have to use a custom postSourcing function. Another crucial difference is that postSourcing is called only after all sourcing is completed — it waits for any slaved or cascaded field changes to complete before calling the user defined function. Therefore, the event handler is not triggered by field changes for a field that does not have any slaved fields.

So, for example, we could code something like this:

function customFieldChange (type, name)
{
  if (nlapiGetContext().getExecutionContext() !== 'webstore') {return true}
  
  if (name === 'shippingcost')
  {
    if (IS_PROCESSING) {return true}

    IS_PROCESSING = true;
    
    // ie theFunctionWeWantToRun();
    nlapiSetFieldValue('shippingcost', 0);
    
    IS_PROCESSING = false;
  }
}

Free shipping for everyone!

We can also do something very similar for postSourcing: in this example, our business case states that shipping tax does not needed to be calculated in the web store as it's already included in the pricing on the shipping cost, so we use the postSourcing function and listen to the shippingtaxcode field.

function customPostSourcing(type, name) {
  if(nlapiGetContext().getExecutionContext() !== 'webstore') {return true}

  if (name === 'shippingtaxcode') {
    if (IS_PROCESSING) {return true}

    IS_PROCESSING = true;

    // ie theFunctionWeWantToRun();
    nlapiSetFieldValue('shippingtax1rate', 0);

    IS_PROCESSING = false;
  }
}

And now with all that, let's take a look a proper example.

Example: Gift Cards

In this example, we are creating a script so that customers can add gift cards to their order. Specifically, we want to offer a special offer: if the shopper buys a gift card worth $50 or more, that order will qualify for free shipping.

For this, we're going to use a non-inventory item for sale with a price of $0 and two transaction column fields: one for the desired value of the gift card and another for the message to be attached to it.

This script makes use of the best practices we described above but uses more modern design decisions. You'll see that we build up a function for our donation item and then, at the bottom, return our custom functions.

var giftcard_item = new function giftcardItemSC ()
{
  var self = this;

  self.needsCommit = false;
  self.isProcessing = false;
  self.updateShipping = false;

  /* Returns a Object structure with specific customer item pricing. */
  this.setCustomizationPricing = function ()
  {
    var giftcardRate = nlapiGetCurrentLineItemValue('item', 'custcol_giftcard_rate')
      , currentRate = nlapiGetCurrentLineItemValue('item', 'rate');

    if (giftcardRate && currentRate && (currentRate !== giftcardRate))
    {
      self.updateShipping = true;
      nlapiSetCurrentLineItemValue('item', 'price', '-1', true, true);
      nlapiSetCurrentLineItemValue('item', 'rate', '' + giftcardRate, true, true);
      self.needsCommit = true;
    }
  };

  this.loopAndProcessCartLines = function (type, action)
  {
    if (type === 'item' && action === 'commit')
    {
      var lines = nlapiGetLineItemCount('item');

      for (var i = 1; i <= lines; i++)
      {
        nlapiSelectLineItem('item', i);
        self.setCustomizationPricing();

        if (self.needsCommit)
        {
          nlapiCommitLineItem('item');
          self.needsCommit = false;
        }
      }
    }
  };

  this.hasGiftCardItem = function ()
  {
    var lines = nlapiGetLineItemCount('item')
      , i = 1
      , has = false
      , giftcardRate;

    while (i <= lines && !has)
    {
      giftcardRate = nlapiGetLineItemValue('item', 'custcol_giftcard_rate', i);
      has = giftcardRate !== '' && parseFloat(giftcardRate) !== 0 && parseFloat(giftcardRate) >= 50;
      i++;
    }

    return has;
  };

  return {
    recalc: function (type, action)
    {
      if (nlapiGetContext().getExecutionContext() !== 'webstore') return true
      if (self.isProcessing) return

      self.isProcessing = true;
      self.loopAndProcessCartLines(type, action);
      self.isProcessing = false;

      return true
    }

  , validateLine: function (type)
    {
      if (nlapiGetContext().getExecutionContext() !== 'webstore') return true

      if (type !== 'item') return true

      var giftcardRate = nlapiGetCurrentLineItemValue('item', 'custcol_giftcard_rate');

      if ((nlapiGetCurrentLineItemValue('item', 'item') === '4500') && (giftcardRate === '' || parseFloat(giftcardRate) === 0))
      {
        alert('Please enter a GiftCard Amount.');
        return false
      }

      return true
    }

  , fieldChange: function (type, name, linenum)
    {
      if (nlapiGetContext().getExecutionContext() !== 'webstore') return true

      if (name === 'shipmethod' || name === 'rate')
      {
        if (self.isProcessing && !self.updateShipping) return true;

        if (self.hasGiftCardItem())
        {
          self.isProcessing = true;
          self.updateShipping = false;
          nlapiSetFieldValue('shippingcost', 0, true, true);
          self.isProcessing = false;
        }
      }
    }

  , postSourcing: function (type, name)
    {
      if (nlapiGetContext().getExecutionContext() !== 'webstore') return true

      if (name === 'shippingtaxcode')
      {
        if (self.hasGiftCardItem())
        {
          if (self.isProcessing) return true

          self.isProcessing = true;
          nlapiSetFieldValue('shippingtax1rate', 0, true, true);
          self.isProcessing = false;
        }
      }
    }
  };
}();

Debugging

During development, we advise you to open a incognito (private) browsing session in your browser and then insert nlapiLogExecution functions into your code to see the data being passed around.

It's important to remove all the calls to log after you're done as they will have a negative impact on performance if triggered. With that said, it's fine to leave them in in case it's important that you have a log in case things go wrong.

If you script goes catastrophically wrong, it can trigger an email to you, a site administrator or some other people in your organization. You can configure who receives notifications in the script record page by going to the Unhandled Errors tab and selecting from the predefined options, or by specifying individual email addresses.

As the tab suggests, this only triggers notifications if there unhandled exceptions, that is something so serious that it prevents the code from running normally. An example would be if you were to add a call to a function called customPageInit but that function does not appear anywhere in the code.

You should refer to the documentation on the steps for creating a script record not only in general but also for the specific section on what happens when exceptions occur and notifications are sent.

Known Issues

When debugging (or, indeed, before you start working) you should be aware of some known issues that affect scriptable cart:

  • Script parameters don't work
  • You cannot use SuiteScript 2.0
  • Changes won't be available in open sessions or browsers
  • Items disappearing from shopping but appearing in checkout
  • Transaction column fields set by the scriptable cart are not shown in the web store
  • Searches over customer’s sales orders may not work as you expect

Final Thoughts

Scriptable cart is an example of what we call in NetSuite terms, a client script but with subtle differences from other kinds. These scripts can be attached to and run on individual forms, or they can be deployed globally and executed on entity and transaction record types. Global client scripts enable centralized management of scripts that can be applied to an entire record type.

The method described in this article is actually just one possible way of implementing scriptable cart: you can also deploy the script against a custom transaction form. You might want to use this method if you run multiple sites and want it only to run on specific ones. You can do this in the Custom Code tab as you customize a transaction form.

Regardless of how you do it, there's a lot of power in scriptable cart. We've listed a number of quality examples in our documentation which you can use as inspiration and reference.

This article was produced as a companion piece to a webinar we did on the same subject. You can see a recording and the slides here.

For a zip of all of the scripts mentioned in this article, see scriptablecart.zip.