Published on

Customize commercetools using API Extensions

Authors

Introduction

commercetools is a dynamically extensible, cloud-native commerce solution. It allows retailers to sculpt a solution that fits their unique needs today, and is flexible to support their evolving business strategy tomorrow.

There are many powerful extensibility features built into commercetools that handle a wide variety of use cases. For an overview of them, see Customization Options for the commercetools Platform.

In this post we will do a deep dive on one powerful technique for customizing commercetools: API Extensions.

What are API Extensions?

API Extensions allow you to modify the response of a commercetools API call to customize behavior.

The commercetools platform provides default data structures and default behavior that is useful for many of the customers. However, each project has its unique requirements. Similar to data structures that can be customized with Custom Types and Product Types, additional behavior can be added. For behavior that needs to be executed before the API call succeeds, API Extensions can be used.

API Extensions, Platform Documentation, commercetools

You may have unique business rules that must be taken into account for cart validation, cart discounts, shipping cost calculations, etc. API Extensions provide a mechanism for these business rules to be applied within the context of a commercetools API’s execution. When commercetools receives an API call for an endpoint you’ve extended, it first performs standard commercetools behaviors on the resource and then calls your code for further business rule processing. Once it gets your updates back, the result is persisted.

To take advantage of this technique, you can expose your business logic via a restful interface and configure commercetools to use it. Alternatively, you can configure commercetools to call a serverless function directly. This removes the need for an HTTPS endpoint. In this scenario the function simply returns the response.

The configuration is handled via commercetools’ /{projectKey}/extensions endpoint where you can specify the destination for the extension (your code) and the trigger for the extension (the API resource and action you are customizing). The destination can be set up to use secrets and access keys for security. Security options will depend on the destination. For instance, AWS uses a secret and access key, Azure uses a key, and HTTP can use an auth header.

Bottom line, you can make runtime changes on the fly. This is powerful stuff! As you might expect, this power comes with some risk: the commercetools API is now dependent on you. If your code fails, the entire API call fails. If your code is slow, the API call will be slow. There are guardrails in place for the latter as you can set a timeoutInMs when you configure the extension.

Let’s drill down on a sample use case to see how to exploit API Extensions.

Implementation Example

Our example implementation will focus on commercetools orders and will use Google’s Cloud Functions service.

Use Case

A retailer wishes to dynamically apply a discount when a cart is created. The discount applied varies based on custom business rules that exist outside the commercetools platform.

Problem

The retailer has not integrated their Google Cloud instance with commercetools.

Solution

Create a commercetools API Extension that will be triggered by calls to the cart resource. When the cart is created, a custom GCP Cloud Function will be called to dynamically apply the right discount to the customer’s cart.

Implementation Steps

Our example implementation is thorough! We will show you how to:

  1. Create a commercetools Project,
  2. Create commercetools Discounts,
  3. Set up a GCP Project,
  4. Create a GCP Cloud Function,
  5. Configure and Deploy the Cloud Function,
  6. Set up commercetools API Extension,
  7. Test commercetools/GCP Integration,
  8. Provide you with resources if you need help.
Step 1: Create a commercetools Project.

If you already have a commercetools project you can skip this step. If not, there is good news: you can easily sign up for a risk-free, fully-functional 60 day commercetools trial. The trial period does introduce a few limits, like the total number of products you can define, but the feature set is rich.

Go to https://commercetools.com/free-trial and fill out the form to get an email with instructions for creating your trial organization and initial project. The process is quite fast because commercetools automates all the work behind the scenes to provision cloud resources for you. Once you have your first Project in place, proceed to the next step.

Step 2: Create commercetools Discounts.

Our example needs two Discount Codes associated with two Cart Discounts. Cart Discounts are used to change the prices of different elements within a cart. If you don’t have discount codes in place already, let’s set them up in the Merchant Center. Login to the Merchant Center and head to the “Cart discount list” which you can find in the site’s left navigation bar under Discounts.

Discounts Menu

Click the “Add cart discount” button in the upper right corner of the Cart discounts page and fill out the form.

Add Cart Discount

Set the following fields.

  • Cart discount name (EN): 5% off all line items
  • Cart discount description (EN): Test discount 1 from an API Extension
  • Discount value type: Relative (%)
  • Discount value (USD): 5
  • Rank (sortOrder): 0.99
  • Discount code: Required
  • The discount will be generated by the following condition(s): This discount applies to all carts without restriction.
  • Cart discount target: Line items
  • Target predicate: This discount applies to all line items without restriction.

Click the Save button at the bottom of the page. Once saved, take a look in the upper right corner to see a few more options:

Duplicate Cart Discount Button

Click the slider to activate the discount and then click the “Duplicate cart discount” button to create a second discount. We will leave some of the settings as they are but change the following:

  • Cart discount name (EN): $5 off your cart
  • Cart discount description (EN): Test discount 2 from an API Extension
  • Discount value type: Absolute (0.00)
  • Discount value (USD): 5
  • Rank (sortOrder): 0.98

Click the save button again.

These discounts are fine for our test use case but they are not very realistic. Most discounts will have at least one rule specifying the conditions for its application. For instance, you might only want to apply a discount if the cart total meets a certain threshold. The Merchant Center provides a powerful rule builder for defining these conditions. You can see details by clicking on the Info Icon icon on the discount editing page next to the label, “The discount will be generated by the following condition(s)”.

Our simplistic discounts can be applied on all carts so the Rank and Stacking options fields are important!

The Stacking options field controls whether commercetools continues applying other matching discounts after applying this one. The checkbox dictates how the stackingMode field is set on the discount resource. If checked, it sets stackingMode to “StopAfterThisDiscount” and if unchecked, to “Stacking”. Since neither of our discounts has Stacking options checked, both could be applied to a single cart.

The Rank field dictates the order discounts will be applied. The discount with the highest rank (closest to 1) will be applied first, the discount with the second highest rank will be applied second, and so on, until no more discounts are available or until a discount is applied that has a stackingMode of “StopAfterThisDiscount”. The Rank supplied in the Merchant Center is used by commercetools to set the sortOrder field on the discount resource.

In our example, the 5% off all line items has a higher Rank so it will be processed first, followed by the $5 off your cart if both are in play. This order matters! Our configuration on a $100.00 cart total will result in a total before tax of $90.00 = (100 * 5%) - 5. If we swapped the rank, the total would be $90.25 = (100 - 5) * 5%. So, be careful how you use Rank.

We now have two cart level discounts but neither will be applied unless associated with a Discount Code. Let’s set those up next by navigating to the “Discount code list” which you can find in the site’s left navigation bar under Discounts. Click the “Add discount code” button in the upper right corner of the window and fill out the form as shown below.

Add Discount Code

Set the following fields.

  • Name (EN): 5% off all line items
  • Description (EN): Test discount code 1 from an API Extension
  • Code: 5%-OFF-ALL-LINE-ITEMS
  • Cart Discounts: 5% off all line items

Click Save and then repeat this process for our other Cart Discount. Click the “Add discount code” button and set up the second Discount code with these values:

  • Name (EN): $5 off your cart
  • Description (EN): Test discount code 2 from an API Extension
  • Code: $5-OFF-YOUR-CART
  • Cart Discounts: $5 off your cart

Click save and we now have two Cart Discounts, each tied to their own Discount Code. Make sure you activate each of the Discount codes by clicking on the Status slider.

Status Deactivated Slider

Let’s use IMPEX to take a look at the discount code objects commercetools created for us. Go to the Login page and click the appropriate API Playground link for your region. From the IMPEX home page, click on the API Playground link in the header and login.

Set Endpoint to ‘Discount Codes’ and Command to ‘Query’. If you have other discount codes in your project, you could set the Where field to just locate our new codes; for instance, set code="5%-OFF-ALL-LINE-ITEMS" OR code="$5-OFF-YOUR-CART". Click GO and copy the results[].id fields from the two objects returned because we will use them in future steps.

In addition to IMPEX, it’s possible to use the HTTP API to interact with the commercetools platform. You can take advantage of tools like Postman or curl to try it out directly. If you are familiar with Postman, commercetools provides a repository containing Postman collections for the platform.

Step 3: Set up a GCP Project.

If you choose to follow the steps in this example on your own, you will need a GCP account, a GCP project and the Google Cloud SDK. If you’re missing any of these things, no worries, you can start running all three for free.

Set up a Google Cloud Account

Go to https://cloud.google.com/free/ to check out the wide array of services available to you on GCP. New customers get a large credit applied to their account to allow for plenty of experimentation before needing to spend money. Click on the “Get Started for Free” link to sign in and get your account up and running.

Set up a Google Cloud Project

Once you have an account, you can set up a project for our example implementation. If you just created a new GCP account, you likely have a default project you can go ahead and use. If you’d like to create a new project, go to https://console.cloud.google.com/projectcreate and set one up by entering a Project name, Organization and Location. In either case, your project will have a “Project ID” which we will use later on so note it for future reference. You can also find your Project ID on your dashboard at https://console.cloud.google.com/home/dashboard.

Install the Google Cloud SDK

The Cloud SDK gives you tools and libraries for interacting with Google Cloud products and services. You can follow Google’s installation instructions for your operating system by going to https://cloud.google.com/sdk/docs/install.

Authenticate using Google Cloud SDK

You need to authorize the gcloud command line interface and the SDK before you can use them. If you’ve not already done so, you can run gcloud init to authorize; see Authorizing Cloud SDK tools for details.

Step 4: Create a GCP Cloud Function.

Let’s look at some code. Open a command line (our examples use bash) and issue these three commands to clone and initialize FTG’s commercetools-gcp-extension repository:

$ git clone https://github.com/FearlessTechnologyGroup/commercetools-gcp-extension
$ cd commercetools-gcp-extension/
$ npm install

This repo contains a cloud function that accepts a commercetools Input object containing a Cart object. The function will apply a discount code based on custom business rules. The function is implemented in index.js.

The entry point is function applyCartDiscount which does 4 things:

  1. Safely destructures req to find the cart object;
  2. Runs our custom business logic to assign a discount;
  3. Logs the discount to be applied so we can test/troubleshoot our implementation;
  4. Responds to commercetools with an Update Action.
/**
 * HTTP Cloud Function to be triggered by a commercetools Extension.
 *
 * @param {Object} req Cloud Function request context.
 *                     More info: https://expressjs.com/en/api.html#req
 * @param {Object} res Cloud Function response context.
 *                     More info: https://expressjs.com/en/api.html#res
 */
exports.applyCartDiscount = (req, res) => {
  // req.body should contain a commercetools Input object
  // req.body.resource should contain a cart
  // in place of input validation, we will
  // 1. safely destructure req to find the cart object
  const { body } = req || {};
  const { resource } = body || {};
  const { typeId, obj: cart } = resource || {};

  if (typeId === 'cart' && cart) {
    // 2. run our custom business logic to assign a discount
    const discountAction = getDiscountForCart(cart);

    // 3. log messages can be seen in GCP's Log Explorer
    console.log(JSON.stringify(discountAction));

    // 4. respond with a discount action
    if (!discountAction) {
      res.status(200).end(); // no discount to apply
    } else {
      res.status(200).json(discountAction); // apply the discount
    }
  } else { // we don't have a cart object
    res.status(400).json({
      errors: [{
        code: 'InvalidInput',
        message: 'Cart object not found.',
      }]
    });
  }
};

The business rules are implemented in getDiscountForCart. This is a fake implementation which does not even interrogate the cart at all! For demonstration purposes, we are just randomly doing one of four things: returning discount code 1, returning discount code 2, returning both discounts or returning null to indicate that no discount code was applied. Note that the object returned in the first three cases are formatted using the Add DiscountCode update action. The DISCOUNT_CODE_ constants are the discount codes we set up in Step 2.

const getDiscountForCart = () => {
  const DISCOUNT_CODE_1 = '5%-OFF-ALL-LINE-ITEMS';
  const DISCOUNT_CODE_2 = '$5-OFF-YOUR-CART';

  switch (getRandomInt(4)) {
    case 1: // apply discount code 1
      return {
        actions: [
          {
            action: 'addDiscountCode',
            code: DISCOUNT_CODE_1,
          }
        ]
      };
    case 2: // apply discount code 2
      return {
        actions: [
          {
            action: 'addDiscountCode',
            code: DISCOUNT_CODE_2,
          }
        ]
      };
    case 3: // apply both discounts
      return {
        actions: [
          {
            action: 'addDiscountCode',
            code: DISCOUNT_CODE_1,
          },
          {
            action: 'addDiscountCode',
            code: DISCOUNT_CODE_2,
          }
        ]
      };
    default: // don't apply any discounts
      return null;
  }
}

You can run the function locally by executing npm start from the command line. You should see the following:

Serving function...
Function: applyCartDiscount
URL: http://localhost:8080/

Using a tool like curl or Postman, you can send a POST to http://localhost:8080/ with a fake sample body like this to see successful responses (be sure to set a header of “Content-Type” to “application/json”):

{
    "action": "Create",
    "resource": {
        "typeId": "cart",
        "id": "76ff51d8-fe66-4266-b9d7-b0730c95f2c3",
        "obj": {
            "id": "76ff51d8-fe66-4266-b9d7-b0730c95f2c4",
            "version": 1
        }
    }
}

Remove the typeId or obj field in the sample body to get a 400 response.

Step 5: Configure and Deploy the Cloud Function.

Before we deploy the function, we need to do some configuration. There are a few GCP prerequisites we need to fulfill; these only need to be done once in your GCP project:

  1. Make sure that billing is enabled for your Google Cloud project by following the instructions at Confirm billing is enabled on a project.
  2. Enable the Cloud Functions and Cloud Build APIs by following the Before you begin instructions.
  3. Make sure the APIs are enabled and the Cloud Functions Developer role is enabled for your service account by following the Open the Cloud Build Settings page link shown in the instructions. If there was an issue enabling the APIs, you can follow the steps to enable that, and then return the Build Settings to enable the role.

You can deploy locally using the gcloud CLI from the root of the repo; substitute PROJECTID with the ID of your GCP project. If you don’t remember your Project ID, you can retrieve it at https://console.cloud.google.com/home/dashboard. Update the region if you’d like to run it in another GCP region.

gcloud functions deploy applyCartDiscount --project PROJECTID --region us-west3 --runtime nodejs12 --trigger-http

The same command is available in the package.json deploy script. So, assuming you update PROJECTID there, you can execute the same command by executing npm run deploy from the command line.

The gcloud CLI should respond with a message like the following. Note down the url returned as we will use it in the next step.

Deploying function (may take a while - up to 2 minutes)...done.
availableMemoryMb: 256
buildId: 8520323f-96f3-4805-b0c3-bf0c23b052c2
entryPoint: applyCartDiscount
httpsTrigger:
  url: https://us-west3-PROJECTID.cloudfunctions.net/applyCartDiscount
[...]
Step 6: Set up commercetools API Extension.

Next, let’s set up the API Extension in commercetools. Head back over the to API Playground and Set Endpoint to ‘Extensions’ and Command to ‘Create’. Set the Payload field to the JSON below but substitute CF_URL with the url we copied at the end of the last step.

{
  "destination": {
    "type": "HTTP",
    "url": "CF_URL"
  },
  "triggers": [{
    "resourceTypeId": "cart",
    "actions": ["Create"]
  }],
  "timeoutInMs": 1000
}

Click GO and the response should look similar to this:

{
  "id": "2c316a4c-3e7f-47eb-9b7d-22dfaf875031",
  "version": 1,
  "createdAt": "2020-10-17T21:17:59.471Z",
  "lastModifiedAt": "2020-10-17T21:17:59.471Z",
  "lastModifiedBy": {
    "isPlatformClient": true
  },
  "createdBy": {
    "isPlatformClient": true
  },
  "destination": {
    "type": "HTTP",
    "url": "CF_URL"
  },
  "triggers": [
    {
      "resourceTypeId": "cart",
      "actions": [
        "Create"
      ]
    }
  ],
  "timeoutInMs": 1000
}
Step 7: Test commercetools/GCP Integration.

We are now ready to test our integration. We can use IMPEX’s API Playground to create carts and see our discount codes applied. Set Endpoint to ‘Carts’ and Command to ‘Create’’ and Payload to {"currency": "USD", "country": "US"}. Click GO and check field discountCodes.

Each time you click GO, a new cart will be created and our API Extension will get called. You should see one of four values in discountCodes: an empty array to indicate no discount was applied or one or more objects corresponding to the Discount Codes we set up in Step 2. Click GO a few times to see it change as carts are created. Here is one example; note that the Discount Code’s id is referenced instead of the code we sent in our cloud function. This is normal. Compare the discountCode.id values with the two id fields we noted in Step 2.

[...]
"discountCodes": [
  {
    "discountCode": {
      "typeId": "discount-code",
      "id": "acc8b249-855f-420b-9060-5302b061110f"
    },
    "state": "MatchesCart"
  }
]
[...]

If you want to see the corresponding cloud function logs on GCP, go to the Cloud Functions dashboard at https://console.cloud.google.com/functions/list, click the applyCartDiscount link and then on the Logs tab.

You should now have a working example API Extension. While this implementation used fake business rules, you could use it as a starting point for your own experimentation. Finally, note that in our example, we did not add any security infrastructure for our cloud function but it is certainly possible to do so and supported by commercetools. See HTTP Destination Authentication and Securing Google Cloud Functions for more information.

Step 8: Provide you with resources if you need help.

We travelled quite a bit of ground covering API Extensions and showing you the power they provide. If you have questions or need additional help, Fearless Technology Group (FTG) is available to assist you. Shoot us an email at contactus@fearlesstg.com so we can lend a hand.

Next Steps

If you’d like to learn more about commercetools, check out the commercetools Documentation. It’s a great resource for learning about the platform and it’s full of tutorials and examples.