Buy Online Pickup In Store: Bonjour Meal - Part 2 - Building a Shopping Cart

1. Introduction

53003251caaf2be5.png 8826bd8cb0c0f1c7.png

Last Updated: 2020-10-30

Building a Shopping Cart on Business Messages!

This is the second codelab in a series aimed at building a Buy Online Pickup In Store user journey. In many e-commerce journeys, a shopping cart is key to the success of converting users into paying customers. The shopping cart also is a way to understand your customers better and a way to offer suggestions on other items that they may be interested in. In this codelab, we'll focus on building the shopping cart experience and deploying the application to Google App Engine.

What makes a good shopping cart?

Shopping carts are key to a successful online shopping experience. As it turns out, Business Messages is not only good at facilitating Q&A on a product with a prospective customer, but it can facilitate the entire shopping experience through to completing a payment within the conversation.

9d17537b980d0e62.png

Beyond a good shopping cart, a good shopping experience allows users to browse items by category and lets the business recommend other products the buyer might be interested in. After adding more items to the shopping cart, the user can review their entire cart, and be able to remove items or add more items before checking out.

What you'll build

In this section of the codelab series, you're going to extend the digital agent you built in part 1 for the fictitious company, Bonjour Meal, so users can browse a catalogue of items and add items to a shopping cart.

In this codelab, your app will

  • Show a catalog of questions within Business Messages
  • Suggest items that users may be interested in
  • Review contents of the shopping cart and create a total price summary

ab2fb6a4ed33a129.png

What you'll learn

  • How to deploy a web application on App Engine on Google Cloud Platform
  • How to use a persistent storage mechanism to save the state of a shopping cart

This codelab is focused on extending the digital agent from part 1 of this codelab series.

What you'll need

  • A GCP project that has been registered and approved for use with Business Messages
  • Check out our developer site for instructions on how
  • A service account JSON credentials file generated for your GCP Project
  • An Android 5+ device OR an iOS device with the Google Maps app
  • Experience with web application programming
  • An Internet connection!

2. Getting set up

This codelab assumes you've created your first agent and completed part 1 of the codelab. As such, we won't be going over the basics of enabling the Business Messages and Business Communications APIs, creating service account keys, deploying an application, or setting up your webhook on the Business Communications Console. With that said, we will clone a sample application to make sure your application is consistent with what we're building on top of, and we'll enable the API for Datastore on Google Cloud Platform to be able to persist data pertaining to the shopping cart.

Cloning the application from GitHub

In a terminal, clone the Django Echo Bot Sample to your project's working directory with the following command:

$ git clone https://github.com/google-business-communications/bm-bonjour-meal-django-starter-code

Copy your JSON credentials file created for the service account into the sample's resources folder and rename the credentials to "bm-agent-service-account-credentials.json".

bm-bonjour-meal-django-starter-code/bonjourmeal-codelab/step-2/resources/bm-agent-service-account-credentials.json

Enable the Google Datastore API

In Part 1 of this codelab, you enabled the Business Messages API, Business communications API, and Cloud Build API.

For this codelab, since we'll be working with Google Datastore, we also need to enable this API:

  1. Open the Google Datastore API in the Google Cloud Console.
  2. Ensure you are working with the correct GCP project.
  3. Click Enable.

Deploying the sample application

In a terminal, navigate to the sample's step-2 directory.

Run the following commands in a terminal to deploy the sample:

$ gcloud config set project PROJECT_ID*
$ gcloud app deploy
  • PROJECT_ID is the project ID for the project you used to register with the APIs.

Note the URL of the deployed application in the output of the last command:

Deployed service [default] to [https://PROJECT_ID.appspot.com]

The code that you just deployed contains a web application with a webhook to receive messages from Business Messages. It contains everything we've done from part 1 of the codelab. If you haven't already done so, please configure your webhook.

The application will respond to some simple inquiries like a user asking about the business hours of Bonjour Meal. You should test this on your mobile device through the test URLs you can retrieve from the Agent Information within the Business Communications Console. The test URLs will launch the Business Messages experience on your mobile device and you can begin interacting with your agent there.

3. The product catalog

An inventory system

In most cases, you can integrate directly with a brand's inventory through an internal API. In other instances, you might scrape a web page or build your own inventory tracking system. Our focus isn't to build an inventory system; we'll use a simple static file that contains images and product information for our agent. In this section, we'll pull information from this static file, surface that information into the conversation, and allow a user to browse the items available to be added to a shopping cart.

The static inventory file looks like this:

bonjourmeal-codelab/step-2/resources/inventory.json

{

    "food": [
        {
            "id":0,
            "name": "Ham and cheese sandwich",
            "price": "6.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/ham-and-cheese.png",
            "remaining": 8
        },
        {
            "id":1,
            "name": "Chicken veggie wrap",
            "price": "9.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/chicken-veggie-wrap.png",
            "remaining": 2
        },
        {
            "id":2,
            "name": "Assorted cheese plate",
            "price": "7.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/assorted-cheese-plate.png",
            "remaining": 6
        },
        {
            "id":3,
            "name": "Apple walnut salad",
            "price": "12.99",
            "image_url": "https://storage.googleapis.com/bonjour-rail.appspot.com/apple-walnut-salad.png",
            "remaining": 1
        }
    ]
}

Let's get the Python application to read this file in!

Reading from our inventory

The inventory is a static file called "inventory.json" found in the ./resources directory. We need to add some Python logic to views.py to read the contents of the JSON file and then surface it to the conversation. Let's create a function that reads in data from the JSON file and returns the list of products available.

This function definition can be placed anywhere in views.py.

bonjourmeal-codelab/step-2/bopis/views.py

...
def get_inventory_data():
        f = open(INVENTORY_FILE)
        inventory = json.load(f)
        return inventory
...

This should give us what we need to read the data from the inventory. Now we need a way to surface this product information into the conversation.

Surfacing the product catalog

For simplicity in this codelab, we have a general product catalog to surface all inventory items to the Business Messages conversation through a single rich card carousel.

To view the product catalog, we are going to create a suggested reply that has text "Show Menu" and postbackData "show-product-catalog". When users tap on the suggested reply and your web application receives the postback data, we'll send the rich card carousel. Let's add a new constant for this suggested reply at the top of views.py.

bonjourmeal-codelab/step-2/bopis/views.py

...
CMD_SHOW_PRODUCT_CATALOG = 'show-product-catalog'
...

From here, we parse the message and route it to a new function that sends a rich card carousel containing the product catalog. First extend the route_message function to call a function "send_product_catalog" when the suggested reply is tapped, and then we'll define the function.

In the following snippet, add an additional condition to the if statement in the route_message function to check if normalized_message equals the constant we defined earlier, CMD_SHOW_PRODUCT_CATALOG.

bonjourmeal-codelab/step-2/bopis/views.py

...
def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATALOG:
        send_product_catalog(conversation_id)
    else:
        echo_message(message, conversation_id)
...

And let's make sure to complete the flow and define send_product_catalog. send_product_catalog calls get_menu_carousel, which generates the carousel of rich cards from the inventory file we read in earlier.

The function definitions can be placed anywhere in views.py. Note that the following snippet makes use of two new constants that should be added to the top of the file.

bonjourmeal-codelab/step-2/bopis/views.py

...

CMD_ADD_ITEM = 'add-item'
CMD_SHOW_CART = 'show-cart'

...

def get_menu_carousel():
    """Creates a sample carousel rich card.

    Returns:
       A :obj: A BusinessMessagesCarouselCard object with three cards.
    """

    inventory = get_inventory_data()

    card_content = []

    for item in inventory['food']:
        card_content.append(BusinessMessagesCardContent(
            title=item['name'],
            description=item['price'],
            suggestions=[
                BusinessMessagesSuggestion(
                    reply=BusinessMessagesSuggestedReply(
                        text='Add item',
                        postbackData='{'+f'"action":"{CMD_ADD_ITEM}","item_name":"{item["id"]}"'+'}'))

                ],
            media=BusinessMessagesMedia(
                height=BusinessMessagesMedia.HeightValueValuesEnum.MEDIUM,
                contentInfo=BusinessMessagesContentInfo(
                    fileUrl=item['image_url'],
                    forceRefresh=False))))

    return BusinessMessagesCarouselCard(
        cardContents=card_content,
        cardWidth=BusinessMessagesCarouselCard.CardWidthValueValuesEnum.MEDIUM)

def send_product_catalog(conversation_id):
    """Sends the product catalog to the conversation_id.

    Args:
        conversation_id (str): The unique id for this user and agent.
    """
    rich_card = BusinessMessagesRichCard(carouselCard=get_menu_carousel())

    fallback_text = ''

    # Construct a fallback text for devices that do not support carousels
    for card_content in rich_card.carouselCard.cardContents:
        fallback_text += (card_content.title + '\n\n' + card_content.description
                          + '\n\n' + card_content.media.contentInfo.fileUrl
                          + '\n---------------------------------------------\n\n')

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        richCard=rich_card,
        fallback=fallback_text,
        suggestions=[
        BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='See my cart',
                postbackData=CMD_SHOW_CART)
            ),
        BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='See the menu',
                postbackData=CMD_SHOW_PRODUCT_CATALOG)
            ),
        ]
        )

    send_message(message_obj, conversation_id)
...

If you examine the creation of the carousel items, we also create an instance of the BusinessMessagesSuggestion class. Each suggestion represents a user selection for a product in the carousel. When a user taps on the suggested reply, Business Messages will send the postbackData that contains JSON describing the item and the action the user wants to take (add or remove from cart) to your webhook. In the following section, we'll parse messages that look like this to be able to actually add the item to the cart.

Now that we've made these changes, let's deploy the web application to Google App Engine and try out the experience!

$ gcloud app deploy

When you have the conversational surface loaded on your mobile device, send the message "show-product-catalog" and you should see a carousel of products that looks like this.

4639da46bcc5230c.png

If you tap on Add item, nothing actually happens except for the agent echoes the postback data from the suggested reply. In the next section, we'll make use of the product catalog and use it to build out the shopping cart where the item will be added.

The product catalog you just built can be extended in a variety of ways. You might have different drink menu options, or vegetarian options. Using carousels or suggestion chips are a great way to let users drill down through menu options to arrive at a set of products they are looking for. As an extension to this codelab, try extending the product catalog system so that a user can view drinks separately from food in the menu, or even be able to specify vegetarian options.

4. The shopping cart

In this section of the codelab, we'll build out the shopping cart functionality building off of the previous section which allows us to browse the products available.

Among many things, the key shopping cart experience allows users to add items to the cart, remove items from the cart, keep track of the number of each item in the cart, and review items in the cart.

Keeping track of the state of the shopping cart means we need to persist data on the web application. For simplicity of experimentation and deployment, we'll use Google Datastore in Google Cloud Platform to persist data. The Conversation ID remains constant between a user and the business, so we can use this to associate users with shopping cart items.

Let's start out by connecting with Google Datastore and persisting the conversation ID when we see it.

Connecting with Datastore

We'll connect with Google Datastore whenever any interaction is executed against the shopping cart, for example, when a user is adding or deleting an item. You can learn more about using this client library to interact with Google Datastore at the official documentation.

The following snippet defines a function to update the shopping cart. The function takes the following input: conversation_id and message. message contains JSON describing the action the user wants to take, which is already built into your carousel displaying the product catalog. The function creates a Google Datastore client and immediately fetches a ShoppingCart entity, where the key is the conversation ID.

Copy the following function to your views.py file. We'll continue to expand on it in the upcoming section.

bonjourmeal-codelab/step-2/bopis/views.py

from google.oauth2 import service_account
from google.cloud import datastore

def update_shopping_cart(conversation_id, message):
        credentials = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_LOCATION)

        client = datastore.Client(credentials=credentials)
        key = client.key('ShoppingCart', conversation_id)
        entity = datastore.Entity(key=key)
        result = client.get(key)
        
        # TODO: Add logic to add and remove items from cart
        
        entity.update(result)
        client.put(entity)

Let's extend this function to add an item to the cart.

Adding items to the cart

When the user taps an Add item suggested action from the product carousel, the postbackData contains JSON describing the action the user wants to take. The JSON dictionary has two keys, "action" and "item_name" and this JSON dictionary is sent to your webhook. The "item_name" field is the unique identifier that is associated with the item in the inventory.json.

Once we have the cart command and the cart item parsed from the message, we can then write conditional statements to add the item. Some edge cases to think about here are if the Datastore has never seen the conversation ID or if the shopping cart is receiving this item for the first time. The following is an extension of the update_shopping_cart functionality defined above. This change adds an item to the shopping cart which is persisted by Google Datastore.

The following snippet is an extension of the previous function added to your views.py. Feel free to add the difference, or to copy the snippet and replace the existing version of the update_shopping_cart function.

bonjourmeal-codelab/step-2bopis/views.py

def update_shopping_cart(conversation_id, message):
    credentials = service_account.Credentials.from_service_account_file(
      SERVICE_ACCOUNT_LOCATION)
    inventory = get_inventory_data()

    cart_request = json.loads(message)
    cart_cmd = cart_request["action"]
    cart_item = cart_request["item_name"]

    item_name = inventory['food'][int(cart_item)]['name']

    client = datastore.Client(credentials=credentials)
    key = client.key('ShoppingCart', conversation_id)
    entity = datastore.Entity(key=key)
    result = client.get(key)

    if result is None:
        if cart_cmd == CMD_ADD_ITEM:
            entity.update({
                item_name: 1
            })

    else:
        if cart_cmd == CMD_ADD_ITEM:
            if result.get(item_name) is None:
                result[item_name] = 1
            else:
                result[item_name] = result[item_name] + 1

        entity.update(result)
    client.put(entity)

This function will be extended later to support the scenario where cart_cmd contains the string ‘del-item' defined in CMD_DEL_ITEM.

Tying it together

Make sure you add the plumbing in the route_message function so that if you receive a message to add an item to the cart that the update_shopping_cart function is called. You will also need to define a constant for adding items using the convention we use throughout the codelab.

bonjourmeal-codelab/step-2bopis/views.py

...

CMD_DEL_ITEM = 'del-item'

...

def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATEGORY:
        send_product_catalog(conversation_id)
    elif CMD_ADD_ITEM in normalized_message or CMD_DEL_ITEM in normalized_message:
       update_shopping_cart(conversation_id, message)
    else:
        echo_message(message, conversation_id)

...

For now, we have the ability to add items to the shopping cart. If you deploy your changes to Google App Engine, you should be able to see the shopping cart changes reflected in the Google Datastore dashboard found in the GCP console. See the screenshot below of the Google Datastore console, there is a single entity which is named after the Conversation ID followed by some relationships to inventory items and quantity of those items that are in the shopping cart.

619dc18a8136ea69.png

In the next section, we'll create a way to list items in the shopping cart. The shopping cart review mechanism should show us all items in the cart, the quantity of those items, and an option to remove an item from the cart.

Reviewing items in the cart

Listing out the items in the shopping cart is the only way we can understand the state of the shopping cart and know which items we can remove.

Let's first send a friendly message like "Here's your shopping cart:", followed by another message that contains a rich card carousel with associated suggested replies for "Remove one" or "Add one". The rich card carousel should additionally list the quantity of items saved in the cart.

Something to be aware of before we actually go in and write our function: if there's only one type of item in the shopping cart, we can't render it as a carousel. Rich card carousels must contain at least two cards. On the flip side, if there are no items in the cart, we want to surface a simple message that says the cart is empty.

With that in mind, let's define a function called send_shopping_cart. This function connects with Google Datastore and requests a ShoppingCart entity based on the Conversation ID. Once we have that, we'll call the get_inventory_data function and use a rich card carousel to report the state of the shopping cart. We will also need to get the ID of a product by name and we can declare a function to look into Google Datastore to determine that value. As the carousel is being produced, we can associate suggested replies to delete items or add items by product ID. The snippet below performs all these operations. Copy the code anywhere into views.py.

bonjourmeal-codelab/step-2/bopis/views.py

...
def get_id_by_product_name(product_name):
  inventory = get_inventory_data()
  for item in inventory['food']:
    if item['name'] == product_name:
      return int(item['id'])
  return False


def send_shopping_cart(conversation_id):
  credentials = service_account.Credentials.from_service_account_file(
      SERVICE_ACCOUNT_LOCATION)

  # Retrieve the inventory data
  inventory = get_inventory_data()

  # Pull the data from Google Datastore
  client = datastore.Client(credentials=credentials)
  key = client.key('ShoppingCart', conversation_id)
  result = client.get(key)

  shopping_cart_suggestions = [
      BusinessMessagesSuggestion(
          reply=BusinessMessagesSuggestedReply(
              text='See total price', postbackData='show-cart-price')),
      BusinessMessagesSuggestion(
          reply=BusinessMessagesSuggestedReply(
              text='Empty the cart', postbackData='empty-cart')),
      BusinessMessagesSuggestion(
          reply=BusinessMessagesSuggestedReply(
              text='See the menu', postbackData=CMD_SHOW_PRODUCT_CATALOG)),
  ]

  if result is None or len(result.items()) == 0:
    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        text='There are no items in your shopping cart.',
        suggestions=shopping_cart_suggestions)

    send_message(message_obj, conversation_id)
  elif len(result.items()) == 1:

    for product_name, quantity in result.items():
      product_id = get_id_by_product_name(product_name)

      fallback_text = ('You have one type of item in the shopping cart')

      rich_card = BusinessMessagesRichCard(
          standaloneCard=BusinessMessagesStandaloneCard(
              cardContent=BusinessMessagesCardContent(
                  title=product_name,
                  description=f'{quantity} in cart.',
                  suggestions=[
                      BusinessMessagesSuggestion(
                          reply=BusinessMessagesSuggestedReply(
                              text='Remove one',
                              postbackData='{'+f'"action":"{CMD_DEL_ITEM}","item_name":"{product_id}"'+'}'))
                  ],
                  media=BusinessMessagesMedia(
                      height=BusinessMessagesMedia.HeightValueValuesEnum.MEDIUM,
                      contentInfo=BusinessMessagesContentInfo(
                          fileUrl=inventory['food'][product_id]
                          ['image_url'],
                          forceRefresh=False)))))

      message_obj = BusinessMessagesMessage(
          messageId=str(uuid.uuid4().int),
          representative=BOT_REPRESENTATIVE,
          richCard=rich_card,
          suggestions=shopping_cart_suggestions,
          fallback=fallback_text)

      send_message(message_obj, conversation_id)
  else:
    cart_carousel_items = []

    # Iterate through the cart and generate a carousel of items
    for product_name, quantity in result.items():
      product_id = get_id_by_product_name(product_name)

      cart_carousel_items.append(
          BusinessMessagesCardContent(
              title=product_name,
              description=f'{quantity} in cart.',
              suggestions=[
                  BusinessMessagesSuggestion(
                      reply=BusinessMessagesSuggestedReply(
                          text='Remove one',
                          postbackData='{'+f'"action":"{CMD_DEL_ITEM}","item_name":"{product_id}"'+'}'))
              ],
              media=BusinessMessagesMedia(
                  height=BusinessMessagesMedia.HeightValueValuesEnum.MEDIUM,
                  contentInfo=BusinessMessagesContentInfo(
                      fileUrl=inventory['food'][product_id]
                      ['image_url'],
                      forceRefresh=False))))

    rich_card = BusinessMessagesRichCard(
        carouselCard=BusinessMessagesCarouselCard(
            cardContents=cart_carousel_items,
            cardWidth=BusinessMessagesCarouselCard.CardWidthValueValuesEnum
            .MEDIUM))

    fallback_text = ''

    # Construct a fallback text for devices that do not support carousels
    for card_content in rich_card.carouselCard.cardContents:
      fallback_text += (
          card_content.title + '\n\n' + card_content.description + '\n\n' +
          card_content.media.contentInfo.fileUrl +
          '\n---------------------------------------------\n\n')

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        richCard=rich_card,
        suggestions=shopping_cart_suggestions,
        fallback=fallback_text,
    )

    send_message(message_obj, conversation_id)

...

Ensure you have already defined CMD_SHOW_CART at the top of views.py and call send_shopping_cart if the user sends a message containing ‘show-cart'.

bonjourmeal-codelab/step-2/bopis/views.py

...
def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATEGORY:
        send_product_catalog(conversation_id)
    elif CMD_ADD_ITEM in normalized_message or CMD_DEL_ITEM in normalized_message:
        update_shopping_cart(conversation_id, message)
    elif normalized_message == CMD_SHOW_CART:
        send_shopping_cart(conversation_id)
    else:
        echo_message(message, conversation_id)
...

34801776a97056ac.png

Based on the logic we introduced in the send_shopping_cart function, when you type in ‘show-cart', we'll either get a message stating there's nothing in the cart, a rich card showing the one item in the cart, or a carousel of cards showing multiple items. Additionally, we have three suggested replies: "See total price", "Empty the cart" and "See the menu".

Try deploying the above code changes to test that your shopping cart is tracking items that you add and that you can review the cart from the conversations surface as shown in the screenshots above. You can deploy the changes with this command run from thep step-2 directory where you are adding your changes.

$ gcloud app deploy

We'll build the "See total price" feature in the next section after building the functionality to remove an item from the cart. The function get_cart_price will behave similarly to the "See shopping cart" feature in the sense that it will cross-reference data in Datastore with the inventory.json file to produce a total price for the shopping cart. This will be handy for the next part of the codelab where we integrate with payments.

Removing items from the cart

Finally, we can complete the shopping cart behavior by introducing functionality to remove the cart. Replace the existing update_shopping_cart function with the following snippet.

bonjourmeal-codelab/step-2/ bopis/views.py

def update_shopping_cart(conversation_id, message):
    credentials = service_account.Credentials.from_service_account_file(
      SERVICE_ACCOUNT_LOCATION)
    inventory = get_inventory_data()

    cart_request = json.loads(message)
    cart_cmd = cart_request["action"]
    cart_item = cart_request["item_name"]

    item_name = inventory['food'][int(cart_item)]['name']


    client = datastore.Client(credentials=credentials)
    key = client.key('ShoppingCart', conversation_id)
    entity = datastore.Entity(key=key)
    result = client.get(key)

    if result is None:
        if cart_cmd == CMD_ADD_ITEM:
            entity.update({
                item_name: 1
            })
        elif cart_cmd == CMD_DEL_ITEM:
            # The user is trying to delete an item from an empty cart. Pass and skip
            pass

    else:
        if cart_cmd == CMD_ADD_ITEM:
            if result.get(item_name) is None:
                result[item_name] = 1
            else:
                result[item_name] = result[item_name] + 1

        elif cart_cmd == CMD_DEL_ITEM:
            if result.get(item_name) is None:
                # The user is trying to remove an item that's no in the shopping cart. Pass and skip
                pass
            elif result[item_name] - 1 > 0:
                result[item_name] = result[item_name] - 1
            else:
                del result[item_name]

        entity.update(result)
    client.put(entity)

Sending a confirmation message

When the user adds an item to the cart, you should send a confirmation message acknowledging their action and that you've processed their request. Not only does this help set expectations, but it also keeps the conversation going.

Let's extend the update_shopping_cart function so that it sends a message to the conversation ID stating that the item has been added or removed and gives suggestions to review their shopping cart or to see the menu again.

bonjourmeal-codelab/step-2/bopis/views.py

def update_shopping_cart(conversation_id, message):

     # No changes to the function, except appending the following logic
     ...
   
    if cart_cmd == CMD_ADD_ITEM:
        message = 'Great! You\'ve added an item to the cart.'
    else:
        message = 'You\'ve removed an item from the cart.'

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        text=message,
        suggestions=[
            BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='Review shopping cart',
                postbackData=CMD_SHOW_CART)
            ),
            BusinessMessagesSuggestion(
            reply=BusinessMessagesSuggestedReply(
                text='See menu again',
                postbackData=CMD_SHOW_PRODUCT_CATALOG)
            ),
            ])
    send_message(message_obj, conversation_id)

905a1f3d89893ba0.png

That should do it! A full-featured shopping cart experience that enables a user to add items, remove items, and review items in the cart.

At this point, if you'd like to see the shopping cart functionality in the Business Messages conversation, deploy the application in order to interact with your agent. You can do so by running this command in the step-2 directory.

$ gcloud app deploy

5. Preparing for payments

In preparation for integrating with a payment processor in the next part of the series, we need a way to get the price of the shopping cart. Let's build a function that retrieves the price for us by cross referencing the shopping cart data in Google Datastore, retrieving the price of each item from the inventory, and multiplying the price by the quantity of each item in the cart.

bonjourmeal-codelab/step-2/bopis/views.py

...
def get_cart_price(conversation_id):
    # Pull the data from Google Datastore
    credentials = service_account.Credentials.from_service_account_file(
    SERVICE_ACCOUNT_LOCATION)
    client = datastore.Client(credentials=credentials)
    key = client.key('ShoppingCart', conversation_id)
    entity = datastore.Entity(key=key)
    result = client.get(key)

    # Retrieve the inventory data
    inventory = get_inventory_data()
   
    # Start off with a total of 0 before adding up the total
    total_price = 0

    if len(result.items()) != 0:
      for product_name, quantity in result.items():
        total_price = total_price + float(
            inventory['food'][get_id_by_product_name(product_name)]['price']) * int(quantity)

    return total_price

...

And finally, we can consume that function and send a message to the user.

bonjourmeal-codelab/step-2/bopis/views.py

...

def send_shopping_cart_total_price(conversation_id):
    cart_price = get_cart_price(conversation_id)

    message_obj = BusinessMessagesMessage(
        messageId=str(uuid.uuid4().int),
        representative=BOT_REPRESENTATIVE,
        suggestions=[],
        text=f'Your cart\'s total price is ${cart_price}.')

    send_message(message_obj, conversation_id)
...

To tie it all together, let's update the route_message function and the constant to trigger the above logic.

bonjourmeal-codelab/step-2/bopis/views.py

...
CMD_GET_CART_PRICE = 'show-cart-price'
...
def route_message(message, conversation_id):
    '''
    Routes the message received from the user to create a response.

    Args:
        message (str): The message text received from the user.
        conversation_id (str): The unique id for this user and agent.
    '''
    normalized_message = message.lower()

    if normalized_message == CMD_RICH_CARD:
        send_rich_card(conversation_id)
    elif normalized_message == CMD_CAROUSEL_CARD:
        send_carousel(conversation_id)
    elif normalized_message == CMD_SUGGESTIONS:
        send_message_with_suggestions(conversation_id)
    elif normalized_message == CMD_BUSINESS_HOURS_INQUIRY:
        send_message_with_business_hours(conversation_id)
    elif normalized_message == CMD_ONLINE_SHOPPING_INQUIRY:
        send_online_shopping_info_message(conversation_id)
    elif normalized_message == CMD_SHOW_PRODUCT_CATEGORY:
        send_product_catalog(conversation_id)
    elif CMD_ADD_ITEM in normalized_message or CMD_DEL_ITEM in normalized_message:
        update_shopping_cart(conversation_id, message)
    elif normalized_message == CMD_SHOW_CART:
        send_shopping_cart(conversation_id)
    elif normalized_message == CMD_GET_CART_PRICE:
        send_shopping_cart_total_price(conversation_id)
    else:
        echo_message(message, conversation_id)
...

Here are some screenshots to showcase what the above logic achieves:

8feacf94ed0ac6c4.png

When we're ready to integrate with the payment processor in the next part of the codelab, we'll call the get_cart_price function to pass the data into the payment processor and start the payment flow.

Again, you can try this shopping cart functionality in the Business Messages conversation by deploying the application and interacting with your agent.

$ gcloud app deploy

6. Congratulations

Congratulations, you've successfully built a shopping cart experience within Business Messages.

Something we didn't go over in this codelab is the feature to empty the entire shopping cart. If you'd like, try to extend the application to fulfill the "Empty the cart" feature. The solution is available in step-3 of the source code you cloned.

In a future section, we'll integrate with an external payment processor to enable your users to complete a payment transaction with your brand.

What makes a good shopping cart?

A good shopping cart experience in a conversation is no different than a mobile app or in a physical store. Being able to add items, remove items, and calculating the price of the cart are just a few features we explored in this codelab. Something that's different from a real world shopping cart is being able to see the price of all items in the cart at any given moment, as you add items or remove items. These kinds of high-value features will make your conversational commerce experience stand out!

What's next?

When you're ready, check out some of the following topics to learn about more complex interactions you can achieve in Business Messages:

Reference docs