Comprar on-line e retirar na loja: Bonjour Meal – parte 3: integração com um processador de pagamentos

1. Introdução

53003251caaf2be5.png 8826bd8cb0c0f1c7.png

Última atualização: 13/09/2021

Coleta de pagamentos

Ao receber pagamentos no Business Messages, você abre um novo mundo de oportunidades de negócios na plataforma de conversa. Imagine um cliente em potencial que está enviando uma pergunta sobre um produto que ele quer conhecer melhor. Depois que as perguntas forem respondidas, você pode fechar o negócio com elas fornecendo um gateway de pagamento dentro da conversa.

fe0c6754fb69d708.png

O que oferece uma boa experiência de pagamento?

Uma boa experiência de pagamento é aquela em que os usuários podem pagar da maneira que estão acostumados.

Os usuários têm preferências quanto a como pagam e diferentes formas de pagamento são mais comuns do que outras em diferentes partes do mundo. Com o Business Messages, você pode fazer a integração com mais de um processador de pagamentos para aumentar a conveniência para os usuários.

Depois que um usuário conclui um fluxo de pagamento, você quer informar que o pagamento foi recebido. A maioria dos processadores de pagamento inclui um callback de sucesso ou falha que envia uma solicitação HTTP para um URL de sua escolha após a conclusão do fluxo de pagamento.

O que você vai criar

Na seção anterior da série do codelab, você ampliou o agente da Bonjour Meal para apresentar um catálogo de itens, criou um carrinho de compras que permite aos usuários adicionar e remover itens e calcular o preço total do carrinho de compras. Nesta seção, você vai estender ainda mais o agente para que ele possa processar os pagamentos com base no conteúdo do carrinho de compras.

Neste codelab, seu app vai:

  • Integrar com o gateway de pagamento do Stripe
  • Permitir que um usuário conclua o fluxo de pagamento com base no preço do carrinho
  • Enviar uma notificação de volta à superfície de conversa para informar o usuário sobre o estado do pagamento

ba08a4d2f8c09c0e.png

O que você vai fazer

  • Faça a integração com o processador de pagamentos Stripe.
  • Envie uma solicitação ao Stripe para iniciar uma sessão de pagamento.
  • Gerencie as respostas de sucesso ou falha do pagamento da Stripe.

O que é necessário

  • Um projeto do GCP registrado e aprovado para uso com o Business Messages
  • Confira nosso site para desenvolvedores para ver instruções sobre como fazer isso
  • Um dispositivo Android com a versão 5 ou mais recente OU um dispositivo iOS com o app Google Maps
  • Experiência com programação de aplicativos da Web
  • Uma conexão com a Internet

2. Como adicionar dependências

Atualização do requirements.txt

Como vamos integrar o processador de pagamentos Stripe, podemos usar a biblioteca de cliente Python do Stripe. Adicione stripe ao arquivo requirements.txt sem uma versão para ter a versão mais recente da dependência.

Isso é necessário para que o ambiente de execução de Python do Google Cloud App Engine inclua o módulo Python da Stripe.

requirements.txt

...
stripe
...

Como preparar bopis/views.py

Na parte de cima de bopis/views.py, importe render de django.shortcuts e JsonResponse de django.http. Além disso, vamos precisar importar stripe para oferecer suporte a chamadas para a biblioteca de cliente Python da Stripe.

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

3. Trabalhando com a Stripe

Criar uma conta no Stripe.com

Neste codelab, acabamos usando o Stripe. No entanto, você pode fazer a integração com qualquer processador compatível com a integração na Web. Crie uma conta em stripe.com. Usaremos esse perfil para fins educacionais e de teste para saber como você pode se integrar diretamente a qualquer processador de pagamentos de terceiros.

6731d123c56feb67.png.

Depois de criar e fazer login em uma conta, você verá um painel como este.

6d9d165d2d1fbb8c.png.

Certifique-se de operar no "modo de teste" e clique no botão Desenvolvedores, conforme descrito na captura de tela acima para procurar as chaves de API. Você verá dois conjuntos de chaves de API: uma chave publicável e uma chave secreta. Essas duas chaves serão necessárias para facilitar as transações de pagamento com o Stripe.

Atualizar bopis/views.py

Seu aplicativo precisa dos dois conjuntos de chaves. Portanto, atualize-os em views.py.

É possível definir a chave secreta diretamente na propriedade Stripe.api_key e atribuir a ela o valor da chave secreta encontrada no painel de desenvolvedor da Stripe. Em seguida, crie uma variável global chamada STRIPE_PUBLIC_KEY e a defina como a chave publicável.

Além disso, a Stripe precisa redirecionar os usuários para uma página da Web que você gerencia. Portanto, vamos criar uma variável global adicional para incluir o domínio acessível publicamente do seu aplicativo.

Ao final destas modificações, você terá algo parecido com isto:

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

Isso é tudo o que você precisa para a configuração da Stripe.

4. Funcionalidade de finalização de compra

Atualizar a função de preço total do carrinho de compras

No momento, a função send_shopping_cart_total_price envia apenas uma mensagem especificando o preço do carrinho de compras. Vamos adicionar uma ação sugerida para abrir um URL na página de finalização da compra.

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)

Quando o usuário toca nessa ação sugerida, ele é direcionado para uma página da Web que mostra o preço total e um botão para iniciar o pagamento com a Stripe.

Vamos criar uma página da Web simples que será compatível com esse fluxo.

No código-fonte do projeto, encontre o diretório chamado bopis. Crie um novo diretório nos bopis chamado templates e, dentro dos modelos, crie outro diretório chamado bopis. Este é um padrão de design do nginx para especificar o nome do app no diretório de modelos. Isso ajuda a reduzir a confusão de modelos entre aplicativos nginx.

Agora você tem um diretório com um caminho em bopis/templates/bopis/. É possível criar arquivos HTML neste diretório para exibir páginas da Web. Ele se refere a eles como modelos renderizados para o navegador. Vamos começar com checkout.html.

Nesse diretório, crie checkout.html. O snippet de código a seguir exibe um botão para finalizar a compra e o preço do carrinho. Ele também inclui JavaScript para iniciar a finalização de compra da Stripe.

{% 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>

Precisamos de uma rota para esta página da Web quando o URL for solicitado. A valor de actionAction sugerida para finalização da compra tem um valor openUrlAction definido como {YOUR_DOMAIN}/checkout/{conversation_id}. Isso se traduz em algo como https://<GCP-Project-ID>.appspot.com/checkout/abc123-cba321-abc123-cba321. Antes de criar essa rota, vamos analisar o JavaScript encontrado no modelo HTML.

...
  <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>
...

Vamos analisar o snippet de código acima.

  1. Primeiro, ele cria uma entidade Stripe com a chave pública que é transmitida pelo contexto da função de visualização, outro paradigma nginx.
  2. Em seguida, o snippet procura um elemento na página com o ID checkout-button.
  3. Um listener de eventos é adicionado a esse elemento.

Este listener de eventos será acionado quando um usuário clicar ou tocar nesse botão, que iniciará uma solicitação POST para o servidor da Web que você especificar pelo URL: {YOUR_DOMAIN}/create-checkout-session/{conversation_id}.

Você pode ver a lógica do servidor da Web nos snippets abaixo. Quando o usuário toca no botão com o ID "checkout-button" podemos esperar que ele retorne um ID de sessão da Stripe, criado com a API Stripe que especifica o preço do carrinho.

Se o servidor tiver conseguido produzir um ID de sessão válido, a lógica do aplicativo redirecionará o usuário para uma página de finalização da compra da Stripe. Caso contrário, alertará o usuário com uma mensagem JavaScript padrão de que algo deu errado.

Vamos começar adicionando novos caminhos à matriz de urlpatterns para oferecer suporte à página de finalização de compra e gerar o ID de sessão. Adicione o seguinte à matriz de urlpatterns em urls.py.

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

Em seguida, vamos criar as funções de visualização em views.py para retornar o modelo de checkout.html e gerar a sessão de finalização da compra da 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)

...

Essas duas funções usam o conversation_id para associar o carrinho de compras ao usuário e para determinar o preço que a Stripe cobrará do usuário.

Esses dois métodos formam a primeira metade do fluxo de pagamento. Se você implantar e testar a experiência, verá um formulário de finalização de compra da Stripe. Nele, é possível concluir o pagamento com um cartão de crédito de teste, conforme recomendado na documentação para desenvolvedores da Stripe sobre como testar a finalização da compra do Visa.

A segunda metade do fluxo é como trazemos o usuário de volta para a conversa quando recebemos a resposta da Stripe sobre o pagamento do usuário.

5. Respostas da Stripe

Quando um usuário interage com seu fluxo de pagamento, ele foi concluído ou não concluiu o pagamento. Na função create_checkout_session, definimos um success_url e um cancel_url. A Stripe redireciona para um desses dois URLs, dependendo do estado do pagamento. Vamos definir essas duas rotas em urls.py e, em seguida, adicionar duas funções de visualização a bopis/views.py para oferecer suporte a esses dois fluxos possíveis.

Adicione estas linhas ao arquivo urls.py.

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

As visualizações correspondentes ficarão assim:

... 

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')

...

A Stripe redireciona de volta ao domínio, conforme especificado na constante DOMAIN. Isso significa que você precisa renderizar uma resposta HTML usando um modelo, ou o site ficará muito vazio. Vamos criar dois arquivos HTML simples no diretório bopis/templates/bopis/ com o checkout.html.

bm-django-echo-bot/bopis/ templates/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/ templates/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>

Com esses dois modelos, um usuário que conclui um fluxo de pagamento com sua integração do Stripe é redirecionado para os URLs apropriados e é exibido com os respectivos modelos. Eles também receberão uma mensagem por meio do Business Messages, que permite que eles retornem à conversa.

6. Receba pagamentos.

Parabéns! Você integrou um processador de pagamentos ao seu agente do Business Messages.

Nesta série, você implantou um aplicativo da Web no Google Cloud App Engine, definiu o webhook no Business Communications Developer Console, estendeu o aplicativo para dar suporte à pesquisa de inventário com um banco de dados estático e criou um carrinho de compras usando o Google Datastore. Na parte final da série, você se integrou ao Stripe, um processador de pagamentos compatível com integrações com a Web e com essa experiência. Agora é possível fazer integrações com outros processadores de pagamentos e muito mais.

d6d80cf9c9fc621.png 44db8d6441dce4c5.png.

Qual é a próxima etapa?

Quando tudo estiver pronto, confira alguns dos seguintes tópicos para saber mais sobre as interações mais complexas que você pode realizar no Business Messages:

Documentos de referência