在实体店购买在线自提服务:Bonjour Meal - 第 3 部分 - 与付款处理方集成

1. 简介

53003251caaf2be5.png 8826bd8cb0c0f1c7.png

上次更新日期:2021 年 9 月 13 日

正在收款!

通过在 Business Messages 中收款,您将获得一个对话平台上的全新商业机会。设想一下,潜在客户向您询问想要详细了解的产品。对方回答问题后,您可以在对话中提供付款网关,与对方达成交易。

fe0c6754fb69d708.png

怎样才算是良好的付款体验?

良好的付款体验可让用户以自己习惯的方式付款。

用户对付款方式的偏好程度不同,而不同国家/地区的用户使用的付款方式不同。通过 Business Messages,您可以集成多个付款处理方,尽可能为用户提供便利。

用户完成付款流程后,您需要告知他们您已成功收到付款。大多数付款处理方都包含成功或失败回调,该回调会在付款流程完成后向您选择的网址发送 HTTP 请求。

构建内容

在本 Codelab 系列的上一部分中,您扩展了 Bonjour Meal 代理来展示商品清单,创建了允许用户添加和移除商品的购物车,并计算出购物的总价购物车。在本部分中,您将进一步扩展代理,以便它能够根据购物车的内容处理付款。

在此 Codelab 中,您的应用将

  • Stripe 支付网关集成
  • 允许用户根据购物车价格完成付款流程
  • 将通知发送回对话界面,以告知用户付款状态

ba08a4d2f8c09c0e.png

您应执行的操作

  • 与 Stripe 付款处理器集成。
  • 向 Stripe 发送请求以发起付款会话。
  • 处理来自 Stripe 的付款成功或失败响应。

所需条件

  • 已注册并获准与 Business Messages 配合使用的 GCP 项目
  • 如需了解具体方法,请访问我们的开发者网站
  • 一台安装了版本 5 或更高版本的 Android 设备或安装了 Google 地图应用的 iOS 设备
  • Web 应用编程经验
  • 已连接到互联网!

2. 添加依赖项

正在更新 required.txt

由于我们即将与 Stripe 付款处理器集成,因此我们可以使用 Stripe Python 客户端库。将 stripe 添加到 required.txt 中而不使用版本,以获取最新版的依赖项。

这是 Google Cloud App Engine Python 运行时必须包含条纹 Python 模块所必需的。

requirements.txt

...
stripe
...

准备 bopis/views.py

在 bopis/views.py 的顶部,从 django.shortcuts 导入 render,并从 django.http 导入 JsonResponse。此外,我们需要导入 stripe 以支持对 Stripe Python 客户端库的调用。

...
from django.shortcuts import render
from django.http import JsonResponse
import stripe
...

3.使用 Stripe

在 Stripe.com 上创建帐号

在此 Codelab 中,我们恰巧使用的是 Stripe,但您可以与任何支持网页集成功能的处理器集成。在 stripe.com 上创建帐号。我们会使用此个人资料进行测试和培训,以了解如何直接与任何第三方付款处理方集成。

6731d123c56feb67.png

创建帐号并登录后,您应该会看到如下所示的信息中心。

6d9d165d2d1fbb8c.png

请确保您在“测试模式”下运行,然后点击上方屏幕截图中所述的开发者按钮以查找您的 API 密钥。您应该会看到两组 API 密钥:一个“可发布密钥”和一个“密钥”。 您需要同时使用这两个密钥,才能通过 Stripe 完成付款交易。

更新 bopis/views.py

您的应用需要两组密钥,因此请在 views.py 中对其进行更新。

您可以直接在 tripe.api_key 属性中设置密钥,并为其分配 Stripe 开发者信息中心内提供的 Secret 密钥值。然后,创建一个名为 STRIPE_PUBLIC_KEY 的全局变量,并将其设置为可发布的键。

此外,Stripe 需要重定向回您管理的网页,所以我们要再创建一个全局变量,用来包含您的应用的可公开访问的域。

完成这些修改后,代码如下所示:

stripe.api_key = 'sk_test_abcde-12345'
STRIPE_PUBLIC_KEY = 'pk_test_edcba-54321'
YOUR_DOMAIN = 'https://<GCP_PROJECT_ID>.appspot.com'

这就是 Stripe 设置的所有操作。

4.结账功能

更新购物车总价函数

目前,send_shopping_cart_total_price 函数仅发送指定购物车价格的消息。我们来添加一个建议操作,用来打开结算页面的网址。

def send_shopping_cart_total_price(conversation_id):
  """Sends shopping cart price to the user through Business Messages.

  Args:
    conversation_id (str): The unique id for this user and agent.
  """
  cart_price = get_cart_price(conversation_id)

  message_obj = BusinessMessagesMessage(
      messageId=str(uuid.uuid4().int),
      representative=BOT_REPRESENTATIVE,
      text=f'Your cart\'s total price is ${cart_price}.',
      suggestions=[
          BusinessMessagesSuggestion(
              action=BusinessMessagesSuggestedAction(
                  text='Checkout',
                  postbackData='checkout',
                  openUrlAction=BusinessMessagesOpenUrlAction(
                      url=f'{YOUR_DOMAIN}/checkout/{conversation_id}'))),
      ]
    )

  send_message(message_obj, conversation_id)

用户点按此建议操作后,会转到显示总价的网页,以及使用 Stripe 启动付款的按钮。

我们来构建一个支持此流程的简单网页。

在项目源代码中,找到名为 bopis 的目录。在 bopi 中创建一个名为 templates 的新目录,并在模板中创建一个名为 bopis 的其他目录。该 Django 设计模式用于在模板目录中指定应用程序名称。这有助于减少 Django 应用之间的模板混淆。

您现在应该有一个路径为 bopis/templates/bopis/ 的目录。您可以在此目录中创建 HTML 文件来提供网页。Django 将这些模板渲染为浏览器。我们先来听听“checkout.html”。

在此目录中创建 checkout.html。以下代码段显示了结账按钮和购物车价格。此外,它还包含用于启动 Stripe 结账的 JavaScript。

{% load static %}

<!DOCTYPE html>
<html>
  <head>
    <title>Purchase from Bonjour Meal</title>

    <script src="https://js.stripe.com/v3/"></script>
    <style>
      .description{
        font-size: 4em;
      }
      button {
        color: red;
        padding: 40px;
        font-size: 4em;
      }
    </style>
  </head>
  <body>
    <section>
      <img
        src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"
        alt="Bonjour Meal Restaurant"
      />
      <div class="product">
        <div class="description">
          <h3>Your Bonjour Meal Total</h3>
          <h5>${{cart_price}}</h5>
        </div>
      </div>
      <button type="button" id="checkout-button">Checkout</button>
    </section>
  </body>
  <script type="text/javascript">
    // Create an instance of the Stripe object with your publishable API key
    var stripe = Stripe("{{stripe_public_key}}");
    var checkoutButton = document.getElementById("checkout-button");

    checkoutButton.addEventListener("click", function () {
      fetch("/create-checkout-session/{{conversation_id}}", {
        method: "POST",
      })
        .then(function (response) {
          return response.json();
        })
        .then(function (session) {
          return stripe.redirectToCheckout({ sessionId: session.id });
        })
        .then(function (result) {
          // If redirectToCheckout fails due to a browser or network
          // error, you should display the localized error message to your
          // customer using error.message.
          if (result.error) {
            alert(result.error.message);
          }
        })
        .catch(function (error) {
          console.error("Error:", error);
        });
    });
  </script>
</html>

请求网址时,我们需要指向此网页的路线。结帐建议 Action 的 openUrlAction 值设置为 {YOUR_DOMAIN}/checkout/{conversation_id}。这相当于 https://<GCP-Project-ID>.appspot.com/checkout/abc123-cba321-abc123-cba321。在创建此路线之前,我们先来回顾一下在 HTML 模板中找到的 JavaScript。

...
  <script type="text/javascript">
    // Create an instance of the Stripe object with your publishable API key
    var stripe = Stripe("{{stripe_public_key}}");
    var checkoutButton = document.getElementById("checkout-button");

    checkoutButton.addEventListener("click", function () {
      fetch("/create-checkout-session/{{conversation_id}}", {
        method: "POST",
      })
        .then(function (response) {
          return response.json();
        })
        .then(function (session) {
          return stripe.redirectToCheckout({ sessionId: session.id });
        })
        .then(function (result) {
          // If redirectToCheckout fails due to a browser or network
          // error, you should display the localized error message to your
          // customer using error.message.
          if (result.error) {
            alert(result.error.message);
          }
        })
        .catch(function (error) {
          console.error("Error:", error);
        });
    });
  </script>
...

我们来一起学习以上代码段。

  1. 首先,它使用公钥从视图函数(另一个 Django 范式)传入 Stripe 实体。
  2. 然后,代码段在页面上查找 ID 为 checkout-button 的元素。
  3. 系统会向该元素添加一个事件监听器。

当用户点击或点按此按钮时,系统会向您在以下网址中指定的网络服务器发送 POST 请求,从而触发此事件监听器:{YOUR_DOMAIN}/create-checkout-session/{conversation_id}.

您可以在以下代码段中看到网络服务器逻辑。当用户点按 ID 为“checkout-button”的按钮时,我们希望系统能返回一个使用 Stripe API 创建的 Stripe 会话 ID,该 ID 指定了购物车的价格。

如果您的服务器能够生成有效的会话 ID,应用逻辑会将用户重定向到 Stripe Checkout 页面,否则,它会使用标准 JavaScript 消息提醒用户出现了问题。

我们先向网址格式模式添加新路径,以支持结账页并生成会话 ID。将以下内容添加到 urls.py 中的 urlpatterns 数组。

...
path('checkout/<str:conversation_id>', bopis_views.payment_checkout),
path('create-checkout-session/<str:conversation_id>', bopis_views.create_checkout_session),
...

然后,我们在 views.py 中创建视图函数,以返回 checkout.html 模板并生成 Stripe 结帐会话。

...

def payment_checkout(request, conversation_id):
  """Sends the user to a payment confirmation page before the payment portal.

  Args:
    request (HttpRequest): Incoming Django request object
    conversation_id (str): The unique id for this user and agent.

  Returns:
    Obj (HttpResponse): Returns an HTTPResponse to the browser
  """

  cart_price = get_cart_price(conversation_id)
  context = {'conversation_id': conversation_id,
             'stripe_public_key': STRIPE_PUBLIC_KEY,
             'cart_price': cart_price
            }
  return render(request, 'bopis/checkout.html', context)

@csrf_exempt
def create_checkout_session(request, conversation_id):
  """Creates a Stripe session to start a payment from the conversation.

  Args:
    request (HttpRequest): Incoming Django request object
    conversation_id (str): The unique id for this user and agent.

  Returns:
    Obj (HttpResponse): Returns an HTTPResponse to the browser
  """
  cart_price = get_cart_price(conversation_id)
  try:
    checkout_session = stripe.checkout.Session.create(
        payment_method_types=['card'],
        line_items=[
            {
                'price_data': {
                    'currency': 'usd',
                    'unit_amount': int(cart_price*100),
                    'product_data': {
                        'name': 'Bonjour Meal Checkout',
                        'images': ['https://storage.googleapis.com/bonjour-rail.appspot.com/apple-walnut-salad.png'],
                    },
                },
                'quantity': 1,
            },
        ],
        mode='payment',
        success_url=YOUR_DOMAIN + '/success/' + conversation_id,
        cancel_url=YOUR_DOMAIN + '/cancel/' + conversation_id,
    )

    return JsonResponse({
        'id': checkout_session.id
    })

  except Exception as e:
    # Handle exceptions according to your payment processor's documentation
    # https://stripe.com/docs/api/errors/handling?lang=python
    return HttpResponse(e)

...

这两个函数都使用 conversation_id 将购物车与用户关联起来,然后确定 Stripe 应向用户收取的价格。

这两种方式构成了付款流程的前半部分。如果您进行此操作并测试应用体验,您将会看到 Stripe 结算表单。在该表单中,您可以使用测试信用卡完成付款,如测试 Visa 结账中的 Stripe 开发者文档中所述。

该流程的后半部分是我们收到 Stripe 关于用户付款的响应后将用户带回到对话的方式。

5. 条纹响应

用户进入您的付款流程后,要么付款成功,要么无法完成付款。在 create_checkout_session 函数中,我们定义了 success_urlcancel_url。Stripe 将根据付款状态重定向到这两个网址之一。让我们在 urls.py 中定义这两个路由,然后向 bopis/views.py 添加两个视图函数,以支持这两个可能的流程。

将下面几行代码添加到 urls.py 文件中。

...
    path('success/<str:conversation_id>', bopis_views.payment_success),
    path('cancel/<str:conversation_id>', bopis_views.payment_cancel),
...

对应的视图将如下所示:

...

def payment_success(request, conversation_id):
  """Sends a notification to the user prompting them back into the conversation.

  Args:
    request (HttpRequest): Incoming Django request object
    conversation_id (str): The unique id for this user and agent.

  Returns:
    Obj (HttpResponse): Returns an HTTPResponse to the browser
  """
  message_obj = BusinessMessagesMessage(
      messageId=str(uuid.uuid4().int),
      representative=BOT_REPRESENTATIVE,
      suggestions=[
          BusinessMessagesSuggestion(
              reply=BusinessMessagesSuggestedReply(
                  text='Check on order', postbackData='check-order')),
      ],
      text='Awesome it looks like we\'ve received your payment.')

  send_message(message_obj, conversation_id)

  return render(request, 'bopis/success.html')

def payment_cancel(request, conversation_id):
  """Sends a notification to the user prompting them back into the conversation.

  Args:
    request (HttpRequest): Incoming Django request object
    conversation_id (str): The unique id for this user and agent.

  Returns:
    Obj (HttpResponse): Returns an HTTPResponse to the browser
  """
  message_obj = BusinessMessagesMessage(
      messageId=str(uuid.uuid4().int),
      representative=BOT_REPRESENTATIVE,
      suggestions=[
          BusinessMessagesSuggestion(
              action=BusinessMessagesSuggestedAction(
                  text='Checkout',
                  postbackData='checkout',
                  openUrlAction=BusinessMessagesOpenUrlAction(
                      url=f'{YOUR_DOMAIN}/checkout/{conversation_id}'))),
      ],
      text='It looks like there was a problem with checkout. Try again?')

  send_message(message_obj, conversation_id)

  return render(request, 'bopis/cancel.html')

...

Stripe 会按照您在 DOMAIN 常量中指定的方式重定向回域名,这意味着您需要通过模板呈现 HTML 响应,否则网站看起来会非常裸露。让我们在 bopis/templates/bopis/ 目录中创建两个简单的 HTML 文件以及 checkout.html。

bm-django-echo-bot/bopis/ template/bopis/success.html

{% load static %}

<html>
<head>
  <title>Business Messages Payment Integration Sample!</title>
  <style>
    p{
      font-size: 4em;
    }
  </style>
</head>
<body>
  <section>

    <p>
      Checkout succeeded - We appreciate your business!
      <br/><br/>
      For support related questions, please email
      <a href="mailto:bm-support@google.com">bm-support@google.com</a>.

    </p>
  </section>
</body>
</html>

bm-django-echo-bot/bopis/ template/bopis/cancel.html

{% load static %}

<html>
<head>
  <title>Checkout canceled</title>
  <style>
    p{
      font-size: 4em;
    }
    </style>
</head>
<body>
  <section>
    <p>Checkout canceled - Forgot to add something to your cart? Shop around then come back to pay!</p>
  </section>
</body>
</html>

使用这两个模板时,系统会将使用 Stripe 集成完成付款流程的用户重定向到相应的网址,并向其显示相应的模板。他们还会通过 Business Messages 收到消息,以便返回对话。

6.接收付款!

恭喜!您已成功将付款处理方集成到您的 Business Messages 代理中!

在本系列中,您已向 Google Cloud App Engine 部署了一个 Web 应用、在 Business Communications Developer Console 上设置网络钩子、扩展了该应用以支持通过静态数据库查询广告资源,并使用 Google Datastore 创建了一个购物车。在本系列文章的最后一部分中,您将与 Stripe 集成。Stripe 是一个支持网络集成和相关体验的付款处理方。现在,您可以与其他付款处理方及更多公司合作!

d6d80cf9c9fc621.png 44db8d6441dce4c5.png

接下来做什么?

完成上述操作后,请查看以下部分主题,了解您可以在 Business Messages 中实现更复杂的互动方式:

参考文档