Платежи — место, где проект Django встречается с реальными деньгами, поэтому сценарий должен быть скучным и предсказуемым. В Stripe Checkout вы никогда не видите данные карты: ваш view создаёт Checkout Session, клиент платит на странице Stripe, а результат Stripe сообщает через вебхук. В этой теме мы делаем сторону Django: модели Order и StripeEvent, view для Checkout, webhook-view с csrf_exempt и проверкой подписи, идемпотентную обработку, тесты и обзор dj-stripe. Общие понятия Stripe разобраны в уроках Stripe Checkout и Вебхуки Stripe. Везде тестовый режим и ключи-заглушки.

Сценарий

Браузер, Django, Stripe и таблица Order во время платежа
Браузер, Django, Stripe и таблица Order во время платежа
  1. Браузер отправляет POST на ваш view Checkout.
  2. View создаёт Order (pending) и Session в Stripe, затем делает редирект на session.url.
  3. Клиент платит на странице Stripe.
  4. Stripe отправляет POST checkout.session.completed на ваш вебхук; только он переводит заказ в paid.
ЧастьГде живёт
Цена и товарваша база (Product), никогда не запрос
Статус заказаOrder.status, меняется только вебхуком
Секретный ключ, секрет вебхукапеременные окружения, читаются в settings.py
Защита от дублейStripeEvent.event_id с unique=True

💡 Страница успеха может написать «спасибо, подтверждаем оплату» и прочитать статус заказа. Помечать заказ оплаченным она не должна.

Настройки и модели

SDK ставится командой pip install stripe, ключи читаются из окружения (настройки и окружения):

python
# settings.py
import os

STRIPE_SECRET_KEY = os.environ["STRIPE_SECRET_KEY"]  # sk_test_...
STRIPE_WEBHOOK_SECRET = os.environ["STRIPE_WEBHOOK_SECRET"]  # whsec_...

Order хранит сумму целым числом в минимальных единицах (центах), а StripeEvent помнит каждое уже обработанное событие вебхука:

python
from django.conf import settings
from django.db import models

class Order(models.Model):
    class Status(models.TextChoices):
        PENDING = "pending", "Pending"
        PAID = "paid", "Paid"
        CANCELED = "canceled", "Canceled"

    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
    amount = models.PositiveIntegerField(help_text="minor units (cents)")
    currency = models.CharField(max_length=3, default="usd")
    status = models.CharField(
        max_length=10, choices=Status.choices, default=Status.PENDING
    )
    stripe_session_id = models.CharField(
        max_length=255, blank=True, db_index=True
    )
    created_at = models.DateTimeField(auto_now_add=True)

class StripeEvent(models.Model):
    event_id = models.CharField(max_length=255, unique=True)
    type = models.CharField(max_length=100)
    received_at = models.DateTimeField(auto_now_add=True)
Состояния заказа: pending, paid, canceled и таблица дедупликации StripeEvent
Состояния заказа: pending, paid, canceled и таблица дедупликации StripeEvent

⚠️ Не используйте FloatField для денег. Берите целые центы (как здесь) или DecimalField.

View для Checkout