Sell everywhere with Commerce Layer Links.

Tutorials

How to build a Commerce Layer Slackbot with Node.js and Slack API.

April 4, 2023 Bolaji Ayodeji

A bot is a software application that runs automated tasks over the internet and can interact with users. Over time, bots have been used on different communication/social mediums (Discord, Slack, Twitter, etc.) to facilitate collaboration, moderate communities, enhance work, automate workflows, and lots more. A popular example in technology companies is Slack bots, which operate within the Slack messaging platform as Slack apps and can provide tons of automation workflows for users in a workspace. Slackbots can be programmed to perform various tasks, from simple commands to complex workflows.

We built a Slackbot to show the flexibility of the Commerce Layer API and developer tools. The bot uses our hosted-checkout library to provide order and return summaries and allow users to checkout pending orders directly from a Slack channel—reinforcing our idea of "checkout anywhere." The possibilities of checkout are limitless, and you can do anything you want with our API and hosted-checkout, as you will see shortly. In this tutorial, we will show you how we built the Slackbot integration using TypeScript, Nodejs, Slack's Bolt JavaScript library, and Slack API. Hopefully, you can use this tutorial as a guide to learn how to build your own Slackbot or how to install/use or set up a custom server for the Commerce Layer Bot.


Prerequisites

  • A general understanding of how APIs and the command-line work.
  • Some prior knowledge of the JavaScript programming language and Nodejs.
  • You have Nodejs, Nodemon, TypeScript, and Ngrok installed/correctly configured on your machine (kindly click on the link on each tool to learn how to install them).
  • You have a Slack account already.
  • You have a Commerce Layer account already.
  • You should have set up your organization and created the required commerce data resources for your market. You can follow the onboarding tutorial or manual configuration guide to achieve this.

Getting started

Creating a Slackbot that integrates with Commerce Layer can be useful in different ways. For example, if you want to provide support to a customer and you have the order ID, you can quickly get a summary right inside your team Slack workspace before considering heading to the Commerce Layer dashboard, or you could easily fetch a summary of the last placed order for some reason. You can also get sales progress reports of your store, amongst other amazing use cases 😃.

Slack provides an API that allows you to connect, simplify, and automate work with Slack apps. The API comes with tons of features and supporting tools. There are also the Slack SDKs (e.g., Slack Nodejs SDK) which have all been deprecated since May 2021 in favor of the new kid in the block—Bolt (which exists for JavaScript, Python, and Java). With this, you can still access all Slack APIs, Interactive Messages, OAuth, and Incoming Webhooks. We will talk more about this Bolt framework shortly.

Slack offers three popular APIs (Web API, Events API, RTM API) that allow you to build various apps with different requirements and use cases. In summary, the Web API is used to query information from and make changes to a Slack workspace, the Events API is used to build apps and bots that respond to activities in Slack, and the RTM (Real Time Messaging) API is used to receive events from Slack in real-time and send messages as users or bot users. If you’re confused about which to use for the app you have in mind, kindly refer to this official article from the Slack platform blog. In our case, we want to build a bot that will basically listen for messages in channels, and we will respond to that event; hence the Events API is our go-to.

The Events API utilizes event subscriptions (you subscribe to specific events) to send JSON payloads to your server when triggered (e.g. when a message or reaction is posted). Your server is responsible for receiving the payload and determining how to respond. Slack apps using this API will follow this event-driven sequence of actions:

  1. A user does something that triggers an event subscription (🧐 sending a message, clicking a button, using a slash command, etc.).
  2. Slack sends a JSON payload describing that event to your server (🚁📦).
  3. Your server acknowledges the receipt of the event (🧾).
  4. Your business logic determines the appropriate action for that event (👩🏼‍🍳).
  5. Your server carries out the determined action (🍔).
  6. Your server responds back to Slack (🚁🎁).

Before implementing the bot, we need to create a Slack application, configure the application, and fetch all the required credentials needed for development. In this section, we will highlight all the essential steps required to create and set up a new slack app from scratch.

Setting up the Slack app

Kindly follow the steps below to create a new slack app:

  1. Create a new slack workspace or ensure you belong to one. If you’re building a bot, you should create a new test workspace so you have all the flexibility to test things without disturbing others in your organization's workspace.
  2. Create a slack app using the “From scratch” or “From app manifest” option, which decides how to configure your application’s scopes and settings.
    1. With the “From scratch” option, you will use Slack’s configuration UI to manually add basic info, scopes, settings, and features to the app.
    2. With the “From app” option, you will use a manifest file (JSON or YAML) to manually add basic info, scopes, settings, and features to the app. This is a new feature, and it comes in very handy, for example, when you need to change your Ngrok request URLs in development across the app when you restart the Ngrok tunnel.
  3. Enter a name for your app and select a workspace to create the app into (don’t forget to use a test workspace), review the app details, and create the app.
  4. Update the app's basic display information (name, short description, long description, icon, and background color) in the provided fields.

Setting up Slack tokens

Slack apps use OAuth to manage access to Slack’s APIs. When an app is installed, you’ll need a token that the app can use to make calls to the API (similar to how it works with Commerce Layer API 😉). Basically, there are three main token types available to a Slack app: user (xoxp), bot (xoxb), and app (xapp) tokens. Kindly follow the steps below to create the required slack scopes and tokens:

  1. Navigate to the ”OAuth & Permissions” tab in the dashboard and create permission for the desired token type.
  2. Scroll to the ”Scopes > Bot Token Scopes” section and click the ”Add an OAuth Scope” button.
  3. For this integration, we added the following scopes (you can see all here):
    • commands (Add shortcuts and/or slash commands that people can use).
    • incoming-webhook (Create one-way webhooks to post messages to a specific channel).
    • channels:history (View messages and other content in public channels that your slack app has been added to).
    • chat:write (Post messages in approved channels and conversations).
    • chat:write.public (Send messages to channels @yourSlackApp isn't a member of).
    • groups:history (View messages and other content in private channels that your slack app has been added to)
    • im:history (View messages and other content in direct messages that your slack app has been added to).
    • im:write (Start direct messages with people).
    • mpim:history (View messages and other content in group direct messages that your slack app has been added to).
    • mpim:write (Start group direct messages with people).
    • users:read (View people in a workspace).
  4. Scroll back to the top of the page and click ”Install App to Workspace” to Install the app to your Slack workspace and generate the required tokens.
  5. Once you authorize the installation, you’ll be redirected to the ”OAuth & Permissions” page and see a ”Bot User OAuth Access Token” section.

Setting up the project

Now that we have a Slack app created and tokens generated, we can start up our TypeScript Nodejs server. You can explore the finished code on GitHub to get all the required files (including linting configurations), but for now, let’s create the most important files with this structure:

┌── src
		├── orders
				├── getOrders.ts
		├── returns
				├── getReturns.ts
		├── utils
				├── config.ts
				├── customError.ts
				├── getToken.ts
				├── parseDates.ts
├── .env
├── .app.ts
├── package.json
└── tsconfig.json

PS: You can generate a new package.json file using the npm init command.

Next, copy and paste the bot token generated earlier and the signing secret from the “Basic Information > App Credentials” tab in the dashboard (Slack signs incoming requests to your server using this secret) as environment variables in the .env file like so:

# Slack API
SLACK_BOT_TOKEN=xoxb-12345678-xyz
SLACK_SIGNING_SECRET=xyz36dxyzzzzz

Next, install the following required dependencies (Commerce Layer authentication library, Slack’s Bolt library for JavaScript, and Dotenv) with the command:

npm i @commercelayer/js-auth @slack/bolt dotenv

Next, add the following code to the app.ts file:

import * as dotenv from "dotenv";
dotenv.config();

import { App, LogLevel } from "@slack/bolt";

// Initialize the app with your bot token and signing secret
const app = new App({
	logLevel: LogLevel.DEBUG,
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET
});

// Start the app
(async () => {
  await app.start(process.env.PORT || 3000);

  console.log('⚡️ Bolt app is running!');
})();

Next, add the following to your tsconfig.json file:

{
  "compilerOptions": {
    "target": "es2022",
    "module": "node16",
    "lib": ["es2022"],
    "moduleResolution": "node16",
    "rootDir": ".",
    "outDir": "build",
    "allowSyntheticDefaultImports": true,
    "importHelpers": true,
    "alwaysStrict": true,
    "sourceMap": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitAny": false,
    "noImplicitThis": false,
    "strictNullChecks": false
  }
}

Next, add the following scripts to your package.json file:

{
	"scripts": {
    "dev": "ngrok http 3000",
    "start": "nodemon build/app.js",
    "build": "npx tsc -w",
    "test": ""
  },
}

Finally, run the command npm run build to automatically compile all TypeScript files into the /build folder with the same file structure. You can then start the Nodejs server using the command npm run start. Since we need our local server to receive a payload from Slack, we need to use a distributed reverse proxy web service to run the local server on the internet through an HTTP tunnel without having to deploy our code yet. Ngrok is a popular tool for this, and you should have installed it from the prerequisites section above. Now run npm run dev to start a Ngrok server on port 3000; this will generate a URL (like https://f09d-2a09-bac5.eu.ngrok.io) proxied to http://localhost:3000.

For the sake of this tutorial, "Slack bot" and "Slack app" mean the same thing, and we will use them interchangeably. Slack calls all integrations using their API "Slack apps." Not all apps are bots hence the different nomenclature. But a Slack bot is still a Slack app.

Setting up Slack events

Slack bots listen for events happening in a Slack workspace (like when a message is posted or when a reaction is posted to a message), and as mentioned earlier, we will use the Events API to subscribe to these events. There are two ways to do this, the default HTTP or Socket Mode. Socket Mode lets apps use the Events API and interactive components without exposing a public HTTP endpoint, while HTTP Mode does the opposite. The HTTP method is recommended for apps being deployed to hosting environments or apps intended for distribution in the Slack App Directory, which is our end goal; hence we would use this method.

When you use the Events API directly over HTTP, you designate a public HTTP endpoint that your app listens on, choose what events to subscribe to, and Slack sends the appropriate events to you. Kindly follow the steps below to create events:

  1. Navigate to the ”Events & Subscriptions” tab in the dashboard and toggle the “Enable Events” switch.
  2. Now enter the previously created Ngrok URL into the Request URL field, ending it with a /slack/events suffix (i.e., https://f09d-2a09-bac5.eu.ngrok.io/slack/events). Slack will then send HTTP POST requests based on triggered events to the provided Request URL endpoint.
  3. Next, configure the Slack events by selecting the specific events you want Slack to listen to. In the “Event Subscriptions” tab, scroll to the “Subscribe to bot events” section and click the “Add Bot User Event” button.
  4. For this integration, we added the following events:
    1. message.channels (Listen for messages in public channels that your app is added to).
    2. message.groups (Listens for messages in private channels that your app is added to).
    3. message.im (Listen for messages in your app’s DMs with users).
    4. message.mpim (Listen for messages in multi-person DMs that your app is added to).
    5. app_home_opened (Listen for when a user clicks into your App Home)

The Slack bot is now ready to listen to messages from Slack. Let’s do one last thing and then write the business logic for the Slack bot. We’d show you how to acknowledge requests from Slack, process the requests, and respond back with a nicely formatted message.

Setting up Slack commands

In Slack, commands enable users in a workspace to interact with Slack apps from within the Slack application (either on the web or with other clients). When a user enters a command and hits submit, this action will trigger an event and cause Slack to send a payload of data to the associated Slack app. The app will then respond in whatever way it wants based on the payload received. Kindly follow the steps below to create some commands:

  1. Navigate to the “Slash Commands” tab in the dashboard and click on the “Create New Command” button.
  2. Enter the command starting with a backslash character (e.g., /cl). Your command name must be shorter than 32 characters and must not contain spaces.
  3. Enter the Request URL we used previously (i.e., https://f09d-2a09-bac5.eu.ngrok.io/slack/events).
  4. Enter a short description of the command and some usage hints. These hints will form the parameters we will use later to write the business logic (e.g., if order comes after the /cl slash command, we should treat the next string after a space as the order ID).

To test this, quickly use the configured slash command (/cl test) in the workspace you have installed the Slack app into. Slack will send an HTTP POST to the Request URL with the following payload:

You can view the requests yourself at http://127.0.0.1:4040/inspect/http or see more details about the payload Slack sends in Slack’s documentation.

Setting up interactivity

To use features like modals, shortcuts, or interactive components (like input fields, buttons, select menus, etc.), you will need to enable interactivity. This allows your app respond back to Slack with a message when a request is triggered using the available interactive components. And when a user interacts with these components (e.g., clicking a button), Slack will send the request as an event, and you can respond back the same way you handle slash commands. Kindly follow the steps below to configure interactivity:

  1. Navigate to the “Interactivity & Shortcuts” tab in the dashboard and toggle the “Interactivity” switch.
  2. Enter the Request URL we used previously (i.e., https://f09d-2a09-bac5.eu.ngrok.io/slack/events) and hit save.
  3. All good!

Bot features

The bot commands trigger certain requests, and the app returns some information about the requested resource (including the resource and customer details) and links to view the full details of certain resources in the Commerce Layer dashboard. Below you will find all the existing and future features of the Commerce Layer Slackbot integration as of when this tutorial was published.

  • ☑️ Command to fetch the details of a particular order.
    • /cl order [order ID] (fetch an order by ID)
    • /cl orders:last and /cl orders:p last (fetch the last placed order)
    • /cl orders:a last (fetch the last approved order)
  • ☑️ Command to fetch the details of a particular return.
    • /cl return [return ID] (fetch a return by ID).
    • /cl returns:last and /cl returns:r last (fetch the last placed return)
    • /cl returns:a last (fetch the last approved return)
  • ☑️ Commands to fetch current total orders (count and revenue) per day.
    • /cl orders:today <currency code>
  • ☑️ Commands to fetch current returns (count) per day.
    • /cl returns:today
  • ☐ Automatic alerts for the total orders revenue at the end of the day/week/month.
  • ☐ Automatic alerts for the total number of returns at the end of the day/week/month.

Fetching data from Commerce Layer SDK

We will now use the official Commerce Layer JavaScript library, which makes it quick and easy to interact with Commerce Layer API. If you don’t have any experience with using our SDK, you should read this introductory article from the blog. You can install the SDK using the command npm install @commercelayer/sdk. Kindly follow the steps below to get the SDK working on this project:

  1. Create an Integration application type on the Commerce Layer dashboard for your organization (you can learn how to do this from the documentation). We will use this to generate an integration access token for the Slack bot.
  2. Create a Sales channel application type too. We will use this to generate a sales channel token for the hosted-checkout library.
  3. Copy the generated API credentials (client ID, client secret, base endpoint, and allowed scopes).
  4. Add the following environmental variables into the .env file inputting the valid API credentials.
# Commerce Layer API (Integration)
CL_ORGANIZATION_SLUG=cake-store
CL_ORGANIZATION_MODE=test
CL_CLIENT_ID=c0mMeRceLaYeRl0VEsyOUvLHxCmjklvyJQPYtU
CL_CLIENT_ID_CHECKOUT=bizmY8bbCK2IoQfF-c0mMeRceLaYeRl0VEsyOU
CL_CLIENT_SECRET=1DdUFqHuT212m8lRT4s7_Wv_aiwJd5s0qBcloj_jugds

All requests to Commerce Layer API must be authenticated with a token. Hence, before using the SDK, you need to get a valid access token. Add the code below in src/utils/config.ts to initialize the Commerce Layer SDK and generate an integration token for fetching resources from Commerce Layer API:

import * as dotenv from "dotenv";
dotenv.config();

import CommerceLayer from "@commercelayer/sdk";
import { getIntegrationToken } from "@commercelayer/js-auth";

export const initConfig = async () => {
  const BASE_ENDPOINT = `https://${process.env.CL_ORGANIZATION_SLUG}.commercelayer.io`;
  const CLIENT_ID = process.env.CL_CLIENT_ID;
  const CLIENT_ID_CHECKOUT = process.env.CL_CLIENT_ID_CHECKOUT;
  const CLIENT_SECRET = process.env.CL_CLIENT_SECRET;
  const organizationSlug = process.env.CL_ORGANIZATION_SLUG;
  const organizationMode = process.env.CL_ORGANIZATION_MODE;

  const { accessToken: token } = await getIntegrationToken({
    endpoint: BASE_ENDPOINT,
    clientId: CLIENT_ID,
    clientSecret: CLIENT_SECRET
  });

  const cl = CommerceLayer({
    organization: organizationSlug,
    accessToken: token
  });

  return {
    cl,
    BASE_ENDPOINT,
    CLIENT_ID,
    CLIENT_ID_CHECKOUT,
    CLIENT_SECRET,
    organizationSlug,
    organizationMode
  };
};

Next add the code below in src/utils/getToken.ts to generate a sales channel tokens for the hosted-checkout:

import * as dotenv from "dotenv";
dotenv.config();

import { getSalesChannelToken } from "@commercelayer/js-auth";

export const getCheckoutToken = async (config, marketNumber: number) => {
	const { BASE_ENDPOINT, CLIENT_ID, CLIENT_SECRET } = config;
  const auth = await getSalesChannelToken({
    endpoint: BASE_ENDPOINT,
    clientId: CLIENT_ID_CHECKOUT,
    scope: `market:${marketNumber}`
  });
  return auth.accessToken;
};

Now unto that main stuff! Add the code below to the src/orders/getOrders.ts file, which includes three exported functions (getOrderById(), getLastOrder(), and getTodaysOrder()) that fetches some data from Commerce Layer and returns an object.

import { getCheckoutToken } from "../utils/getToken";
import { generateDate } from "../utils/parseDate.js";

const getOrderById = async (id: string, config) => {
	const { cl, organizationSlug, organizationMode } = config;
  try {
    const orders = await cl.orders.retrieve(id, {
      include: ["customer", "market", "shipments", "shipping_address", "billing_address", "payment_method"]
    });

    const checkoutAccessToken = await getCheckoutToken(config, orders.market.number);

    return { orders, organizationSlug, organizationMode, checkoutAccessToken };
  } catch (error) {
    return { error };
  }
};

const getLastOrder = async (status: string, config) => {
	const { cl, organizationSlug, organizationMode } = config;
  try {
    const orders = (
      await cl.orders.list({
        include: ["customer", "market", "shipments", "shipping_address", "billing_address", "payment_method"],
        filters: { status_eq: `${status}` },
        sort: status === "placed" ? { placed_at: "desc" } : { approved_at: "desc" }
      })
    ).first();

    return { orders, organizationSlug, organizationMode };
  } catch (error) {
    return { error };
  }
};

const getTodaysOrder = async (currency: string, config) => {
	const { cl, organizationSlug } = config;
  const currencyName = currency.toUpperCase();

  const allOrders = await cl.orders.list({
    filters: {
      status_eq: "placed",
      placed_at_gteq: `${generateDate("today")}}`,
      placed_at_lt: `${generateDate("next")}}`
    }
  });
  const allOrdersCount = allOrders.meta.recordCount;

  const allOrdersByMarket = await cl.orders.list({
    filters: {
      currency_code_eq: `${currencyName}`,
      status_eq: "placed",
      placed_at_gteq: `${generateDate("today")}}`,
      placed_at_lt: `${generateDate("next")}}`
    }
  });
  const allOrdersByMarketCount = allOrdersByMarket.meta.recordCount;

  const revenue = allOrdersByMarket.reduce((acc, order) => {
    return acc + order.total_amount_cents;
  }, 0);

  let revenueCount;
  allOrdersByMarketCount !== 0
    ? (revenueCount = (revenue / 100).toLocaleString(
        `${allOrdersByMarket[0].language_code}-${allOrdersByMarket[0].country_code}`,
        {
          style: "currency",
          currency: `${allOrdersByMarket[0].currency_code}`
        }
      ))
    : (revenueCount = 0);

  return {
    allOrdersCount,
    allOrdersByMarketCount,
    revenueCount,
    currencyName,
    organizationSlug
  };
};

export { getOrderById, getLastOrder, getTodaysOrder };

You can find the code for the date formatter utility functions in this file on GitHub.

Using the Slack Bolt library

Now that we have everything set up, let’s write a few more code to acknowledge requests from Slack and respond back to them using the command() method. As described below, we will check if the submitted slash command starts with a particular parameter, then we will call another function from a different file that we will set up later and assign this function to either the getOrderResource or getReturnResource function. These modular functions will include the business logic that will receive some data objects and respond back to Slack. Kindly add the code below in addition to the existing app.ts file.

import { initConfig } from "./src/utils/config";

app.command("/cl", async ({ command, client, ack, say }) => {
  await ack();

	const config = await initConfig();

  if (command.text.startsWith("order ")) {
    const resourceType = getOrderById(command.text.replace("order ", ""), config);
    getOrderResource(resourceType, command, client, say);
  } else if (command.text === "orders:p last" || command.text === "orders:last") {
    const resourceType = getLastOrder("placed", config);
    getOrderResource(resourceType, command, client, say);
  } else if (command.text === "orders:a last") {
    const resourceType = getLastOrder("approved", config);
    getOrderResource(resourceType, command, client, say);
  }

  if (command.text.startsWith("return ")) {
    const resourceType = getReturnById(command.text.replace("return ", ""), config);
    getReturnResource(resourceType, command, client, say);
  } else if (command.text === "returns:r last" || command.text === "returns:last") {
    const resourceType = getLastReturn("requested", config);
    getReturnResource(resourceType, command, client, say);
  } else if (command.text === "returns:a last") {
    const resourceType = getLastReturn("approved", config);
    getReturnResource(resourceType, command, client, say);
  }

  if (command.text.startsWith("orders:today ")) {
    const resourceType = getTodaysOrder(command.text.replace("orders:today ", ""));
    countOrders(resourceType, command, client, say);
  }

  if (command.text.startsWith("returns:today")) {
    const resourceType = getTodaysReturn();
    countReturns(resourceType, command, client, say);
  }
});

The code snippet below shows what the getOrderResource function looks like. The client.users.info() function accepts an object to fetch information about a user, and the say() function accepts a string to post simple messages or JSON payloads for more complex messages. Please read the comments in the code to better understand what each part does.

const getOrderResource = async (resourceType, userInput, client, say) => {
  // Make a request to fetch the user information of the user that triggered the command.
  // We will use their profile image and display name in the response message.
  const triggerUser = await client.users.info({
    user: userInput.user_id
  });
  const resource = await resourceType;

  // If the functions in getOrders.ts returns undefined or an empty object, return an error message.
  // The custom error handler can be found here on GitHub here:
  // https://github.com/commercelayer/examples/tree/main/solutions/commercelayer-slackbot/src/utils/customError.ts
  if (!resource.orders) {
    await say({
      blocks: [
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text: `> :warning: Command ${"`"}${userInput.command} ${
              userInput.text
            }${"`"} failed with error: ${"```"}${JSON.stringify(customError("Order"), null, 2)}${"```"}`
          }
        }
      ],
      // Add a fallback plain text for notifications and accessibility.
      text: customError("Order")
    });
    // If the functions in getOrders.ts returns some data object, return an interactive message.
  } else {
    await say({
      blocks: [
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text: `:shopping_trolley: Order ${"`"}${resource.orders.id}${"`"} from the *${
              resource.orders.market.name
            }* market has a total amount of *${resource.orders.formatted_subtotal_amount}* and was ${
              resource.orders.placed_at !== null ? "placed" : "created"
            } on <!date^${formatTimestamp(
              resource.orders.placed_at !== null ? resource.orders.placed_at : resource.orders.created_at
            )}^{date_long} at {time}|${
              resource.orders.placed_at !== null ? resource.orders.placed_at : resource.orders.created_at
            }>. Here's a quick summary of the resource:`
          }
        },
        {
          type: "divider"
        },
        {
          type: "section",
          fields: [
            {
              type: "mrkdwn",
              text: `*Customer email:*\n${resource.orders.customer !== null ? resource.orders.customer.email : "null"}`
            },
            {
              type: "mrkdwn",
              text: `*Customer ID:*\n<https://dashboard.commercelayer.io/${
                resource.organizationMode === "live" ? "live" : "test"
              }/${resource.organizationSlug}/resources/customers/${
                resource.orders.customer !== null ? resource.orders.customer.id : "null"
              }|${resource.orders.customer !== null ? resource.orders.customer.id : "null"}>`
            }
          ]
        },
        {
          type: "section",
          fields: [
            {
              type: "mrkdwn",
              text: `*Order number:*\n${"`"}${resource.orders.number}${"`"}`
            },
            {
              type: "mrkdwn",
              text: `*Order status:*\n${"`"}${resource.orders.status}${"`"}`
            }
          ]
        },
        {
          type: "section",
          fields: [
            {
              type: "mrkdwn",
              text: `*Payment status:*\n${"`"}${resource.orders.payment_status}${"`"}`
            },
            {
              type: "mrkdwn",
              text: `*Fulfillment status:*\n${"`"}${resource.orders.fulfillment_status}${"`"}`
            }
          ]
        },
        {
          type: "section",
          fields: [
            {
              type: "mrkdwn",
              text: `*Shipping address:*\n${
                resource.orders.shipping_address !== null
                  ? resource.orders.shipping_address.full_name + ", " + resource.orders.shipping_address.full_address
                  : "null"
              }.`
            },
            {
              type: "mrkdwn",
              text: `*Billing address:*\n${
                resource.orders.billing_address !== null
                  ? resource.orders.billing_address.full_name + ", " + resource.orders.billing_address.full_address
                  : "null"
              }.`
            }
          ]
        },
        {
          type: "section",
          fields: [
            {
              type: "mrkdwn",
              text: `*Payment method:*\n${
                resource.orders.payment_method !== null ? resource.orders.payment_method.name : "null"
              }`
            },
            {
              type: "mrkdwn",
              text: `*Shipment number(s):*\n${
                resource.orders.shipments.length > 0
                  ? resource.orders.shipments.map((shipment) => {
                      if (resource.orders.shipments.length > 1) {
                        return `<https://dashboard.commercelayer.io/${
                          resource.organizationMode === "live" ? "live" : "test"
                        }/${resource.organizationSlug}/resources/shipments/${shipment.id}|${shipment.number}>, `;
                      } else {
                        return `<https://dashboard.commercelayer.io/${
                          resource.organizationMode === "live" ? "live" : "test"
                        }/${resource.organizationSlug}/resources/shipments/${shipment.id}|${shipment.number}>`;
                      }
                    })
                  : "null"
              }`
            }
          ]
        },
        {
          type: "actions",
          elements: [
            resource.orders.status === "pending"
              ? {
                  type: "button",
                  text: {
                    type: "plain_text",
                    text: "Checkout Order",
                    emoji: true
                  },
                  style: "primary",
                  value: "checkout_order",
                  url: `https://${resource.organizationSlug}.checkout.commercelayer.app/${resource.orders.id}?accessToken=${resource.checkoutAccessToken}`,
                  action_id: "check_order"
                }
              : {
                  type: "button",
                  text: {
                    type: "plain_text",
                    text: "View Order",
                    emoji: true
                  },
                  style: "primary",
                  value: "view_order",
                  url: `https://dashboard.commercelayer.io/${resource.organizationMode === "live" ? "live" : "test"}/${
                    resource.organizationSlug
                  }/resources/orders/${resource.orders.id}`,
                  action_id: "check_order"
                },
            {
              type: "button",
              text: {
                type: "plain_text",
                text: "View Customer",
                emoji: true
              },
              value: "view_customer",
              url: `https://dashboard.commercelayer.io/${resource.organizationMode === "live" ? "live" : "test"}/${
                resource.organizationSlug
              }/resources/customers/${resource.orders.customer !== null ? resource.orders.customer.id : "null"}`,
              action_id: "view_customer"
            }
          ]
        },
        {
          type: "divider"
        },
        {
          type: "context",
          elements: [
            {
              type: "image",
              image_url: `${triggerUser.user.profile.image_72}`,
              alt_text: `${triggerUser.user.profile.display_name || triggerUser.user.profile.real_name}'s avatar`
            },
            {
              type: "mrkdwn",
              text: `${
                triggerUser.user.profile.display_name || triggerUser.user.profile.real_name
              } has triggered this request.`
            }
          ]
        }
      ],
      // Add a fallback plain text for notifications and accessibility.
      text: `:shopping_trolley: Order ${"`"}${resource.orders.id}${"`"} from the *${
        resource.orders.market.name
      }* market has a total amount of *${resource.orders.formatted_subtotal_amount}* and was ${
        resource.orders.placed_at !== null ? "placed" : "created"
      } on <!date^${formatTimestamp(
        resource.orders.placed_at !== null ? resource.orders.placed_at : resource.orders.created_at
      )}^*{date_long}* at *{time}*|${
        resource.orders.placed_at !== null ? resource.orders.placed_at : resource.orders.created_at
      }>.`
    });
  }
};

PS: You can find the similar code for the getReturnResource function in this file on GitHub.


Here are a couple of things you should note:

  1. Slack provides a <!date> formatting command that formats text containing a date or time and displays them in the local timezone of the person seeing the text. All you need to do is use the syntax: <!date^timestamp^token_string^optional_link|fallback_text> and pass in the timestamp in Unix time format (we have a utility function in the GitHub repository that converts the datetime attribute from Commerce Layer to that format). Here’s an example: "<!date^1514808000^{date_long}|2018-01-01T12:00:00.000Z".
  2. Blocks are the building components of a Slack message and can range from text to images to interactive components (like buttons). If you observe, the code above includes some blocks: [] array with different types of content with text in markdown format. The say() method then contains an array of blocks, which are transmitted and processed by Slack.
  3. Slack's brilliant Block Kit Builder allows you to prototype interactive messages with all the interactive components. The builder lets you (or anyone on your team) mockup messages and generates the corresponding JSON you can paste directly into your app.
  4. Currently, all Slack interactive buttons dispatch a request (see this GitHub thread); hence you need to respond with 200 OK using the action() method and the action_id of each button element to avoid a warning icon error when the button is clicked. If the button isn’t a link button (maybe the button triggers some other kind of action), then you will use the same method to acknowledge the button request.
app.action("view_customer", ({ ack }) => ack());
app.action("check_order", ({ ack }) => ack());
app.action("view_return", ({ ack }) => ack());

Testing everything!

Now let’s test for the order resource. If you enter /cl order wJJKhAcvBO passing in an order ID, the returned response will look like this (the images below) for both light and dark themes.

Clicking the “Checkout Order” button will take you to a beautiful checkout page where you can complete the pending order. Isn’t this cool? Checkout anywhere powered by Commerce Layer!

As mentioned earlier, this Slackbot can be useful in different ways! Even though this should work for only authorized users (admin or staff), you can extend the bot to customers and have them checkout unfinished orders right from Slack (if you have a community for your ecommerce business). Now you can think of other use cases based on your existing workflows and needs and bring them to life!

The JSON payload sent back to Slack would look something like this:

{
    "token": "l8V3x7zxuHhHELLOXSrlpif3",
    "team_id": "T34NRBRSLK9",
    "context_team_id": "T34NRBRSLK9",
    "context_enterprise_id": null,
    "api_app_id": "A04NTNAWORL",
    "event": {
        "bot_id": "B04PXV400EU",
        "type": "message",
        "text": ":shopping_trolley: Order `wJZYhAmvBO` from the *USA* market has a total amount of *$102.00* and was created on <!date^1676918681^*{date_long}* at *{time}*|2023-02-20T18:44:41.309Z>.",
        "user": "U04NTNKLPPEK",
        "ts": "1679328956.227089",
        "app_id": "A04NTNAWORL",
        "blocks": [
            {
                "type": "section",
                "block_id": "U/UMb",
                "text": {
                    "type": "mrkdwn",
                    "text": ":shopping_trolley: Order `wJZYhAmvBO` from the *USA* market has a total amount of *$102.00* and was created on <!date^1676918681^{date_long} at {time}|2023-02-20T18:44:41.309Z>. Here's a quick summary of the resource:",
                    "verbatim": false
                }
            },
            {
                "type": "divider",
                "block_id": "GTpp"
            },
            {
                "type": "section",
                "block_id": "O6XY",
                "fields": [
                    {
                        "type": "mrkdwn",
                        "text": "*Customer email:*\n<mailto:aeafury@gmail.com|aeafury@gmail.com>",
                        "verbatim": false
                    },
                    {
                        "type": "mrkdwn",
                        "text": "*Customer ID:*\n<https://dashboard.commercelayer.io/test/cake-store/resources/customers/kNXNhbDNmB|kNXNhbDNmB>",
                        "verbatim": false
                    }
                ]
            },
            {
                "type": "section",
                "block_id": "gQX",
                "fields": [
                    {
                        "type": "mrkdwn",
                        "text": "*Order number:*\n`34085857`",
                        "verbatim": false
                    },
                    {
                        "type": "mrkdwn",
                        "text": "*Order status:*\n`pending`",
                        "verbatim": false
                    }
                ]
            },
            {
                "type": "section",
                "block_id": "lB6",
                "fields": [
                    {
                        "type": "mrkdwn",
                        "text": "*Payment status:*\n`unpaid`",
                        "verbatim": false
                    },
                    {
                        "type": "mrkdwn",
                        "text": "*Fulfillment status:*\n`unfulfilled`",
                        "verbatim": false
                    }
                ]
            },
            {
                "type": "section",
                "block_id": "n3Si",
                "fields": [
                    {
                        "type": "mrkdwn",
                        "text": "*Shipping address:*\nErdaulet Adikhanov , Zhetisay, Home Home, 160500 Turkistan Please select a region, state or province. (KZ) 87059829273.",
                        "verbatim": false
                    },
                    {
                        "type": "mrkdwn",
                        "text": "*Billing address:*\nErdaulet Adikhanov , Zhetisay, Home Home, 160500 Turkistan Please select a region, state or province. (KZ) 87059829273.",
                        "verbatim": false
                    }
                ]
            },
            {
                "type": "section",
                "block_id": "YnfYw",
                "fields": [
                    {
                        "type": "mrkdwn",
                        "text": "*Payment method:*\nnull",
                        "verbatim": false
                    },
                    {
                        "type": "mrkdwn",
                        "text": "*Shipment number(s):*\n<https://dashboard.commercelayer.io/test/cake-store/resources/shipments/YkEzCELVRJ|34085857/S/001>",
                        "verbatim": false
                    }
                ]
            },
            {
                "type": "actions",
                "block_id": "=mnr1",
                "elements": [
                    {
                        "type": "button",
                        "action_id": "check_order",
                        "text": {
                            "type": "plain_text",
                            "text": "Checkout Order",
                            "emoji": true
                        },
                        "style": "primary",
                        "value": "checkout_order",
                        "url": "https://cake-store.checkout.commercelayer.app/wJZYhAmvBO?accessToken=eyJhbGciOiJIUzUxMiJ9.eyJvcmdhbml6YXRpb24iOnsiaWQiOiJFbnBWT0ZyS3Z5Iiwic2x1ZyI6ImNha2Utc3RvcmUiLCJlbnRlcnByaXNlIjpmYWxzZX0sImFwcGxpY2F0aW9uIjp7ImlkIjoibHBKcW5pSlhkTSIsImtpbmQiOiJzYWxlc19jaGFubmVsIiwicHVibGljIjp0cnVlfSwidGVzdCI6dHJ1ZSwiZXhwIjoxNzEwODY0OTU1LCJtYXJrZXQiOnsiaWQiOlsiWWxxeEdoS3JRZyJdLCJwcmljZV9saXN0X2lkIjoid2xacnZDWXJLbCIsInN0b2NrX2xvY2F0aW9uX2lkcyI6WyJ6bkJqeHVsTFZuIiwiWmtZcVh1ZFZkayJdLCJnZW9jb2Rlcl9pZCI6bnVsbCwiYWxsb3dzX2V4dGVybmFsX3ByaWNlcyI6ZmFsc2V9LCJyYW5kIjowLjY1MzEyNjIwMzIyNTY5MjV9.7iU1rCXijtBQ8LCuluPYCXKKYjXUIGz_FEfbgOSyAJL5f9bILtclWg3uoUuE9tI5VKgj_teFiKBSZOVyMEFchA"
                    },
                    {
                        "type": "button",
                        "action_id": "view_customer",
                        "text": {
                            "type": "plain_text",
                            "text": "View Customer",
                            "emoji": true
                        },
                        "value": "view_customer",
                        "url": "https://dashboard.commercelayer.io/test/cake-store/resources/customers/kNXNhbDNmB"
                    }
                ]
            },
            {
                "type": "divider",
                "block_id": "GnGf"
            },
            {
                "type": "context",
                "block_id": "DXTjR",
                "elements": [
                    {
                        "type": "image",
                        "image_url": "https://secure.gravatar.com/avatar/551d7dcacd90143bd0cbe29ac68c212c.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2Fdf10d%2Fimg%2Favatars%2Fava_0024-72.png",
                        "alt_text": "bolaji-bot's avatar"
                    },
                    {
                        "type": "mrkdwn",
                        "text": "bolaji-bot has triggered this request.",
                        "verbatim": false
                    }
                ]
            }
        ],
        "team": "T04NTHRSKK9",
        "bot_profile": {
            "id": "B04PXV323EU",
            "deleted": false,
            "name": "Commerce Layer Bot",
            "updated": 1678213218,
            "app_id": "A04NTNAPX2B",
            "icons": {
                "image_36": "https://avatars.slack-edge.com/2023-02-12/4790294597874_713e6192e0862a34e839_36.png",
                "image_48": "https://avatars.slack-edge.com/2023-02-12/4790294597874_713e6192e0862a34e839_48.png",
                "image_72": "https://avatars.slack-edge.com/2023-02-12/4790294597874_713e6192e0862a34e839_72.png"
            },
            "team_id": "T34NRBRSLK9"
        },
        "channel": "C04P5617VFC",
        "event_ts": "1679328956.227089",
        "channel_type": "channel"
    },
    "type": "event_callback",
    "event_id": "Ev04UCFFK6KZ",
    "event_time": 1679328956,
    "authorizations": [
        {
            "enterprise_id": null,
            "team_id": "T34NRBRSLK9",
            "user_id": "U04NTNKLPPEK",
            "is_bot": true,
            "is_enterprise_install": false
        }
    ],
    "is_ext_shared_channel": false,
    "event_context": "4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDROVEhSU0tLOSIsImFpZCI6IkEwNE5UTkFQWDJCIiwiY2lkIjoiQzA0UDg0MTdWRkMifQ"
}

Conclusion

And that’s it! We hope you found this tutorial helpful and that you feel confident about building any Slack bot. So far, we have been able to implement a Slack app and write all the business logic we need in development. The next phase is to prepare the application for distribution by adding more features that will allow users to install the Slack app into their Slack workspace from the Slack App Directory or a unique installation link. In the next tutorial, we will show you how to set up Slack OAuth installation, store Slack and the app user's data in a database using Supabase, and deploy the entire application. We will also make the Commerce Layer Slackbot open for test public downloads then. Brace up! There’s more fun coming up soon :).

If you need it, the final manifest file with all the configured scopes can be found in this GitHub repository. You can use it to debug your own app or replicate the same configuration options we used for the Commerce Layer Bot. Cheers!