Compra en línea, retira en la tienda: Bonjour Meal - Parte 3: Integración con un procesador de pagos

1. Introducción

53003251caaf2be5.png 8826bd8cb0c0f1c7.png

Última actualización: 13/09/2021

Cobrar pagos

La recopilación de pagos en Business Messages te abre la puerta a un nuevo mundo de oportunidades comerciales dentro de la plataforma de conversación. Imagine que un cliente potencial le envía una consulta sobre un producto del que desea obtener más información. Una vez que se haya respondido a sus preguntas, puede cerrar el acuerdo proporcionando una puerta de enlace de pago directamente en la conversación.

fe0c6754fb69d708.png

¿Qué características tiene una buena experiencia de pago?

Una buena experiencia de pago es aquella en la que los usuarios pueden pagar de la forma en que están acostumbrados.

Los usuarios tienen preferencias sobre cómo pagan y diferentes formas de pago que en otras partes del mundo. Con Business Messages puedes integrar más de un procesador de pagos a fin de lograr el mayor grado de comodidad para los usuarios.

Una vez que un usuario completa un flujo de pago, desea informarle que recibió su pago correctamente. La mayoría de los procesadores de pagos incluyen una devolución de llamada exitosa o con errores que envía una solicitud HTTP a una URL de tu elección cuando se completa el flujo de pago.

Qué compilarás

En la sección anterior de la serie de codelab, extendiste el agente de Bonjour Meal para presentar un catálogo de artículos, creaste un carrito de compras que permite a los usuarios agregar y quitar artículos, y calcular el precio total del carrito de compras. En esta sección, ampliarás el agente para que pueda procesar pagos según el contenido del carrito de compras.

En este codelab, tu app hará lo siguiente:

  • Integración en la puerta de enlace de pagos Stripe
  • Permitir que un usuario complete el flujo de pago según el precio del carrito
  • Enviar una notificación a la superficie de conversación para informar al usuario sobre el estado de pago

ba08a4d2f8c09c0e.png

Actividades

  • Realiza la integración con el procesador de pagos de Stripe.
  • Envía una solicitud a Stripe para iniciar una sesión de pago.
  • Maneja las respuestas de éxito o falla de pago de Stripe.

Requisitos

  • Un proyecto de GCP registrado y aprobado para su uso con Business Messages
  • Consulta nuestro sitio para desarrolladores a fin de obtener instrucciones sobre cómo hacerlo
  • Un dispositivo Android con la versión 5 o posterior, O un dispositivo iOS con la app de Google Maps
  • Tener experiencia en programación de aplicaciones web
  • Una conexión a Internet

2. Cómo agregar dependencias

Cómo actualizar requirements.txt

Como realizaremos la integración con el procesador de pagos de Stripe, podemos usar la biblioteca cliente de Python de Stripe. Agrega stripe al archivo requirements.txt sin una versión para obtener la versión más reciente de la dependencia.

Esto es necesario para que el entorno de ejecución de Python en Google Cloud App Engine incluya el módulo de Python seccionado.

requirements.txt

...
stripe
...

Cómo preparar bopis/views.py

En la parte superior de bopis/views.py, importa render desde django.shortcuts y JsonResponse desde django.http. Además, necesitaremos importar stripe para admitir llamadas a la biblioteca cliente de Python de Stripe.

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

3. Trabajar con Stripe

Crea una cuenta en Stripe.com

En este codelab, acabamos de usar Stripe, pero puedes integrarlo con cualquier procesador que admita la integración web. Crea una cuenta en stripe.com. Usaremos este perfil para realizar pruebas y con fines educativos para aprender a integrarlo directamente a cualquier procesador de pagos de terceros.

6731d123c56feb67.png

Una vez que hayas creado una cuenta y hayas accedido, deberías ver un panel como este.

6d9d165d2d1fbb8c.png

Asegúrate de que estés en un &modo de prueba y haz clic en el botón Developers como se describe en la captura de pantalla anterior para buscar tus claves de API. Deberías ver dos conjuntos de claves de API: una clave publicable y una clave secreta. Necesitarás ambas claves para facilitar las transacciones de pago con Stripe.

Actualiza bopis/views.py

Tu aplicación necesita ambos conjuntos de claves, por lo que debes actualizarlas en views.py.

Puedes configurar la clave secreta directamente en la propiedad Stripe.api_key y asignarle el valor de esa clave que se encuentra en el panel del desarrollador de Stripe. Luego, crea una variable global llamada STRIPE_PUBLIC_KEY y configúrala con la clave publicable.

Además, Stripe necesita redireccionar a los usuarios a una página web que administres, por lo tanto, creemos una variable global adicional para incluir el dominio de tu aplicación disponible de forma pública.

Al final de estas modificaciones, tendrás algo como lo siguiente:

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

Eso es todo lo que necesitas hacer para la configuración de Stripe.

4. Funcionalidad de Checkout

Actualiza la función de precio total del carrito de compras

Actualmente, la función send_shopping_cart_total_price solo envía un mensaje que especifica el precio del carrito de compras. Agreguemos una acción sugerida para abrir una URL a la página de confirmación de la 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)

Cuando el usuario presiona esta acción sugerida, se lo dirige a una página web que muestra el precio total y un botón para iniciar el pago con Stripe.

Creemos una página web simple que admita este flujo.

En el código fuente del proyecto, busca el directorio llamado bopis. Crea un directorio nuevo dentro de los bopis llamado templates y, dentro de las plantillas, crea otro directorio llamado bopis. Este es un patrón de diseño de Django para especificar el nombre de la app dentro del directorio de plantillas. Ayuda a reducir la confusión de las plantillas entre las apps de Django.

Ahora deberías tener un directorio con una ruta en bopis/templates/bopis/. Puedes crear archivos HTML en este directorio para entregar páginas web. Django las denomina plantillas que se procesan en el navegador. Comencemos con checkout.html.

En este directorio, crea checkout.html. En el siguiente fragmento de código, se muestra un botón de confirmación de la compra y el precio del carrito. También incluye JavaScript para iniciar la confirmación de la compra en 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>

Necesitamos una ruta a esta página web cuando se solicita la URL. La acción sugerida de confirmación de la compra tiene un valor de openUrlAction establecido en {YOUR_DOMAIN}/checkout/{conversation_id}. Esto se traduce en algo como https://<GCP-Project-ID>.appspot.com/checkout/abc123-cba321-abc123-cba321. Antes de crear esta ruta, repasemos el código JavaScript que se encuentra en la plantilla 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>
...

Analicemos juntos el fragmento de código anterior.

  1. Primero, crea una entidad de Stripe con la clave pública que se pasa por el contexto desde la función de vista, otro paradigma de Django.
  2. Luego, el fragmento busca un elemento en la página con el ID checkout-button.
  3. Se agrega un objeto de escucha de eventos a ese elemento.

Este objeto de escucha de eventos se activará cuando un usuario haga clic en este botón o lo presione para iniciar una solicitud POST al servidor web que especifiques a través de la URL: {YOUR_DOMAIN}/create-checkout-session/{conversation_id}..

Puedes ver la lógica del servidor web en los siguientes fragmentos. Cuando el usuario presiona el botón con el ID &checkout-button; podemos esperar que muestre un ID de sesión de Stripe que se crea mediante la API de Stripe y especifica el precio del carrito.

Si tu servidor pudo producir un ID de sesión válido, la lógica de la aplicación redireccionará al usuario a una página de Stripe Checkout; de lo contrario, alertará al usuario con un mensaje de JavaScript estándar que indica que se produjo un error.

Comencemos por agregar nuevas rutas al array urlpatterns para admitir la página de confirmación de la compra y generar el ID de sesión. Agregue lo siguiente al array urlpatterns en urls.py.

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

Luego, creemos las funciones de vistas en views.py para mostrar la plantilla checkout.html y generar la sesión de confirmación de la compra con 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)

...

Ambas funciones usan el ID de conversación para asociar el carrito de compras con el usuario y, luego, determinar el precio que Stripe debería cobrarle.

Estos dos métodos constituyen la primera mitad del flujo de pago. Si implementas esto y pruebas la experiencia, verás un formulario de confirmación de la compra en Stripe con el que puedes completar el pago con una tarjeta de crédito de prueba, como se indica en la documentación para desarrolladores de Stripe a fin de probar la confirmación de la compra en Visa.

La segunda mitad del flujo es cómo llevamos al usuario de vuelta a la conversación una vez que recibimos la respuesta de Stripe con respecto al pago del usuario.

5. Respuestas de Stripe

Cuando un usuario participa en tu flujo de pago, se completó correctamente o no pudo completarlo. En la función create_checkout_session, definimos un success_url y un cancel_url. Stripe redireccionará a una de estas dos URL según el estado del pago. Definamos estas dos rutas en urls.py y agreguemos dos funciones de vista a bopis/views.py para admitir estos dos flujos posibles.

Agregue estas líneas al archivo urls.py.

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

Y las vistas correspondientes se verán de la siguiente manera:

... 

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 redirecciona al dominio como especificaste en la constante DOMAIN, lo que significa que debes procesar una respuesta HTML a través de una plantilla o el sitio web se verá muy simple. Creemos dos archivos HTML simples en el directorio bopis/templates/bopis/ junto con 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>

Con estas dos plantillas, se redirecciona a un usuario que completa un flujo de pago con la integración de Stripe a las URL adecuadas y se le presentan las plantillas respectivas. También recibirán un mensaje a través de Business Messages, lo que les permitirá volver a la conversación.

6. Recibir pagos

¡Felicitaciones! Integraste correctamente un procesador de pagos en tu agente de Business Messages.

En esta serie, implementaste una aplicación web en Google Cloud App Engine, configuraste tu webhook en Business Communications Developer Console, extendiste la aplicación para que admita la búsqueda de inventario a través de una base de datos estática y creaste un carrito de compras con Google Datastore. En la parte final de la serie, integraste con Stripe, un procesador de pagos que admite integraciones web y con esta experiencia. Ahora puede participar en integraciones con otros procesadores de pagos y mucho más.

d6d80cf9c9fc621.png 44db8d6441dce4c5.png

¿Qué sigue?

Cuando estés listo, consulta algunos de los siguientes temas para obtener más información sobre las interacciones más complejas que puedes lograr en Business Messages:

Documentos de referencia