How to build an external promotion using lambda functions.
Commerce Layer is different from other ecommerce solutions because we're solely focused on building the best transactional engine possible. As part of that mission, we've build our API to be extremely flexible, enabling developers to build what they need to build. In this article, we'll examine using AWS Lambda functions, which shows exactly what we mean when we talk about flexibility.
For example, for some of the core resources, Commerce Layer enables you to delegate the management to external services. Using external resources, it is possible to use custom logic when working with prices, promotions, payment gateways, and tax calculators.
The below diagram explains how external resources are used by Commerce Layer:
The concept is pretty simple: when a resource configured as external is requested, Commerce Layer will trigger a POST
call to the endpoint defined at the resource level. This endpoint can be a micro-service, a web interface of a legacy system, or an AWS Lambda. The result of the POST
call will be used by Commerce Layer for the intended resource used (i.e. for external prices, the result of the POST
request will be used as the actual price of an SKU).
The goal: implementing a birthday promotion
The goal of this tutorial is to create an external promotion that triggers a special discount on a customer’s birthday. Let’s take a look at how a promotion might be calculated using an external system like an AWS Lambda function.
To begin, we know that promotions are calculated at the shopping cart, requiring a recalculation each time the cart is modified or when a promo code is entered successfully. Upon a cart recalculation, the Commerce Layer promotional engine calculates any promos that might exist. Depending on activation rules, one or more promos will fire off and be added to the cart. If a promo is defined as external, Commerce Layer will execute a POST
request to the endpoint specified as a promotion URL, sending the order in the payload of the request. The external system (in our case, the lambda) will elaborate on the context of the request, and provide a discount value back to Commerce Layer. Commerce Layer takes the response result and applies it to the current order.
The execution flow is depicted in the sequence diagram:
To make this happen with Commerce Layer, and using the AWS lambda function as our external resource, we will do the following:
- Set up a fresh Commerce Layer environment.
- Set up the AWS environment.
- Create a lambda function containing the birthday promotion logic.
- Configure an external promotion in Commerce Layer.
- Set up the API proxy to expose the lambda.
- Wire everything together.
1. Setting up the Commerce Layer environment
If you don’t have a Commerce Layer account, now is the right time to create one. You can create one for free (and it will always be free) here. You don’t need a credit card. You just need to provide your first and last name, your email and a password.
Now that you have an account, you can follow the onboarding tutorial that explains how to create an organization and seed it with test data. If you already have an account, you can use an existing organization, or create a new one. It’s up to you.
Assuming everything goes smoothly, at the end of this step you will have:
- A Commerce Layer account.
- An organization with some data.
- The Commerce Layer CLI installed on your computer, alongside with some of its plugins.
2. Setting up the AWS environment
Setting up an AWS environment is free for testing/development purposes. You can set up your AWS account here. You will need to provide a valid email address and an account name. AWS requires a credit card on file even if you will not pass the free tier limits. Here you can find a comprehensive explanation of AWS’ Free Tier.
There’s quite a lot you can configure when setting up your account, including the region that hosts your environment. The setup process is smooth and you should be presented with a screen like this once done:
For this tutorial we are going to use three AWS Services:
- The AWS Lambda environment — the runtime that will execute our custom promotion code as we will see later in the tutorial.
- An instance (table) of AWS DynamoDB — the NoSQL database that will be used to store all customer birthday information as we will see later in the tutorial.
- The AWS API Gateway — this is how we will expose our lambda on the web. Basically, the service allows for the creation of custom endpoints that can be further connected to other AWS services (such as Lambda).
3. Setting up the DynamoDB for customers’ dates of birth (DOBs)
You can access the DynamoDB dashboard from the AWS console. Type "DynamoDB" in the search bar and select the service:
Select Table and then Create table as shown below:
We need now to create the CUSTOMER_DOB
table as shown in the panel below:
EMAIL
will be our partition key, the primary key that will be used to fetch data in the table. Keep in mind that since DynamoDB is a schema-less NoSQL database, we are not going to create any columns. Attributes will be added starting with the first insertions. Let’s see how that works.
From the DynamoDB Tables page, select the newly created table, CUSTOMER_DOB
:
Then go to Explore items and press Create item:
At this point, we need to add a new object to the table, as shown below:
Let’s add three new attributes of our object:
DOB_D
— The day of the month (1-31). Use Number as the attribute type.DOB_M
— The month of the year (1-12). Use Number as the attribute type.DOB_Y
— The year (e.g. "20222). Use Number as the attribute type.
Once the form is filled with the necessary values, click Create item and a new item will be added to the database:
For the purpose of this demo, our DB setup is now complete. Let’s look at the lambda function.
4. Building the birthday promotion lambda
AWS Lambdas can be used as a way to implement an external service that can be used as an external resource by Commerce Layer. Let’s start by explaining AWS Lambdas.
Let’s unpack this definition, remembering the sequence we showed at the very beginning of this article:
- Event-driven — AWS Lambdas "react" to events. This means that our function is triggered only when a specific event occurs, and will stay active only for the time needed to perform the calculation. This is different from micro-services, for example, which are "always on". Because of this, AWS bills customers on execution time and not on resources used by the code. In our example, the event is the
POST
request coming from Commerce Layer targeted to an API gateway proxy (our external resource endpoint) that eventually triggers the lambda. - Serverless — AWS Lambdas don’t require any provisioning in terms of containers or virtual/physical resources. Resources are automatically managed by the runtime platform.
- Code — Yes, there’s code. In our case, the code will be the logic of the external promotion we want to run. This is the "core" of our lambda function.
Now that we have the terminology down, let’s create a function on AWS Lambda. From the AWS home page click Lambda and you will be directed to your functions:
Click the orange button Create function.
Select Author from scratch and give your function a name. In our case, we are naming it "BirthdayPromoTutorial".
AWS Lambda allows different runtimes for your function, meaning that you can write code in your preferred language. We'll be using JavaScript and the Node.js 14.x runtime.
For the purposes of the tutorial, you don’t need to deviate from the default permissions settings. After you click the Create function button, you will see Amazon’s IDE where you can begin writing the code for the lambda function:
The basic function template receives an event
as input, and then returns a response
:
- The
event
contains the data from Commerce Layer (in our case, the customer’s email address which we specified as the primary key above). - The
response
is the "outcome" (which is the benefit the customer will get as a result of the birthday promotion).
Let’s take a closer look to these two elements in more details.
The event
As we mentioned above, the event provides context as the payload of the POST
request done to the external promotion endpoint. Our documentation for external promotions specifies the structure of the payload:
{
"data": {
"id": "wBXVhKzrnq",
"type": "orders",
"links": { ... },
"attributes": { ... },
"relationships": { ... },
"meta": { ... }
},
"included": [
{
"id": "DvlGRmhdgX",
"type": "markets",
"links": { ... },
"attributes": { ... },
"relationships": { ... },
"meta": { ... }
},
{
"id": "kdPgtRXOKL",
"type": "line_items",
"links": { ... },
"attributes": { ... },
"relationships": { ... },
"meta": { ... }
},
{
"id": "XGZwpOSrWL",
"type": "skus",
"links": { ... },
"attributes": { ... },
"relationships": { ... },
"meta": { ... }
}
{
"id": "BgnguJvXmb",
"type": "addresses",
"links": { ... },
"attributes": { ... },
"relationships": { ... },
"meta": { ... }
},
{
"id": "AlrkugwyVW",
"type": "addresses",
"links": { ... },
"attributes": { ... },
"relationships": { ... },
"meta": { ... }
},
{ ... }
]
}
As you can see, the the customer identifier (aka the primary key, which is the user’s email) can be found in the data.attributes.customer_email
property. This identifier will be used by our lambda function to fetch the customer date of birth from the DynamoDB we created above.
The response
The response is what gets returned to Commerce Layer. From the documentation we expect something like the following:
{
"success": true,
"data": {
"name": "My External Promotion",
"discount_cents": 1500,
"metadata": {
"foo": "bar"
}
}
}
As you can see, data.discount_cents
contains the discount the customer will get as part of the birthday promotion.
The lambda code
Now that we have introduced all the pieces of our lambda, let’s look at the sample code we are going to use to calculate the special discount:
const AWS = require("aws-sdk");
exports.handler = async (event) => {
let body, statusCode;
const headers = {
"Content-Type": "application/json",
};
try {
const customer = JSON.parse(event.body).data.attributes.customer_email;
const customer_dob = await getCustomerDob(customer);
if (customer_dob) {
console.log(customer_dob);
if (isBirthdayToday(customer_dob)) {
statusCode = 200;
body = {
success: true,
data: {
name: "Birthday Promo",
discount_cents: 1500,
},
};
}
} else {
statusCode = 404;
body = {
success: "false",
error: {
code: "error 2",
message: "customer not found!",
},
};
}
} catch (err) {
statusCode = 500;
body = {
success: "false",
error: {
code: "error 1",
message: err.message,
},
};
} finally {
body = JSON.stringify(body);
}
return {
statusCode,
headers,
body,
};
};
// Get Customer Date of Birth from a Dynamo DB
async function getCustomerDob(customer) {
const dynamo = new AWS.DynamoDB.DocumentClient();
let customer_dob;
customer_dob = await dynamo
.get({
TableName: "CUSTOMER_DOB",
Key: {
EMAIL: customer,
},
})
.promise();
return customer_dob.Item;
}
// Simple function that says if today date match a date of Birth (dob)
function isBirthdayToday(dob) {
if (
dob.DOB_M == new Date().getMonth() + 1 &&
dob.DOB_D == new Date().getDate()
)
return true;
else return false;
}
The above code does the following:
- Extracts a customer’s email from an event.
- Fetches the customer DOB from the database (note: if no customer is found, the function returns a status code
400
withsuccess: false
andmessage: customer not found
). - Compares the current date with customer DOB.
- If a match exists, the function returns both a status code
200
withsuccess: true
, and the promotion value in the format expected by Commerce Layer’s external promotion feature.
The entire function body is surrounded by try / catch / finally
to capture any system exceptions and return appropriate error codes (500
in our case for system exceptions).
Please note that the code shown above is for example’s sake. Undoubtedly, there are better ways to implement the promotion logic. Feel free to share your code if you like!
That said, let’s paste this code in the IDE and deploy:
We also need to grant the lambda function proper access to the DynamoDB, which we can do by using the AWS Identity and Access Management tool (IAM). From the configuration panel of the lambda, select Permissions, and click the Execution role that was created by default:
This opens the IAM for the selected role. Make sure that the AmazonDynamoDBReadOnlyAccess
is present in the Permission policies lists. If it isn’t, add it by clicking on Add Permissions and searching for it in the dropdown:
Make also sure to always hit the Deploy button in the AWS Lambda IDE, otherwise you won’t see the changes to your code.
Our lambda setup is complete, and now it’s time to move to the next step.
Setting up the API gateway
Now that our lambda is ready, we can expose it on the cloud by using the Amazon API Gateway service. There are different ways to set up a new API gateway, for this tutorial we’ll start from the AWS Lambda panel. As mentioned in the previous section, a Lambda function is triggered by an external event so you will need to click on Add trigger:
In the Add trigger page, select the API Gateway option for Trigger configuration:
Once you select the API Gateway option, some other configuration options will appear. Select Create an API in the API drop down, select REST API, and Open for the security options (please note that this is not the recommended way to configure a production service, but for the sake of this tutorial it’ll be ok).
All you need to do now is press Add and your API Gateway is set. You will see API Gateway in the triggers section of the Function overview panel:
Now, let’s take a closer look at what we created in the AWS API Gateway page. Go to the AWS home console and select API Gateway:
The screen below will appear. As you can see a new API has been deployed as a result of the previous steps:
Let’s open the BirthdayPromoTutorial-API to see how things are set up by clicking on the API name. From the panel below, we can see that a resource has been created for our endpoint (/BirthdayPromoTutorial
) and this resource will have all the HTTP methods behaving the same. This means it will forward the payload to our lambda function, as the handy generated diagram shows:
This API Gateway setup is called Proxy, meaning that it will transparently forward the payload to the consumer (our lambda). The lambda itself handles requests and responses, as we saw in the previous section. Now that our API is set up, we just need to deploy and test it. From the Actions dropdown, you can see in the API panel, select Deploy API:
Select "default" as you deployment stage. This is where you can manage different stages for you API such as, production, development and staging:
And, there you go — our API is deployed and ready to accept requests:
As indicated by the screen above, the base URL of our API gateway is:
https://16xy12aiwl.execute-api.eu-south-1.amazonaws.com/default
And the /BirthdayPromoTutorial
endpoint can be reached at:
https://16xy12aiwl.execute-api.eu-south-1.amazonaws.com/default/BirthdayPromoTutorial
Remember that when you are working inside your AWS instance, URLs are newly generated. This means that your actual URLs will be different from the examples here.
Let's do a do a quick smoke test using curl:
curl --location --request POST \
'https://16xy12aiwl.execute-api.eu-south-1.amazonaws.com/default/BirthdayPromoTutorial' \
--header 'Content-Type: application/json' \
--data-raw '{
"data": "just testing!"
}'
As you can see, our API is up and running! We receive an error response since we sent some "dummy" data:
{
"success": "false",
"error": {
"code": "error 1",
"message": "Cannot read property 'attributes' of undefined"
}
}
So, in the next step, we will configure Commerce Layer to send real data to our endpoint, and we’ll see the birthday promo in action.
6. Wiring everything together
The last step is connecting Commerce Layer to our newly created AWS Lambda environment.
We need to create and configure an external promotion. To do that we'll use the Commerce Layer CLI (the same result can be achieved also from our admin dashboard UI directly).
Our CLI helps you to manage your Commerce Layer applications, including promotions, right from the terminal. If you followed the onboarding tutorial as mentioned at the begininng of the article you should already have it installed. Otherwise, you can do it using you favorite package manager:
//npm
npm install -g @commercelayer/cli
//yarn
yarn global add @commercelayer/cli
Installing the CLI provides access to the commercelayer
command that can also be accessed with the cl
alias.
Follow this guide in the documentation to create an integration application and save the provided client ID, client secret, and base endpoint credentials. Then use those credentials to log into your application like so:
cl applications:login -o <organizationSlug> -i <clientId> -s <clientSecret> -a <applicationAlias>
The application alias can be named anything (e.g., "promotions-demo"). It will be associated with the application you want to log into on your terminal (this is useful when switching between multiple applications).
Now you can nstall the resources plugin using the command below:
cl plugins:install resources
To create an external promotion associated with a name, promotion URL, activation rules (the start time, expire time, and currency) attributes use the command below:
cl res:create external_promotions -a \
name="My Birthday Promotion" \
promotion_url="https://16xy12aiwl.execute-api.eu-south-1.amazonaws.com/default/BirthdayPromoTutorial" \
starts_at="2022-06-10T12:00:21.000Z" \
expires_at="2022-06-12T23:00:00.000Z" \
currency_code="EUR"
Once done successfully, your external promotion should be created with the object below:
{
id: 'ZJMmjfRgzJ',
type: 'external_promotions',
name: 'My Birthday Promotion',
currency_code: 'EUR',
starts_at: '2022-06-10T12:00:21.000Z',
expires_at: '2022-06-12T23:00:00.000Z',
total_usage_limit: null,
total_usage_count: 0,
active: false,
created_at: '2022-06-29T10:23:51.937Z',
updated_at: '2022-06-29T10:23:51.937Z',
reference: null,
reference_origin: null,
metadata: {},
promotion_url: 'https://16xy12aiwl.execute-api.eu-south-1.amazonaws.com/default/BirthdayPromoTutorial'
}
Let’s give the new setup a try. To do so, we will generate a checkout link using the checkout plugin of the CLI and we’ll then see the behavior of the promo in our hosted checkout application. To install the plugin use the command below:
cl plugins:install checkout
Now we have to login to our organisation as a registered customer so that we have an email that we can use to fetch customer birthday. To do that we'll use the applications login command:
commercelayer applications:login -o <organizationSlug> -i <clientId> -e <customerEmail> -p <customerPassword> -S <marketScope> -a <applicationAlias>
It's now time to generate a checkout link for an SKU. In our case, it will be TSHIRTMMFFFFFFE63E74MXXX
.
cl checkout -S TSHIRTMMFFFFFFE63E74MXXX
At this point you can copy/paste the generated URL in the browser, and you will see a nice checkout page thanks to Commerce Layer hosted checkout application. As we generated the checkout using an authenticated customer, the checkout will populate customer details automatically:
As you can see, there’s no promotion assigned to this order. That’s because we need to add our customer to the DynamoDB of our lambda. Let's do it in the DynamoDB console:
Now you can just refresh the checkout page to see a nice €15,00 birthday discount appear:
Conclusion
External promotions are a really powerful feature as they can bring promotions to a whole new level, allowing merchants and builders to have promotional logic that can based on external systems (such as CRMs) or even external promotion engines.
We believe that composable commerce is all about putting the power into the hands of the developer, enabling them to turn any experience they can dream up into a shoppable moment, like a birthday promotion calculated at runtime. For example, we just illustrated how a tool like AWS can work with Commerce Layer to deliver real benefits of the composable commerce architecture.
If you’ve created something similar with AWS Lambda and have a question about how to work with Commerce Layer, or just want to share it with us, please reach out! We would love to share your work with our community as well.