# Payment and Subscription

# Introduction

The payment feature is built on Stripe, which is the most popular solution for now.

# Project Structure

If you check django_app directory.

django_app
├── demo_stripe
├── payment

Notes:

  1. demo_stripe contains demo code, which show you how to integrate dj-stripe
  2. payment is the Django app which used by SaaS Hammer live site, you can ignore it.

# ENV

# stripe
STRIPE_LIVE_MODE=0
STRIPE_TEST_SECRET_KEY=sk_test_xxxx
STRIPE_TEST_PUBLIC_KEY=pk_test_xxxx
STRIPE_LIVE_SECRET_KEY=sk_live_xxxx
STRIPE_LIVE_PUBLIC_KEY=pk_live_xxxx
DJSTRIPE_WEBHOOK_SECRET=whsec_xxxx

I will talk about env variables with more details in a bit.

# dj-stripe

dj-stripe (opens new window) is a Django package which can let you work with Django ORM instead of calling Stripe API directly.

# Example 1

Create Stripe Customer from Django User.

You can create Customer using stripe API.

response = stripe.Customer.create(
    email=self.request.user.email,
    metadata=metadata,
)

Or you can do it via dj-stripe

from djstripe.models import Customer

Customer.get_or_create(
    subscriber=request.user,
)

Notes:

  1. The dj-stripe will help send request to Stripe API to create Customer, and also create Customer instance in the database.

# Example 2

from djstripe.settings import djstripe_settings


class PaymentsContextMixin:

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context.update(
            {
                "STRIPE_PUBLIC_KEY": djstripe_settings.STRIPE_PUBLIC_KEY,
            }
        )
        return context

Notes:

  1. dj-stripe can help us expose stripe public key according to the STRIPE_LIVE_MODE
  2. So we can switch between stripe test mode and stripe live mode by just changing STRIPE_LIVE_MODE env variable.

# Setup

This section will show you how to config Stripe and make it run.

To avoid confusion, please delete stripe testing data first, you can do it on stripe developers page in test mode.

# install stripe CLI https://stripe.com/docs/stripe-cli

$ stripe login
$ stripe listen --forward-to 127.0.0.1:8000/stripe/webhook/

> Ready! You are using Stripe API Version [2020-08-27]. Your webhook signing secret is XXXXXX

The above command will redirect stripe webhook events to local dev server, I will talk about it with more details in a bit.

And then, add Stripe relevant env variable in the env file, you can get DJSTRIPE_WEBHOOK_SECRET from the stripe listen --forward-to command terminal output.

# stripe
STRIPE_LIVE_MODE=0
STRIPE_TEST_SECRET_KEY=sk_test_xxxx
STRIPE_TEST_PUBLIC_KEY=pk_test_xxxx
STRIPE_LIVE_SECRET_KEY=sk_live_xxxx
STRIPE_LIVE_PUBLIC_KEY=pk_live_xxxx
DJSTRIPE_WEBHOOK_SECRET=whsec_xxxx
# create dj-stripe tables
$ python manage.py migrate

# test dj-stripe
$ python manage.py djstripe_sync_models

Since we already deleted Stripe testing data, the djstripe_sync_models will not create new records.

Let's run command to create Product, Price in Stripe test mode.

$ python manage.py bootstrap_demo_products

If you check Products page on Stripe dashboard, then you would be able to see them.

Basic plan
    $10/month
    $100/year

Starter plan
    $20/month
    $200/year

Pro plan
    $30/month
    $300/year
# sync specific models
$ python manage.py djstripe_sync_models Product Price
$ python manage.py runserver
# check on http://127.0.0.1:8000/admin/djstripe/product/

If you can see the data, then it means the demo_stripe is setup successfully.

# Manual Test

Before the test, please make sure the stripe listen --forward-to command is running, or some features might not work as expected.

Please test Stripe Checkout Session on http://127.0.0.1:8000/demo-stripe/item-checkout-session/ (opens new window)

Please test Subscription on http://127.0.0.1:8000/demo-stripe/subscription-payment-intent-lander/ (opens new window)

Stripe Subscription

# Webhook

A webhook enables Stripe to push real-time notifications to your app. Stripe uses HTTPS to send these notifications to your app as a JSON payload. You can then use these notifications to execute actions in your backend systems.

# Solution 1: dj-stripe

It is recommended to write webhook handler with dj-stripe

import logging
from djstripe import webhooks

LOGGER = logging.getLogger(__name__)

@webhooks.handler("checkout.session.completed")
def checkout_complete_webhook_handler(event):
    """
    We can confirm payment in the webhook handler
    """
    session = event.data["object"]
    customer_details = session["customer_details"]
    LOGGER.info(customer_details)

# Solution 2

@csrf_exempt
def webhook_view(request):
    endpoint_secret = settings.DJSTRIPE_WEBHOOK_SECRET
    payload = request.body
    sig_header = request.META["HTTP_STRIPE_SIGNATURE"]

    try:
        event = stripe.Webhook.construct_event(
            payload,
            sig_header,
            endpoint_secret,
        )
    except ValueError as e:
        # Invalid payload
        LOGGER.exception(e)
        return HttpResponse(status=400)
    except stripe.error.SignatureVerificationError as e:
        # Invalid signature
        LOGGER.exception(e)
        return HttpResponse(status=400)

    # Handle the checkout.session.completed event
    if event["type"] == "checkout.session.completed":
        pass

    return HttpResponse(status=200)

# Frontend

  1. For Stripe Checkout Session, please check frontend/src/controllers/checkout_controller.js
  2. For Stripe Payment element, please check frontend/src/controllers/payment_controller.js
  3. For Price Toggle, please check frontend/src/controllers/pricing_controller.js

# Subscription

We can let dj-stripe to process below Stripe events:

  1. customer.subscription.updated
  2. customer.subscription.deleted

Once dj-stripe receive the event, it will automatically sync relevant data to database.

class SubscriptionMixin(PaymentsContextMixin):
    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(**kwargs)
        context["customer"], _created = Customer.get_or_create(
            subscriber=self.request.user,
            livemode=djstripe_settings.STRIPE_LIVE_MODE,
        )
        context["subscription"] = context["customer"].subscription
        return context

So we can access user's valid subscription from the database, which is convenient.

# Customer Portal

To let user manage Subscription and payment method, we can integrate the customer portal

portal_session = stripe.billing_portal.Session.create(
    customer=context["customer"].id,
    return_url=...,
)

# Permission

# Mixin

We can use ValidSubscriptionRequiredMixin

class SubscriptionPermissionCheckView(
    LoginRequiredMixin, ValidSubscriptionRequiredMixin, TemplateView
):
    limit_to_products = [PRO_PRODUCT, STARTER_PRODUCT]
    redirect_url = reverse_lazy("demo-stripe:subscription_payment_intent_lander")

    template_name = "demo_stripe/subscription_permission_check.html"


subscription_permission_check_view = SubscriptionPermissionCheckView.as_view()

# Group

Another solution is to use Django Group.

For example, if user subscribe to Pro plan, we can add the user to Pro group.

And then check permission using user.has_perm()