viernes, 12 de enero de 2024

23. Gestión de pagos y pedidos con Stripe.

Integración de una pasarela de pago

Una pasarela de pago es una tecnología utilizada por los comercios para procesar pagos de clientes en línea. Al utilizar una pasarela de pago, puedes gestionar los pedidos de los clientes y delegar el procesamiento de pagos a un tercero confiable y seguro. Al usar una pasarela de pago confiable, no tendrás que preocuparte por la complejidad técnica, de seguridad y regulatoria de procesar tarjetas de crédito en tu propio sistema.


Gestión de Pagos y Pedidos

Existen varios proveedores de pasarelas de pago para elegir. Vamos a integrar Stripe, que es una pasarela de pago muy popular utilizada por servicios en línea como Shopify, Uber, Twitch y GitHub, entre otros.

Stripe proporciona una Interfaz de Programación de Aplicaciones (API) que te permite procesar pagos en línea con varios métodos de pago, como tarjeta de crédito, Google Pay y Apple Pay. Puedes obtener más información sobre Stripe en https://www.stripe.com/.

Stripe ofrece diferentes productos relacionados con el procesamiento de pagos. Puede gestionar pagos únicos, pagos recurrentes para servicios de suscripción, pagos multiparte para plataformas y mercados, entre otros.

Stripe ofrece diferentes métodos de integración, desde formularios de pago alojados por Stripe hasta flujos de pago totalmente personalizables. Vamos a integrar el producto Stripe Checkout, que consiste en una página de pago optimizada para la conversión. Los usuarios podrán pagar fácilmente con tarjeta de crédito u otros métodos de pago por los artículos que ordenen. Recibiremos notificaciones de pago de Stripe. Puedes ver la documentación de Stripe Checkout en https://stripe.com/docs/payments/checkout.

Al aprovechar Stripe Checkout para procesar pagos, confías en una solución que es segura y cumple con los requisitos de la Industria de Tarjetas de Pago (PCI). Podrás recopilar pagos de Google Pay, Apple Pay, Afterpay, Alipay, débitos directos SEPA, débitos directos Bacs, débitos directos BECS, iDEAL, Sofort, GrabPay, FPX y otros métodos de pago.


Creación de una cuenta en Stripe

Necesitas una cuenta en Stripe para integrar la pasarela de pago en tu sitio. Creemos una cuenta para probar la API de Stripe. Abre https://dashboard.stripe.com/register en tu navegador. Verás un formulario como el siguiente:

The Stripe signup form


Rellena el formulario con tus datos y haz clic en Crear Cuenta. Recibirás un email con un link para verificar tu correo electrónico. El correo será similar a este:

The verification email to verify your email address


Abre el email y haz clic en el enlace que se adjunta para verificar tu cuenta. Serás redirigido al panel de control de Stripe que tiene un aspecto parecido a este:

The Stripe dashboard after verifying the email address


En la esquina superior derecha de la pantalla, puedes ver que el modo de prueba está activado. Stripe te proporciona un entorno de prueba y un entorno de producción. Si tienes un negocio o eres autónomo, puedes agregar los detalles de tu empresa para activar la cuenta y obtener acceso para procesar pagos reales. Sin embargo, esto no es necesario para implementar y probar pagos a través de Stripe, ya que estaremos trabajando en el entorno de prueba.

Necesitas agregar un nombre de cuenta para procesar pagos. Abre https://dashboard.stripe.com/settings/account en tu navegador. Verás la siguiente pantalla:

The Stripe account settings


En nombre de la cuenta, introduce un nombre y haz clic en guardar. Vuelve de nuevo al panel de control de Stripe. Verás como el nombre que hayas introducido aparece ahora en la parte superior izquierda de la página.


The Stripe dashboard header including the account name

Lo siguiente va a ser instalar el Stipe Python SDK y añadir Stripe a nuestro proyecto de Django.


Instalando la libreria Stripe de Python.


Stripe nos facilita una librería de Python que facilita la comunicación con su API. Vamos a integrar la pasarela de pagos en nuestro proyecto usando la librería Stripe.

Puedes encontrar el código fuente de la librería en https://github.com/stripe/stripepython.

Instala la librería Stripe desde el shell usando el siguiente comando:

pip install stripe


Añadiendo Stripe a nuestro proyecto.

Abre https://dashboard.stripe.com/test/apikeys en tu navegador. También puedes acceder a esta página desde el panel de Stripe haciendo clic en 'Desarrolladores' y luego en 'Claves API'. Verás la siguiente pantalla:


The Stripe test API keys screen



Stripe proporciona un par de claves para dos entornos diferentes, pruebas y producción. Hay una clave pública y una clave secreta para cada entorno. Las claves públicas del modo de prueba tienen el prefijo pk_test_ y las claves públicas del modo en vivo tienen el prefijo pk_live_. Las claves secretas del modo de prueba tienen el prefijo sk_test_ y las claves secretas del modo en vivo tienen el prefijo sk_live_.

Necesitarás esta información para autenticar las solicitudes a la API de Stripe. Siempre debes mantener tu clave privada en secreto y almacenarla de forma segura. La clave pública se puede utilizar en el código del lado del cliente, como scripts de JavaScript. Puedes obtener más información sobre las claves de la API de Stripe en https://stripe.com/docs/keys.

Añade la siguiente configuración al archivo settings.py de tu proyecto:

# Stripe settings
STRIPE_PUBLISHABLE_KEY = '' # Publishable key
STRIPE_SECRET_KEY = '' # Secret key
STRIPE_API_VERSION = '2023-10-16'

Reemplaza los valores STRIPE_PUBLISHABLE_KEY y STRIPE_SECRET_KEY con la clave pública de prueba y la clave secreta proporcionadas por Stripe. Utilizarás la versión 2023-10-16 de la API de Stripe. Puedes ver las notas de lanzamiento para esta versión de la API en https://stripe.com/docs/upgrades#2023-10-16.

Estás utilizando las claves del entorno de prueba para el proyecto. Una vez que vayas en vivo y valides tu cuenta de Stripe, obtendrás las claves del entorno de producción. Más adelante veremos cómo configurar ajustes para múltiples entornos.

Integremos la pasarela de pago en el proceso de pago. Puedes encontrar la documentación en Python para Stripe en https://stripe.com/docs/api?lang=python.


Construyendo el proceso de pago


El proceso de pago funcionará de la siguiente manera:

  1. Agregar artículos al carrito de compras
  2. Pagar el carrito de compras
  3. Ingresar detalles de la tarjeta de crédito y pagar

Vamos a crear una nueva aplicación para gestionar los pagos. Crea una nueva aplicación en tu proyecto usando el siguiente comando:

python manage.py startapp payment

Edita el archivo settings.py del proyecto y añade la nueva aplicación:

PracticaDjango/PracticaDjango/settings.py

    #... 
    'Carro.apps.CarroConfig',
    'Autentificacion.apps.AutentificacionConfig',
    'Orders.apps.OrdersConfig',
    # Aplicaciones de terceros
    'django_bootstrap5',
    'social_django',
    'django_extensions',
    'easy_thumbnails',
    'debug_toolbar',
    'Payment.apps.PaymentConfig',
    # Mapa del Sitio
    'django.contrib.sites', # add sites to installed_apps
    'django.contrib.sitemaps',  # add Django sitemaps to installed app
    # PostgreSQL
    'django.contrib.postgres',
    # Aplicaciones por defecto

La aplicación de pagos está ahora activa en el proyecto.

Actualmente, los usuarios pueden realizar pedidos pero no pueden pagarlos. Después de que los clientes realicen un pedido, necesitamos redirigirlos al proceso de pago.

Edita el archivo views.py de la aplicación de pedidos (Orders) e incluye las siguientes importaciones:

PracticaDjango/Orders/views.py

from django.shortcuts import render
from .models import OrderItem
from .forms import OrderCreateForm
from Carro.carro import Carro
# Crea una tarea asincronica al finalizar la orden
from .tasks import order_created
# Para la gestion de la pasarela de pago
from django.urls import reverse
from django.shortcuts import redirect

# Create your views here.


En el mismo archivo, busca la siguiente línea en la vista order_create:

# launch asynchronous task
order_created.delay(order.id)
return render(request, "Orders/order/created.html", {"order": order})

y sustitúyelas por esta otra:

# launch asynchronous task            
order_created.delay(order.id)
# guarda el pedido en la sesión
request.session['order_id'] = order.id
# redirecciona para hacer el pago
return redirect(reverse('payment:process'))

Ahora el archivo de las vista debería parecerse a esto:

PracticaDjango/Orders/views.py

from django.shortcuts import render
from .models import OrderItem
from .forms import OrderCreateForm
from Carro.carro import Carro
# Crea una tarea asincronica al finalizar la orden
from .tasks import order_created
# Para la gestion de la pasarela de pago
from django.urls import reverse
from django.shortcuts import redirect

# Create your views here.


def order_create(request):
    carro = Carro(request)
    if request.method == "POST":
        form = OrderCreateForm(request.POST)
        if form.is_valid():
            order = form.save()
            for item in carro:
                OrderItem.objects.create(
                    order=order,
                    product=item["producto"],
                    price=item["precio"],
                    quantity=item["cantidad"],
                )
            # Limpia el carro
            carro.clear()
            # launch asynchronous task
            order_created.delay(order.id)
            # guarda el pedido en la sesión
            request.session['order_id'] = order.id
            # redirecciona para hacer el pago
            return redirect(reverse('payment:process'))
    else:
        form = OrderCreateForm()
    return render(request, "Orders/order/create.html", {"cart": carro, "form": form})
En lugar de renderizar la plantilla Orders/order/created.html al realizar un nuevo pedido, el ID del pedido se guarda en la sesión del usuario y se redirige al usuario a la URL payment:process. Vamos a implementar esta URL más adelante. Recuerda que Celery debe estar en ejecución para que la tarea order_created se ponga en cola y se ejecute.

Integremos la pasarela de pago.

Integrando Stripe Checkout


La integración de Stripe Checkout consiste en una página de pago alojada por Stripe que permite al usuario ingresar los detalles del pago, generalmente una tarjeta de crédito, y realizar el pago. Si el pago es exitoso, Stripe redirige al cliente a una página de éxito. Si el cliente cancela el pago, lo redirige a una página de cancelación.

Vamos a implementar tres vistas:

payment_process: Crea una Sesión de Checkout de Stripe y redirige al cliente al formulario de pago alojado por Stripe. Una sesión de checkout es una representación programática de lo que ve el cliente cuando se redirige al formulario de pago, incluyendo los productos, cantidades, moneda y cantidad a cobrar.
payment_completed: Muestra un mensaje para pagos exitosos. El usuario es redirigido a esta vista si el pago es exitoso.
payment_canceled: Muestra un mensaje para pagos cancelados. El usuario es redirigido a esta vista si el pago es cancelado."

El proceso completo de pago funcionará de la siguiente manera:

1. Después de que se cree un pedido, el usuario es redirigido a la vista payment_process. Se le presenta al usuario un resumen del pedido y un botón para proceder con el pago.

2. Cuando el usuario procede a pagar, se crea una sesión de pago de Stripe. La sesión de pago incluye la lista de artículos que el usuario comprará, una URL para redirigir al usuario después de un pago exitoso y una URL para redirigir al usuario si el pago es cancelado.

3. La vista redirige al usuario a la página de pago alojada por Stripe. Esta página incluye el formulario de pago. El cliente ingresa los detalles de su tarjeta de crédito y envía el formulario.

4. Stripe procesa el pago y redirige al cliente a la vista payment_completed. Si el cliente no completa el pago, Stripe en su lugar redirige al cliente a la vista payment_canceled.

Comencemos a construir las vistas de pago. Edita el archivo views.py de la aplicación de pagos y agrega el siguiente código:

PracticaBlog/PracticaDjango/Payment/views.py

from django.shortcuts import render, redirect, reverse, get_object_or_404
from decimal import Decimal
import stripe
from django.conf import settings
from Orders.models import Order


# Creamos la instancia de Stripe
stripe.api_key = settings.STRIPE_SECRET
stripe.api_version = settings.STRIPE_API_VERSION


def payment_process(request):
    order_id = request.session.get('order_id', None)
    order = get_object_or_404(Order, id=order_id)

    if request.method == 'POST':
        success_url = request.build_absolute_uri(reverse('payment:completed'))
        cancel_url = request.build_absolute_uri(reverse('payment:canceled'))

        # Stripe checkout session data
        session_data = {
            'mode': 'payment',
            'client_reference_id': order.id,
            'success_url': success_url,
            'cancel_url': cancel_url,
            'line_items': []
        }
        # create Stripe checkout session
        session = stripe.checkout.Session.create(**session_data)
        # redirect to Stripe payment form
        return redirect(session.url, code=303)
    else:
        return render(request, 'payment/process.html', locals())

En el código anterior, se importa el módulo "stripe" y se establece la clave de la API de Stripe usando el valor del ajuste "STRIPE_SECRET_KEY". También se establece la versión de la API que se va a utilizar usando el valor del ajuste "STRIPE_API_VERSION".

La vista "payment_process" realiza las siguientes tareas:

1. Se recupera el objeto de Pedido actual de la base de datos utilizando la clave de sesión "order_id", que fue almacenada previamente en la sesión por la vista "order_create".

2. Se recupera el objeto de Pedido para el ID dado. Al utilizar la función de atajo "get_object_or_404()", se genera una excepción Http404 (página no encontrada) si no se encuentra ningún pedido con el ID dado.

3. Si la vista se carga con una solicitud GET, se renderiza y devuelve la plantilla "payment/process.html". Esta plantilla incluirá el resumen del pedido y un botón para proceder con el pago, que generará una solicitud POST a la vista.

4. Si la vista se carga con una solicitud POST, se crea una sesión de pago de Stripe con "stripe.checkout.Session.create()" utilizando los siguientes parámetros:

   - "mode": El modo de la sesión de pago. Usamos "payment" para un pago único. Puedes ver los diferentes valores aceptados para este parámetro en https://stripe.com/docs/api/checkout/sessions/object#checkout_session_object-mode.

   - "client_reference_id": La referencia única para este pago. Usaremos esto para conciliar la sesión de pago de Stripe con nuestro pedido. Al pasar el ID del pedido, vinculamos los pagos de Stripe a los pedidos en nuestro sistema, y podremos recibir notificaciones de pago de Stripe para marcar los pedidos como pagados.

   - "success_url": La URL a la que Stripe redirigirá al usuario si el pago es exitoso. Usamos "request.build_absolute_uri()" para generar un URI absoluto a partir de la ruta URL. Puedes ver la documentación para este método en https://docs.djangoproject.com/en/4.1/ref/request-response/#django.http.HttpRequest.build_absolute_uri.

   - "cancel_url": La URL a la que Stripe redirigirá al usuario si se cancela el pago.

   - "line_items": Esta es una lista vacía. A continuación, la poblaremos con los elementos del pedido que se van a comprar.

5. Después de crear la sesión de pago, se devuelve una redirección HTTP con el código de estado 303 para redirigir al usuario a Stripe. El código de estado 303 se recomienda para redirigir aplicaciones web a un nuevo URI después de haber realizado una solicitud HTTP POST.

   Puedes ver todos los parámetros para crear un objeto de sesión de Stripe en https://stripe.com/docs/api/checkout/sessions/create.

La función `locals()` en Python retorna un diccionario que representa las variables locales en el contexto actual. En el fragmento de código que hemos utilizado, se utiliza `locals()` dentro de la función 'render()'` para pasar todas las variables locales del contexto de la vista a la plantilla de renderizado.

```

return render(request, 'Payment/process.html', locals())

```

Lo que hace es tomar todas las variables locales en ese punto de la función 'payment_process' y las pasa a la plantilla como contexto. Esto significa que cualquier variable definida antes de la llamada a 'render()' estará disponible en la plantilla 'Payment/process.html'. Esta es una forma rápida de pasar todas las variables locales al contexto de la plantilla sin necesidad de enumerarlas explícitamente.

¡Vamos a poblar la lista "line_items" con los elementos del pedido para crear la sesión de pago! Cada elemento contendrá el nombre del artículo, la cantidad a cobrar, la moneda a utilizar y la cantidad comprada. Agrega el siguiente código resaltado en azul a la vista "payment_process":

PracticaBlog/PracticaDjango/Payment/views.py

from django.shortcuts import render, redirect, reverse, get_object_or_404
from decimal import Decimal
import stripe
from django.conf import settings
from Orders.models import Order


# Creamos la instancia de Stripe
stripe.api_key = settings.STRIPE_SECRET_KEY
stripe.api_version = settings.STRIPE_API_VERSION


def payment_process(request):
    order_id = request.session.get('order_id', None)
    order = get_object_or_404(Order, id=order_id)

    if request.method == 'POST':
        success_url = request.build_absolute_uri(reverse('payment:completed'))
        cancel_url = request.build_absolute_uri(reverse('payment:canceled'))

        # Stripe checkout session data
        session_data = {
            'mode': 'payment',
            'client_reference_id': order.id,
            'success_url': success_url,
            'cancel_url': cancel_url,
            'line_items': []
        }
        # add order items to the Stripe checkout session
        for item in order.items.all():
            session_data['line_items'].append({
                'price_data': {
                    'unit_amount': int(item.price * Decimal('100')),
                    'currency': 'eur',
                    'product_data': {
                        'name': item.product.name,
                    },
                },
                'quantity': item.quantity,
            })
        # create Stripe checkout session
        session = stripe.checkout.Session.create(**session_data)
        # redirect to Stripe payment form
        return redirect(session.url, code=303)
    else:
        return render(request, 'Payment/process.html', locals())

Utilizamos la siguiente información para cada artículo:

  • price_data: Información relacionada con el precio.
  • unit_amount: La cantidad en centimos que se recogerá con el pago. Este es un número entero positivo que representa cuánto cobrar en la unidad de moneda más pequeña sin lugares decimales. Por ejemplo, para cobrar 10.00€, esto serían 1000 (es decir, 1,000 céntimos). El precio del artículo, item.price, se multiplica por Decimal(‘100’) para obtener el valor en céntimos y luego se convierte en un entero.
  • currency: La moneda a utilizar en formato ISO de tres letras. Utilizamos 'eur' para euros. Puedes ver una lista de monedas admitidas en https://stripe.com/docs/currencies.
  • product_data: Información relacionada con el producto.
  • name: El nombre del producto.
  • quantity: La cantidad de unidades a comprar.

La vista payment_process está ahora lista. Creemos vistas simples para las páginas de éxito y cancelación de pago.

Agrega el siguiente código al archivo views.py de la aplicación de pagos:

PracticaBlog/PracticaDjango/Payment/views.py

#...
def payment_completed(request):
    return render(request, 'Payment/completed.html')
def payment_canceled(request):
    return render(request, 'Payment/canceled.html')


Crea un nuevo archivo dentro del directorio de la aplicación y llámale urls.py. Añádele el siguiente código:

PracticaBlog/PracticaDjango/Payment/urls.py

from django.urls import path
from . import views

app_name = 'payment'

urlpatterns = [
    path('process/', views.payment_process, name='process'),
    path('completed/', views.payment_completed, name='completed'),
    path('canceled/', views.payment_canceled, name='canceled'),
]


Estas son las URL para el flujo de pago. Hemos incluido los siguientes patrones de URL:

• process: La vista que muestra el resumen del pedido al usuario, crea la sesión de pago de Stripe y redirige al usuario al formulario de pago alojado por Stripe.

• completed: La vista a la que Stripe redirige al usuario si el pago se realiza con éxito.

• canceled: La vista a la que Stripe redirige al usuario si se cancela el pago.

Edita el archivo urls.py principal del proyecto PracticaDjano e incluye los patrones de URL para la aplicación de pago, de la siguiente manera:

PracticaBlog/PracticaDjango/Payment/urls.py

#...
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('Proyecto_web_app.urls')),
    path('servicios/', include('Servicios.urls')),
    path('blog/', include('Blog.urls')),
    path('contacto/', include('Contacto.urls')),
    path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
    path('payment/', include('Payment.urls', namespace='payment')),
    path('tienda/', include('Tienda.urls')),
    path('carro/', include('Carro.urls')),
    path('cuenta/', include('Autentificacion.urls')),
    path('social-auth/', include('social_django.urls', namespace='social')),
    path('orders/', include('Orders.urls', namespace='orders')),
    path('__debug__/', include('debug_toolbar.urls')),
]
#...

Hemos colocado la nueva ruta antes del patrón tienda.urls y caroo.urls para evitar una coincidencia no deseada con un patrón definido en ambas. Recuerda que Django recorre cada patrón de URL en orden y se detiene en el primero que coincide con la URL solicitada.

Construyamos una plantilla para cada vista. Crea la siguiente estructura de archivos dentro del directorio de la aplicación de pago:

templates/

    Payment/

        process.html

        completed.html

        canceled.html


Edita la plantilla process.html y añade el siguiente código:

PracticaDjango/Payment/templates/Payment/process.html

<!--Cargamos la plantilla base-->
{% extends "Proyecto_web_app/base.html" %}
{% load thumbnail %}

<!-- Establecemos el titulo de la página -->
{% block title %}Pagar el Pedido{% endblock %}

<!-- Definimos su contenido -->
{% block content %}
<div class="container">
    <h1>Resumen del Pedido</h1>
    <table class="table table-dark table-striped">
        <thead>
            <tr>
                <th>Imagen</th>
                <th>Producto</th>
                <th>Precio</th>
                <th>Cantidad</th>
                <th>Total</th>
            </tr>
        </thead>
        <tbody>
            {% for item in order.items.all %}
            <tr>
                <td>
                    <img src="{% thumbnail item.product.imagen 100x0 %}">
                </td>
                <td>{{ item.product.nombre }}</td>
                <td class="num">{{ item.price }}€</td>
                <td class="num">{{ item.quantity }}</td>
                <td class="num">{{ item.get_cost }}€</td>
            </tr>
            {% endfor %}

            <tr>
                <td>Total</td>
                <td colspan="3"></td>
                <td class="num">{{ order.get_total_cost }} €</td>
            </tr>
        </tbody>
    </table>
    <div class="d-flex justify-content-end">
        <p class="text-right">
        <form action="{% url 'payment:process' %}" method="post">
            <input type="submit" value="Pagar">
            {% csrf_token %}
        </form>
        </p>
    </div>

</div>
{% endblock %}


Este es el formato para mostrar el resumen del pedido al usuario y permitir al cliente proceder con el pago. Incluye un formulario y un botón 'Pagar ahora' para enviarlo mediante POST. Cuando se envía el formulario, la vista payment_process crea la sesión de pago de Stripe y redirige al usuario al formulario de pago alojado por Stripe.

Edita la plantilla Payment/completed.html y añade el siguiente código a ella:

PracticaDjango/Payment/templates/Payment/completed.html

{% extends "Proyecto_web_app/base.html" %}

{% block title %}Payment successful{% endblock %}

{% block content %}
<h1>Pago Realizado.</h1>
<p>Su pago ha sido realizado correctamente.</p>
<p>¡Muchas Gracias!</p>
{% endblock %}

Esta es la plantilla de la página a la que el usuario será redirigido si el proceso de pago se efectúa correctamente.

Edita la plantilla Payment/canceled.html y añade el siguiente código a ella:

PracticaDjango/Payment/templates/Payment/canceled.html

{% extends "Proyecto_web_app/base.html" %}

{% block title %}Payment canceled{% endblock %}

{% block content %}
<h1>Pago NO REALIZADO</h1>
<p>Ha habido un problema al procesar el pago.</p>
{% endblock %}

Esta es la plantilla de la página a la que el usuario será redirigido si el proceso de pago no se efectúa correctamente.

Con esto hemos implementado las vistas necesarias para realizar el proceso de pago, asi como los patrones de sus URLS asociadas. Es hora de probar si todo funciona.


Prueba del proceso de pago

Ejecuta el siguiente comando en la terminal para iniciar el servidor RabbitMQ con Docker:

sudo docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:management-alpine

Esto ejecutará RabbitMQ en el puerto 5672 y la interfaz de gestión basada en web en el puerto 15672.

Abre otra terminal y inicia el worker de Celery desde el directorio de tu proyecto con el siguiente comando:

celery -A PracticaDjango worker -l info

Abre una terminal más y inicia el servidor de desarrollo desde el directorio de tu proyecto con este comando:

python manage.py runserver

Abre http://127.0.0.1:8000/ en tu navegador, agrega algunos productos al carrito de compras y completa el formulario de pago. Haz clic en el botón Realizar pedido. El pedido se guardará en la base de datos, el ID del pedido se guardará en la sesión actual y serás redirigido a la página de proceso de pago.

El aspecto de la página de proceso de pago será el siguiente:

imagen de la pagina con la lista de productos antes de la compra

En esta página, podrás ver un resumen del pedido y un botón de Pagar ahora. Haz clic en Pagar ahora. La vista del proceso de pago creará una sesión de pago en Stripe y serás redirigido al formulario de pago alojado por Stripe. Verás la siguiente página:


página de pago

Utilizando tarjetas de crédito de prueba

Stripe proporciona diferentes tarjetas de crédito de prueba de diferentes emisores y países, lo que te permite simular pagos para probar todos los escenarios posibles (pago exitoso, pago rechazado, etc.). La siguiente tabla muestra algunas de las tarjetas que puedes usar para diferentes escenarios:

tarjetas que se pueden usar para testeo

Puedes encontrar la lista completa de tarjetas de crédito para pruebas en https://stripe.com/docs/testing.

Vamos a usar la tarjeta de prueba 4242 4242 4242 4242, que es una tarjeta Visa que devuelve una compra exitosa. Utilizaremos el CVC 123 y cualquier fecha de vencimiento futura, como por ejemplo 12/29. Ingresa los detalles de la tarjeta de crédito en el formulario de pago de la siguiente manera:


datos introducidos en la página de pago

Después de un par de segundos si todo ha ido correctamente, Stripe te redirigirá a la página que antes habíamos definido para una compra exitosa.


Verificando la información del pago en el panel de control de Stripe

Accede al panel de control de Stripe en https://dashboard.stripe.com/test/payments. Bajo 'Pagos', podrás ver el pago como se muestra en la siguiente imagen:

The payment object with status Succeeded in the Stripe dashboard



El estado del pago es 'Succeeded' (Exitoso). La descripción del pago incluye el ID del intento de pago que comienza con 'pi_'. Cuando se confirma una sesión de pago, Stripe crea un intento de pago asociado con la sesión. Un intento de pago se utiliza para recopilar un pago del usuario. Stripe registra todos los intentos de pago como intentos de pago. Cada intento de pago tiene un ID único y encapsula los detalles de la transacción, como los métodos de pago admitidos, el monto a cobrar y la moneda deseada. Haz clic en la transacción para acceder a los detalles del pago.

Verás la siguiente pantalla:

Payment details for a Stripe transaction

Payment details for a Stripe transaction

Payment details for a Stripe transaction


Puedes encontrar mucha más información en los enlaces de la página.


Usar webhooks para recibir notificaciones de pago


Stripe puede enviar eventos en tiempo real a nuestra aplicación usando webhooks. Un webhook, también llamado callback, puede considerarse como una API basada en eventos en lugar de una API basada en solicitudes. En lugar de consultar frecuentemente la API de Stripe para saber cuándo se completa un nuevo pago, Stripe puede enviar una solicitud HTTP a una URL de nuestra aplicación para notificar sobre pagos realizados en tiempo real. Estas notificaciones de eventos serán asíncronas, ocurren cuando el evento sucede, independientemente de nuestras llamadas síncronas a la API de Stripe.

Construiremos un punto final de webhook para recibir eventos de Stripe. El webhook consistirá en una vista que recibirá un carga JSON con la información del evento para procesarla. Usaremos la información del evento para marcar pedidos como pagados cuando la sesión de pago se complete exitosamente.

Creación de un punto final de webhook


Puedes agregar URLs de puntos finales de webhook a tu cuenta de Stripe para recibir eventos. Como estamos utilizando webhooks y no tenemos un sitio web alojado accesible a través de una URL pública, usaremos la Interfaz de Línea de Comandos (CLI) de Stripe para escuchar eventos y enviarlos a nuestro entorno local.

Abre la dirección https://dashboard.stripe.com/test/webhooks en tu navegador. Verás  la siguiente pantalla:

The Stripe webhooks default screen

Aquí puedes ver un esquema de cómo Stripe notifica a tu integración de manera asíncrona. Recibirás notificaciones de Stripe en tiempo real cada vez que ocurra un evento. Stripe envía diferentes tipos de eventos, como creación de sesión de pago, creación de intención de pago, actualización de intención de pago o sesión de pago completada. Puedes encontrar una lista de todos los tipos de eventos que Stripe envía en https://stripe.com/docs/api/events/types

En esta misma pantalla haz clic en "Probar en un entorno local". Verás la siguiente imagen:

The Stripe webhook setup screen


Esta pantalla muestra los pasos para escuchar los eventos de Stripe desde tu entorno local. También incluye un ejemplo de un punto final de webhook en Python. Copia solo el valor del endpoint_secret.

Edita el archivo settings.py del proyecto PracticaDjango y añade la siguiente configuración:

STRIPE_WEBHOOK_SECRET = ''

Reemplaza el valor STRIPE_WEBHOOK_SECRET con el valor del endpoint_secret proporcionado por Stripe.

Para construir un punto final de webhook, crearemos una vista que reciba un carga JSON con los detalles del evento. Revisaremos los detalles del evento para identificar cuándo se completa una sesión de pago y marcar el pedido relacionado como pagado.

Stripe firma los eventos de webhook que envía a tus puntos finales incluyendo un encabezado Stripe-Signature con una firma en cada evento. Al verificar la firma de Stripe, puedes asegurarte de que los eventos fueron enviados por Stripe y no por un tercero. Si no verificas la firma, un atacante podría enviar eventos falsos intencionalmente a tus webhooks. El SDK de Stripe proporciona un método para verificar firmas. Lo utilizaremos para crear un webhook que verifique la firma.

Añade un nuevo archivo al directorio de la aplicación Payment/ y nómbralo webhooks.py. Añade el siguiente código al nuevo archivo webhooks.py:

/PracticaDjango/Payment/webhooks.py

import stripe
from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from Orders.models import Order


@csrf_exempt
def stripe_webhook(request):
    payload = request.body
    sig_header = request.META['HTTP_STRIPE_SIGNATURE']
    event = None
    try:
        event = stripe.Webhook.construct_event(
            payload,
            sig_header,
            settings.STRIPE_WEBHOOK_SECRET)
    except ValueError as e:
        # Invalid payload
        return HttpResponse(status=400)
    except stripe.error.SignatureVerificationError as e:
        # Invalid signature
        return HttpResponse(status=400)
    return HttpResponse(status=200)


El decorador @csrf_exempt se utiliza para evitar que Django realice la validación CSRF que se realiza por defecto para todas las peticiones POST. Utilizamos el método stripe.Webhook.construct_event() de la librería stripe para verificar el encabezado de firma del evento. Si la carga útil (payload) del evento o la firma no son válidos, devolvemos una respuesta HTTP 400 Bad Request. De lo contrario, devolvemos una respuesta HTTP 200 OK. Esta es la funcionalidad básica necesaria para verificar la firma y construir el evento a partir del payload JSON. Ahora podemos implementar las acciones del punto final del webhook. 

Agrega el siguiente código resaltado en azul a la archivo webhooks.py que creamos anteriormente:

/PracticaDjango/Payment/webhooks.py

import stripe
from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from Orders.models import Order


@csrf_exempt
def stripe_webhook(request):
    payload = request.body
    sig_header = request.META['HTTP_STRIPE_SIGNATURE']
    event = None
    try:
        event = stripe.Webhook.construct_event(
            payload,
            sig_header,
            settings.STRIPE_WEBHOOK_SECRET)
    except ValueError as e:
        # Invalid payload
        return HttpResponse(status=400)
    except stripe.error.SignatureVerificationError as e:
        # Invalid signature
        return HttpResponse(status=400)
    if event.type == 'checkout.session.completed':
        session = event.data.object
        if session.mode == 'payment' and session.payment_status == 'paid':
            try:
                order = Order.objects.get(id=session.client_reference_id)
            except Order.DoesNotExist:
                return HttpResponse(status=404)
            # marca el pedido como pagado 
            order.paid = True
            order.save()
    return HttpResponse(status=200)
En el nuevo código, verificamos si el evento recibido es checkout.session.completed. Este evento indica que la sesión de pago se ha completado con éxito. Si recibimos este evento, recuperamos el objeto de sesión y verificamos si el modo de sesión es de pago, ya que este es el modo esperado para pagos únicos. Luego, obtenemos el atributo client_reference_id que usamos cuando creamos la sesión de pago y usamos el ORM de Django para recuperar el objeto Order con el ID proporcionado. Si la orden no existe, generamos una excepción HTTP 404. De lo contrario, marcamos la orden como pagada con order.paid = True y guardamos la orden en la base de datos.

Ahora, edita el archivo urls.py de la aplicación de pagos y añade el siguiente código resaltado en azul:

PracticaDjango/Payment/urls.py

from django.urls import path
from . import views
from . import webhooks

app_name = 'payment'

urlpatterns = [
    path('process/', views.payment_process, name='process'),
    path('completed/', views.payment_completed, name='completed'),
    path('canceled/', views.payment_canceled, name='canceled'),
    path('webhook/', webhooks.stripe_webhook, name='stripe-webhook'),
]
Con esto hemos importado el módulo "webhooks" y establecido los parámetros de las URLS de la aplicación.


Realizando una prueba de las notificaciones mediante webhooks.


Para probar webhooks, necesitarás instalar Stripe CLI. Stripe CLI es una herramienta para desarrolladores que te permite probar y gestionar tu integración con Stripe directamente desde tu terminal. Encontrarás las instrucciones de instalación en https://stripe.com/docs/stripe-cli?locale=es-ES.

Si estás usando macOS o Linux, puedes instalar Stripe CLI con el gestor de paquetes Homebrew usando el siguiente comando:

bash

brew install stripe/stripe-cli/stripe
Si estás usando Windows, o estás usando macOS o Linux sin el gestor de paquetes Homebrew, descarga la última versión de Stripe CLI para macOS, Linux o Windows desde https://github.com/stripe/stripe-cli/releases/latest y descomprime el archivo. Si estás usando Windows, ejecuta el archivo .exe descomprimido. Puedes ver varias formas de instalación si consultas la documentación o el wiki de github donde está el proyecto. Si lo prefieres y ya que lo tenemos instalado también podemos usar un contenedor de Docker que es lo que yo voy a utilizar.

En cualquier caso, después de instalar Stripe CLI, tienes ejecuta el siguiente comando desde la terminal:

bash

stripe login
o si utilizas Docker lo mismo pero con matices. Cuando ejecutas comandos dentro de un contenedor de Docker, los cambios que se realicen, como la autenticación con "stripe login", generalmente se limitan al ámbito de ese contenedor específico. Esto nos plantea el problema de que al ejecutar este comando la autenticación no se guarde entre diferentes ejecuciones del servidor.

Una solución a este problema, consiste en asegurarnos de que el archivo de configuración que almacena la información de autenticación se conserve entre ejecuciones. Para ello, podemos utilizar un volumen de Docker para montar un directorio local en el contenedor donde Stripe CLI almacene su configuración.

En el shell escribimos los siguientes comandos:

bash

# Crea un volumen para almacenar la configuración de Stripe CLI
sudo docker volume create stripe_config

# Ejecuta el contendor con el volumen montado
sudo docker run -it --rm -v stripe_config:/root stripe/stripe-cli login

Verás la siguiente salida:

Your pairing code is: xxxx-yyyy-zzzz-oooo
This pairing code verifies your authentication with Stripe.
Press Enter to open the browser or visit https://dashboard.stripe.com/
stripecli/confirm_auth?t=....
Pulsa Enter o ve a la dirección que te indica la URL en tu navegador. Verás la siguiente pantalla:

The Stripe CLI pairing screen



Verifica que el código ("pairing code") que se muestra en el terminal es el mismo que aparece en la página web y haz clic en "Permitir el acceso" (Allow access). Verás el siguiente mensaje en pantalla:

The Stripe CLI pairing confirmation

mientras que en el terminal verás una salida parecida a esta:

Your pairing code is: xxxx-yyyy-zzzz-oooo
This pairing code verifies your authentication with Stripe.
Press Enter to open the browser or visit https://dashboard.stripe.com/
stripecli/confirm_auth?t=....
Done! The Stripe CLI is configured for unikgame with account id acct_XXXXXXXXXXXXXXXXX

Please note: this key will expire after 90 days, at which point you'll need 
to re-authenticate.
Ahora ejecuta el siguiente comando desde el shell:

bash

stripe listen --forward-to localhost:8000/payment/webhook/
o si estás utilizando un contenedor de Docker:

bash

# Después de crear el volumen, hacer login, puedes ejecutar el comando stripe listen
sudo docker run --network="host" -it --rm -v stripe_config:/root stripe/stripe-cli listen --forward-to localhost:8000/payment/webhook/

Usamos este comando para decirle a stripe que escuche los eventos y los reenvie a nuestro host local. Usamos el puerto 8000, donde se ejecuta el servidor de desarrollo de Django y la ruta /payment/webhook/ que coincide con el patrón URL de nuetro webhook. Verá que en la terminal aparece los siguiente:

bash

stripe_config:/root stripe/stripe-cli listen --forward-to localhost:8000/payment/webhook/
> Ready! You are using Stripe API Version [2023-10-16]. 
Your webhook signing secret is XXXXXXXXXXXXXXXXXXXX (^C to quit)

En tu navegador abre la siguiente dirección web https://dashboard.stripe.com/test/webhooks. Verás la siguiente pantalla:


The Stripe Webhooks page


NOTA: En un entorno de producción, no necesitaremos el Stripe CLI, lo haremos de otra forma que veremos más adelante.

Ahora abre en tu navegador la dirección http://127.0.0.1:8000, regístrate, añade algunos productos a la cesta y completa el proceso de compra. (recuerda que tienes que tener en ejecución el contenedor de RabbitMQ y el worker de Celery, antes de ejecutar el sevidor de desarrollo de Django)

Comprueba en el shell que se está ejecutando el Stripe Cli y que muetra un aspecto similar a este:

bash

stripe_config:/root stripe/stripe-cli listen --forward-to localhost:8000/payment/webhook/
> Ready! You are using Stripe API Version [2023-10-16]. 
Your webhook signing secret is XXXXXXXXXXXXXXXXXXXX (^C to quit)
024-01-11 18:08:05   --> charge.succeeded [evt_...]
2024-01-11 18:08:05   --> payment_intent.succeeded [evt_...]
2024-01-11 18:08:05  <--  [200] POST http://localhost:8000/payment/webhook/ [evt_...]
2024-01-11 18:08:05   --> payment_intent.created [evt_...]
2024-01-11 18:08:05  <--  [200] POST http://localhost:8000/payment/webhook/ [evt_...]
2024-01-11 18:08:05  <--  [200] POST http://localhost:8000/payment/webhook/ [evt_...]
2024-01-11 18:08:05   --> checkout.session.completed [evt_...]
2024-01-11 18:08:05  <--  [200] POST http://localhost:8000/payment/webhook/ [evt_...]
Puedes ver los diferentes eventos que han sido enviados por Stripe al punto final local de webhook. Estos son:

- payment_intent.created: Se ha creado el intento de pago.
- payment_intent.succeeded: El intento de pago ha tenido éxito.
- charge.succeeded: El cargo asociado al intento de pago ha tenido éxito.
- checkout.session.completed: La sesión de pago ha sido completada. Este es el evento que utilizamos para marcar el pedido como pagado.

El webhook stripe_webhook devuelve una respuesta HTTP 200 OK a todas las solicitudes enviadas por Stripe. Sin embargo, solo procesamos el evento checkout.session.completed para marcar el pedido relacionado con el pago como pagado.

A continuación, abre http://127.0.0.1:8000/admin/orders/order/ en tu navegador. El pedido debería estar marcado como pagado ahora.

Ahora, los pedidos se marcan automáticamente como pagados con las notificaciones de pago de Stripe. A continuación, veremos cómo hacer referencia a los pagos de Stripe en los pedidos de tu tienda.


Haciendo referencia a los pagos de Stripe en los pedidos


Cada pago de Stripe tiene un identificador único. Podemos utilizar el ID de pago para asociar cada pedido con su correspondiente pago de Stripe. Agregaremos un nuevo campo al modelo de Order (Pedido) en la aplicación de pedidos, de modo que podamos hacer referencia al pago relacionado por su ID. Esto nos permitirá vincular cada pedido con la transacción de Stripe correspondiente.

Edita el archivo models.py de la aplicación de pedidos y agrega el siguiente campo al modelo Order. El nuevo campo está resaltado en azul:

PracticaDjango/Orders/models.py

class Order(models.Model):
    #...
    stripe_id = models.CharField(max_length=250, blank=True)
y como siempre que modificamos algo de un archivo models.py hay que realizar las migraciones correspondientes:

$ python manage.py makemigrations
$ python manage.py migrate

Con esto los cambios en el modelo están sincronizados con la base de datos. Ahora será posible almacenar el id_stripe con cada orden.

Edita la funcion stripe_webhook del archivo webhooks.py de la aplicación de pagos y añade las líneas resaltadas en azul:

PracticaDjango/Payment/webhooks.py

#...
@csrf_exempt
def stripe_webhook(request):
    payload = request.body
    sig_header = request.META['HTTP_STRIPE_SIGNATURE']
    event = None
    try:
        event = stripe.Webhook.construct_event(
            payload,
            sig_header,
            settings.STRIPE_WEBHOOK_SECRET)
    except ValueError as e:
        # Invalid payload
        return HttpResponse(status=400)
    except stripe.error.SignatureVerificationError as e:
        # Invalid signature
        return HttpResponse(status=400)
    if event.type == 'checkout.session.completed':
        session = event.data.object
        if session.mode == 'payment' and session.payment_status == 'paid':
            try:
                order = Order.objects.get(id=session.client_reference_id)
            except Order.DoesNotExist:
                return HttpResponse(status=404)
            # marca el pedido como pagado 
            order.paid = True
            # store Stripe payment ID
            order.stripe_id = session.payment_intent
            order.save()
            
    return HttpResponse(status=200)

Con este cambio, al recibir una notificación de webhook para una sesión de pago completada, el ID del pago se almacena en el campo stripe_id del objeto de pedido.

Abre http://127.0.0.1:8000/ en tu navegador, agrega algunos productos al carrito de compras y completa el proceso de pago. Luego, accede a http://127.0.0.1:8000/admin/orders/order/ en tu navegador y haz clic en el último ID de pedido para editarlo. El campo stripe_id debería contener el ID de la intención de pago como se muestra en la siguiente imagen:

The Stripe ID field with the payment intent ID


Bien. Estamos referenciando con éxito los pagos de Stripe en los pedidos. Ahora, podemos agregar los IDs de pago de Stripe a la lista de pedidos en el sitio de administración. También podemos incluir un enlace a cada ID de pago para ver los detalles del pago en el panel de control de Stripe.

Edita el archivo models.py de la aplicación de pedidos y agrega el siguiente código resaltado en azul:

PracticaDjango/Orders/models.py

from django.db import models
from Tienda.models import Producto
from django.conf import settings

# Create your models here.
class Order(models.Model):
   #...
    class Meta:
        #...

    def __str__(self):
        return f"Orden {self.id}"

    def get_total_cost(self):
        return sum(item.get_cost() for item in self.items.all())

    def get_stripe_url(self):
        if not self.stripe_id:
            # no payment associated
            return ''
        if '_test_' in settings.STRIPE_SECRET_KEY:
            # Stripe path for test payments
            path = '/test/'
        else:
            # Stripe path for real payments
            path = '/'
        return f'https://dashboard.stripe.com{path}payments/{self.stripe_id}'

Hemos añadido el nuevo método get_stripe_url() al modelo Order. Este método se utiliza para devolver la URL del panel de control de Stripe asociada al pago de la orden. Si no se almacena ningún ID de pago en el campo stripe_id del objeto Order, se devuelve una cadena vacía. De lo contrario, se devuelve la URL del pago en el panel de control de Stripe. Verificamos si la cadena _test_ está presente en la configuración STRIPE_SECRET_KEY para discriminar el entorno de producción del entorno de prueba.

Los pagos en el entorno de producción siguen el patrón https://dashboard.stripe.com/payments/{id}, mientras que los pagos de prueba siguen el patrón https://dashboard.stripe.com/payments/test/{id}.

Añadamos un enlace a cada objeto Order en la página de visualización de la lista del sitio de administración. Edita el archivo admin.py de la aplicación de pedidos y agrega el siguiente código resaltado en azul:

PracticaDjango/Orders/models.py

from django.contrib import admin
from .models import Order, OrderItem

from django.utils.safestring import mark_safe

# Register your models here.
def order_payment(obj):
    url = obj.get_stripe_url()
    if obj.stripe_id:
        html = f'<a href="{url}" target="_blank">{obj.stripe_id}</a>'
        return mark_safe(html)
    return ''

order_payment.short_description = 'Stripe payment'

class OrderItemInline(admin.TabularInline):
    model = OrderItem
    raw_id_fields = ['product']

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ['id', 'first_name', 'last_name', 'email',
                    'address', 'postal_code', 'city', 'paid', order_payment,
                    'created', 'updated']
    list_filter = ['paid', 'created', 'updated']
    inlines = [OrderItemInline]
#...
La función `order_stripe_payment()` toma un objeto Order como argumento y devuelve un enlace HTML con la URL de pago en Stripe. Django escapa automáticamente la salida HTML por defecto. Utilizamos la función `mark_safe` para evitar el autoescape.

Abre http://127.0.0.1:8000/admin/Orders/order/ en tu navegador. Verás una nueva columna llamada "STRIPE PAYMENT". Encontrarás el ID de pago de Stripe relacionado con la última orden. Si haces clic en el ID de pago, serás redirigido a la URL de pago en Stripe, donde podrás encontrar detalles adicionales sobre el pago.

The Stripe payment ID for an order object in the administration site



NOTA: Evitar el uso de `mark_safe` en la entrada proveniente del usuario, ya que es crucial para prevenir el Cross-Site Scripting (XSS). XSS permite a los atacantes inyectar scripts del lado del cliente en el contenido web visualizado por otros usuarios.

Ir en vivo

Una vez que hayas probado que todo funciona, puedes solicitar una cuenta de Stripe en producción. Cuando estés listo para pasar a producción, recuerda reemplazar tus credenciales de prueba de Stripe con las credenciales en vivo en el archivo settings.py. También deberás agregar un punto final de webhook (endpoint) para tu sitio web alojado en https://dashboard.stripe.com/webhooks en lugar de utilizar la interfaz de línea de comandos de Stripe. 

Puedes encontrar el código de este capítulo en el siguiente enlace de GITHUB.

Apéndice.

Cuando termines el proyecto si quieres borrar todos los rastros relacionados con los contenedores Docker, aquí hay algunos pasos que puedes seguir:

    1.- Detener todos los contenedores en ejecución.
    
    sudo docker stop $(docker ps -q)

    2.- Eliminar todos los contenedores.

    sudo docker rm $(docker ps -a -q)

    3.- Eliminar la imagen de Stripe CLI.

    sudo docker rmi stripe/stripe-cli

    4.- Eliminar volúmenes y redes no utilizados.

    sudo docker image prune -a

    Este comando eliminará todas las imágenes que no estén asociadas a un contenedor en ejecución.


No hay comentarios:

Publicar un comentario