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.


miércoles, 3 de enero de 2024

22. Tareas Asincrónicas

Cuando se recibe una solicitud HTTP, es necesario devolver una respuesta al usuario lo más rápido posible. Cada tarea ejecutada durante el ciclo de solicitud/respuesta se suma al tiempo total de respuesta. Las tareas de larga duración pueden ralentizar seriamente la respuesta del servidor. ¿Cómo podemos devolver una respuesta rápida al usuario mientras completamos tareas que consumen mucho tiempo? 

Podemos hacerlo con la ejecución asíncrona.

Trabajar con tareas asíncronas

Podemos aligerar la carga del ciclo solicitud/respuesta ejecutando ciertas tareas en segundo plano. Por ejemplo, una plataforma de intercambio de videos permite a los usuarios subir videos, pero necesita mucho tiempo para transcodificar los videos subidos. Cuando un usuario sube un video, el sitio podría devolver una respuesta informando que la transcodificación comenzará pronto y empezar a transcodificar el video de manera asíncrona. Otro ejemplo es el envío de correos electrónicos a los usuarios. Si tu sitio envía notificaciones por correo electrónico desde una vista, la conexión del Protocolo Simple de Transferencia de Correo (SMTP) podría fallar o ralentizar la respuesta. Al enviar el correo electrónico de manera asíncrona, evitas bloquear la ejecución del código.

La ejecución asíncrona es especialmente relevante para procesos intensivos en datos, intensivos en recursos y que consumen mucho tiempo, o procesos sujetos a fallos, que podrían requerir una política de reintento.


Trabajadores, colas de mensajes y corredores de mensajes


Mientras tu servidor web procesa solicitudes y devuelve respuestas, necesitas un segundo servidor basado en tareas, llamado trabajador, para procesar las tareas asíncronas. Puede haber uno o varios trabajadores en ejecución, llevando a cabo tareas en segundo plano. Estos trabajadores pueden acceder a la base de datos, procesar archivos, enviar correos electrónicos, etc. Incluso pueden poner en cola futuras tareas. Todo esto mientras se mantiene el servidor web principal libre para procesar las solicitudes HTTP.

Para indicar a los trabajadores qué tareas ejecutar, necesitamos enviar mensajes. Nos comunicamos con los corredores añadiendo mensajes a una cola de mensajes, que básicamente es una estructura de datos de tipo "primero en entrar, primero en salir" (FIFO, por sus siglas en inglés). Cuando un corredor está disponible, toma el primer mensaje de la cola y comienza a ejecutar la tarea correspondiente. Una vez terminada, el corredor toma el siguiente mensaje de la cola y ejecuta la tarea correspondiente. Los corredores quedan inactivos cuando la cola de mensajes está vacía. Cuando se usan múltiples corredores, cada uno toma el primer mensaje disponible en orden cuando están disponibles. La cola asegura que cada corredor reciba solo una tarea a la vez y que ninguna tarea sea procesada por más de un trabajador.

Asynchronous execution using a message queue and workers


Un productor envía un mensaje a la cola, y el/los trabajador(es) consumen los mensajes en orden de llegada; el primer mensaje añadido a la cola de mensajes es el primero en ser procesado por el/los trabajador(es).

Para gestionar la cola de mensajes, necesitamos un corredor de mensajes. El corredor de mensajes se utiliza para traducir mensajes a un protocolo formal de mensajería y gestionar colas de mensajes para múltiples receptores. Proporciona almacenamiento confiable y entrega garantizada de mensajes. El corredor de mensajes nos permite crear colas de mensajes, dirigir mensajes, distribuir mensajes entre trabajadores, etc.


Usando Django con Celery y RabbitMQ


Celery es una cola de tareas distribuida que puede procesar grandes cantidades de mensajes. Usaremos Celery para definir tareas asíncronas como funciones de Python dentro de nuestras aplicaciones de Django. Ejecutaremos trabajadores de Celery que escucharán al corredor de mensajes para obtener nuevos mensajes y procesar tareas asíncronas.

Usando Celery, no solo puedes crear tareas asíncronas fácilmente y permitir que sean ejecutadas por los trabajadores tan pronto como sea posible, sino que también puedes programarlas para que se ejecuten en un momento específico. Puedes encontrar la documentación de Celery en https://docs.celeryq.dev/es/stable/index.html.

Celery se comunica a través de mensajes y requiere un corredor de mensajes para intermediar entre los clientes y los trabajadores. Hay varias opciones para un corredor de mensajes para Celery, incluyendo almacenes de llave/valor como Redis, o un corredor de mensajes real como RabbitMQ.

RabbitMQ es el mediador o corredor de mensajes más ampliamente desplegado. Soporta múltiples protocolos de mensajería, como el Protocolo Avanzado de Cola de Mensajes (AMQP), y es el mediador de mensajes recomendado para Celery. RabbitMQ es liviano, fácil de desplegar y se puede configurar para escalabilidad y alta disponibilidad.

La siguiente Figura muestra cómo utilizaremos Django, Celery y RabbitMQ para ejecutar tareas asíncronas.

Architecture for asynchronous tasks with Django, RabbitMQ, and Celery


Instalando Celery


Vamos a instalar Celery e integrarlo en el proyecto. Instala Celery vía pip utilizando el siguiente comando:

```

pip install celery

```

Puedes encontrar una introducción a Celery en https://docs.celeryq.dev/en/stable/getting-started/introduction.html.


Instalando RabbitMQ


La comunidad de RabbitMQ proporciona una imagen de Docker que facilita mucho desplegar un servidor RabbitMQ con una configuración estándar. 

Docker es una popular plataforma de contenerización de código abierto. Permite a los desarrolladores empaquetar aplicaciones en contenedores, simplificando el proceso de construcción, ejecución, gestión y distribución de aplicaciones.

Primero, descarga e instala Docker para tu sistema operativo. Encontrarás instrucciones para descargar e instalar Docker en Linux, macOS y Windows en https://docs.docker.com/get-docker/.

Después de instalar Docker en tu máquina, puedes fácilmente descargar la imagen de RabbitMQ de Docker ejecutando el siguiente comando desde la terminal (para una distribución basada en Debian en mi ordenador). En este caso necesitaremos utilizar una versión de RabbitMQ que incluya la interfaz de gestión web (RabbitMQ Management Plugin) y que sea ligera:

```

sudo docker pull rabbitmq:management-alpine

```

Esto descargará la imagen de RabbitMQ de Docker a tu máquina local. Puedes encontrar información sobre la imagen oficial de RabbitMQ en Docker en https://hub.docker.com/_/rabbitmq.

Si prefieres instalar RabbitMQ directamente en tu máquina en lugar de utilizar Docker, encontrarás guías detalladas de instalación para diferentes sistemas operativos en https://www.rabbitmq.com/download.html.

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

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

Con este comando, estamos indicando a RabbitMQ que se ejecute en el puerto 5672, y estamos ejecutando su interfaz de usuario de gestión basada en web en el puerto 15672.

Verás una salida que incluye las siguientes líneas:

 Starting broker...

completed with 4 plugins.

... Server startup complete; 4 plugins started.

RabbitMQ está en funcionamiento en el puerto 5672 y listo para recibir mensajes.


Accediendo a la interfaz de gestión de RabbitMQ

Abre http://127.0.0.1:15672/ en tu navegador. Verás la pantalla de inicio de sesión para la interfaz de usuario de gestión de RabbitMQ. Se verá así:

The RabbitMQ management UI login screen


Introduce "guest" tanto en el nombre de usuario (Username) como en la contraseña (Password).

The RabbitMQ management UI dashboard

Este es el usuario administrador predeterminado para RabbitMQ. En esta pantalla, puedes monitorear la actividad actual de RabbitMQ. Puedes ver que hay un nodo en funcionamiento sin conexiones o colas registradas.

Si usas RabbitMQ en un entorno de producción, necesitarás crear un nuevo usuario administrador y eliminar el usuario invitado predeterminado. Puedes hacerlo en la sección de Administración de la interfaz de gestión.

Ahora vamos a agregar Celery al proyecto. Luego, ejecutaremos Celery y probaremos la conexión con RabbitMQ.

Agregar Celery a tu proyecto

Debes proporcionar una configuración para la instancia de Celery. Crea un nuevo archivo junto al archivo settings.py de ''PracticaDjango" y llámalo celery.py. Este archivo contendrá la configuración de Celery para tu proyecto.

Añade el siguiente código a él:


PracticaDjango/PracticaDjango/celery.py

import os
from celery import Celery
# Establece las opciones de configuración para el modulo Celery.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'PracticaDjango.settings')
app = Celery('PracticaDjango')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()

  • Estableces la variable DJANGO_SETTINGS_MODULE para el programa de línea de comandos de Celery.
  • Creas una instancia de la aplicación con app = Celery('PracticaDjango').
  • Cargas cualquier configuración personalizada desde la configuración de tu proyecto utilizando el método config_from_object(). El atributo namespace especifica el prefijo que tendrán las configuraciones relacionadas con Celery en tu archivo settings.py. Al establecer el namespace CELERY, todas las configuraciones de Celery deben incluir el prefijo CELERY_ en su nombre (por ejemplo, CELERY_BROKER_URL).
  • Finalmente, le indicas a Celery que descubra automáticamente tareas asíncronas para tus aplicaciones. Celery buscará un archivo tasks.py en cada directorio de aplicación agregado a INSTALLED_APPS para cargar tareas asíncronas definidas en él.

Necesitas importar el módulo celery en el archivo __init__.py de tu proyecto para asegurarte de que se cargue cuando Django se inicie.

Edita el archivo PracticaDjango/init.py y agrega el siguiente código:

PracticaDjango/PracticaDjango/__init__.py

# import celery
from .celery import app as celery_app

__all__ = ['celery_app']
Con esto ya tenemos añadido Celery a nuestro proyecto de Django, y podemos empezar a utilizarlo.


Ejecutando un trabajador de Celery


Un trabajador de Celery es un proceso que maneja funciones como enviar/recibir mensajes en colas, registrar tareas, finalizar tareas bloqueadas, seguir el estado, etc. Una instancia de trabajador puede consumir cualquier cantidad de colas de mensajes.

Abre otra terminal y inicia un trabajador de Celery desde el directorio de tu proyecto, utilizando el siguiente comando:

celery -A PracticaDjango worker -l info

El trabajador de Celery está ahora en ejecución y listo para procesar tareas. Verifiquemos si hay una conexión entre Celery y RabbitMQ.

Abre http://127.0.0.1:15672/ en tu navegador para acceder a la interfaz de gestión de RabbitMQ. Ahora verás un gráfico bajo 'Mensajes en cola' (Queued messages) y otro gráfico bajo 'Tasas de mensajes' (message rates), similar a la siguiente imagen:

The RabbitMQ management dashboard displaying connections and queues

Obviamente, no hay mensajes en cola ya que aún no hemos enviado ningún mensaje a la cola de mensajes. El gráfico bajo 'Tasas de mensajes' debería actualizarse cada cinco segundos; puedes ver la frecuencia de actualización en la esquina superior derecha de la pantalla. Esta vez, tanto 'Conexiones' como 'Colas' deberían mostrar un número mayor que cero.

Ahora podemos comenzar a programar tareas asíncronas.


Agregar tareas asíncronas a tu aplicación


Enviemos un correo electrónico de confirmación al usuario cada vez que se realice un pedido en la tienda en línea. Implementaremos el envío del correo electrónico en una función de Python y la registraremos como una tarea con Celery. Luego, la agregaremos a la vista order_create para ejecutar la tarea de forma asíncrona.

La configuración CELERY_ALWAYS_EAGER te permite ejecutar tareas localmente de manera sincrónica, en lugar de enviarlas a la cola. Esto es útil para ejecutar pruebas unitarias o ejecutar la aplicación en tu entorno local sin ejecutar Celery.


Agregar tareas asíncronas a tu aplicación


Enviemos un correo electrónico de confirmación al usuario cada vez que se realice un pedido en la tienda en línea. Implementaremos el envío del correo electrónico en una función de Python y la registraremos como una tarea con Celery. Luego, la agregaremos a la vista order_create para ejecutar la tarea de forma asíncrona.

Cuando se ejecuta la vista order_create, Celery enviará el mensaje a una cola de mensajes gestionada por RabbitMQ y luego un intermediario de Celery ejecutará la tarea asíncrona que definimos con una función de Python.

La convención para facilitar el descubrimiento de tareas por parte de Celery es definir tareas asíncronas para tu aplicación en un módulo de tareas dentro del directorio de la aplicación.

Crea un nuevo archivo dentro de la aplicación de pedidos (Orders) y nómbralo tasks.py. Este es el lugar donde Celery buscará tareas asíncronas. Agrega el siguiente código a él:

PracticaBlog/PracticaDjango/Orders/tasks.py

from celery import shared_task
from django.core.mail import send_mail
from .models import Order

@shared_task
def order_created(order_id):
    """
    Función para enviar una notificación por correo electrónico cuando se realiza un pedido
    correctamente.
    """
    order = Order.objects.get(id=order_id)
    subject = f'Orden nº. {order.id}'
    message = f'Estimado {order.first_name},\n\n' \
              f'Su pedido ha sido realizado correctamente.' \
              f'Su ID del pedido es {order.id}.'
    mail_sent = send_mail(subject,
                          message,
                          'admin@myshop.com',
                          [order.email])
    return mail_sent

Hemos definido la función order_created utilizando el decorador @shared_task. Como puedes ver, una tarea de Celery es simplemente una función de Python decorada con @shared_task. La función order_created recibe un parámetro order_id. Siempre es recomendable pasar solo identificadores a las funciones de tarea y recuperar objetos de la base de datos cuando se ejecuta la tarea. De esta manera, evitamos acceder a información desactualizada, ya que los datos en la base de datos podrían haber cambiado mientras la tarea estaba en cola. Hemos utilizado la función send_mail() proporcionada por Django para enviar una notificación por correo electrónico al usuario que realizó el pedido.

Ya vimos cómo configurar Django para usar tu servidor SMTP en el Capítulo 10, Django. Formulario de contacto y envío de email con datos. Variables de Entorno. Si no quieres configurar los ajustes de correo electrónico, puedes indicarle a Django que escriba correos electrónicos en la consola agregando la siguiente configuración al archivo settings.py:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend

Utiliza tareas asíncronas no solo para procesos que consumen mucho tiempo, sino también para otros procesos que no requieren tanto tiempo para ejecutarse pero que están sujetos a fallos de conexión o requieren una política de reintentos.

Ahora debes agregar la tarea a tu vista order_create. Edita el archivo views.py de la aplicación de órdenes (Orders), importa la tarea y llama a la tarea asíncrona order_created después de vaciar el carrito, de la siguiente manera:

PracticaBlog/PracticaDjango/Orders/tasks.py

#...
# Crea una tarea asincronica al finalizar la orden
from .tasks import order_created


# 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)
            return render(request, "Orders/order/created.html", {"order": order})
    else:
        form = OrderCreateForm()
    return render(request, "Orders/order/create.html", {"cart": carro, "form": form})

Llamas al método delay() de la tarea para ejecutarla de manera asíncrona. La tarea se añadirá a la cola de mensajes y será ejecutada por el trabajador de Celery tan pronto como sea posible.

Asegúrate de que RabbitMQ esté en funcionamiento. Luego, detén el proceso del trabajador de Celery y vuelve a iniciarlo con el siguiente comando:

celery -A PracticaDjango worker -l info

El trabajador de Celery ha registrado ahora la tarea. En otra terminal, inicia el servidor de desarrollo desde el directorio del proyecto con el siguiente comando:

python manage.py runserver

Abre http://127.0.0.1:8000/ en tu navegador, añade algunos productos a tu carrito de compras y completa un pedido. En la terminal donde iniciaste el trabajador de Celery, verás una salida similar a la siguiente:

[2024-01-02 20:00:07,087: WARNING/ForkPoolWorker-2] Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: =?utf-8?b?T3JkZW4gbsK6LiAz?=
From: admin@myshop.com
To: perico@gamil.com
Date: Tue, 02 Jan 2024 19:00:07 -0000
Message-ID: <170422200708.12830.17805739684368165117@machine>

Estimado fdfdf,

Su pedido ha sido realizado correctamente.Su ID del pedido es 3.

La tarea order_created ha sido ejecutada y se ha enviado una notificación por correo electrónico para el pedido. Si estás utilizando el backend de correo electrónico console.EmailBackend, no se enviará ningún correo electrónico, pero deberías ver el texto renderizado del correo electrónico en la salida de la consola.


Monitorizando Celery con Flower


Además de la interfaz de gestión de RabbitMQ, puedes usar otras herramientas para monitorear las tareas asíncronas que se ejecutan con Celery. Flower es una herramienta basada en web para monitorear Celery.

Instala Flower usando el siguiente comando:

pip install flower

Una vez instalado, puedes iniciar Flower ejecutando el siguiente comando en una nueva terminal desde el directorio de tu proyecto:

celery -A PracticaDjango flower

Abre http://localhost:5555/dashboard en tu navegador. Podrás ver los trabajadores activos de Celery y estadísticas de tareas asíncronas. La pantalla debería parecerse a esta:


página de inicio de flower


Verás un trabajador activo, cuyo nombre comienza con celery@ y cuyo estado es En línea.

Haz clic en el nombre del trabajador y luego haz clic en la pestaña Colas (Queues). Verás la siguiente pantalla:

pestaña de colas de flowers


En este lugar, puedes ver la cola activa llamada celery. Esta es la cola activa que está conectada al broker de mensajes.

Si vuelves a la pantalla inicial y haces clic en el número total de tareas procesadas, verás la siguiente pantalla:

pestaña de mensajes procesados

Aquí puedes ver las tareas que han sido procesadas y la cantidad de veces que han sido ejecutadas. Deberías ver la tarea order_created y el número total de veces que ha sido ejecutada. Este número podría variar dependiendo de cuántos pedidos hayas realizado.

Abre http://localhost:8000/ en tu navegador. Agrega algunos artículos al carrito y luego completa el proceso de compra.

Abre http://localhost:5555/dashboard en tu navegador. Flower ha registrado la tarea como procesada. Ahora deberías ver al menos un 1 bajo "Procesado" y un 1 bajo "Completado" también.


Si al refrescar Flower te da este error:

FLOWER_UNAUTHENTICATED_API environment variable is required to enable API without authentication

Si estás en un entorno Unix/Linux/macOS, puedes establecer la variable de entorno antes de ejecutar Flower en la línea de comandos: export FLOWER_UNAUTHENTICATED_API=true celery -A PracticaDjango flower

Si estás en Windows, puedes establecer la variable de entorno y luego ejecutar Flower: set FLOWER_UNAUTHENTICATED_API=true celery -A myshop flower

Al configurar FLOWER_UNAUTHENTICATED_API en true, habilitarás el acceso a la API de Flower sin autenticación. Esto solucionará el error que estás viendo al intentar refrescar Flower en tu navegador web.

Puedes encontrar más información sobre este programa en https://flower.readthedocs.io/.

Puedes encontrar el código de es Post en el siguiente enlace de GITHUB.