Adding Payment to Django app
Tags: django, stripe, payment, webdev
Published: 2024-07-01 06:29:36

So, you've decided to build your eCommerce or SaaS platform. Congratulation! But now comes the big question: how will you collect payments from your customers? 

Having a solid and secure payment system is a must for any online business. This is where adding a [payment gateway](https://templates.foxcraft.tech/blog/b/what-are-payment-gateways) to your Django app comes in. 

Many payment gateways, such as [Stripe](https://stripe.com), [Paypal](https://paypal.com), have made this easier to collect payments. All you need to do is integrate their API into your application, and they handle the rest—securely collecting payments, ensuring compliance, and more.

In this post, we'll walk you through how to set up a payment system in your Django application using Stripe, making sure your transactions are smooth and secure for your customers, and straightforward for you.

First make sure to create a [stripe account](https://stripe.com), we'll only need a test account for this tutorial

> **NOTE**
> Use only stripe test account for testing purposes. Don't send money to youself in production as this violates stripe policy

## Overview of Stripe and Django
1. First we make a call to the stripe API, and redirect the customer to a Stripe secure form, to collect Payment.
2. If the charge was successful, the stripe form will redirect to a success page in our website, otherwise a failed page.
3. We listen to webhook events to confirm the transaction.

## Installing stripe to Django

We'll focus on building a small SaaS Payment subscription, even if you are building something else, the below steps will remain the same

You can check the implementation in the [Django Saas Boilerplate](https://github.com/PaulleDemon/Django-SAAS-Boilerplate)

Install dependency
```
pip install stripe
```
Optain stripe test secret key from dashboard and click on developers

![Stripe key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wxomt37n7wbwawnvfpxk.png)

Now under webhook click on test in local environment, you'll find the stripe webhook key as well
![Webhook key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o0oytalrelejqwpqxsic.png)


now add it to settings.py
```py
INSTALLED_APPS = [
]
.
.
.
STRIPE_API_KEY = "sk_test_"
STRIPE_WEBHOOK_KEY = "whsec_"
.
.
.
```

Let's create an app and call it transaction, where everything related to the payments is added
```
python manage.py startapp transaction
```
Add a model called Plan (subscription plan) inside transaction/models.py
```py
from decimal import Decimal

class Plan(models.Model):
    name = models.CharField()
    description = models.CharField(max_length=150) # small description of the plan

    price = models.DecimalField(max_digits=9, decimal_places=2, default="0.0")

    datetime = models.DateTimeField(auto_now=True) # created datetime
    def get_total_cents(self):
        # converts  dollar to cents.
        integer = int(self.price)
        decimal = int((self.price % 1)*100)

        return (integer * 100) + decimal
        return dollar_to_cents(self.price)

```
Now lets add a `Transaction` model that records all the transaction initiated, status and more.
```py
class SUBSCRIPTION_STATUS(models.IntegerChoices):

    INACTIVE  = (0, 'inactive')
    ACTIVE  = (1, 'active')
    CANCELLED = (2, 'cancelled')

class PAYMENT_STATUS(models.IntegerChoices):
    
    UNPAID = (0, 'unpaid')
    PAID = (1, 'paid')

class Transaction(BasePayment):

    user = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) # foreign key to user model
    plan = models.ForeignKey(Plan, null=True, blank=True, on_delete=models.SET_NULL) # foreign key to subscription plan
    
    total = models.DecimalField(max_digits=9, decimal_places=2, default="0.0")
    status = models.PositiveSmallIntegerField(choices=PAYMENT_STATUS.choices, default=PAYMENT_STATUS.UNPAID)

    created = models.DateTimeField(auto_now_add=True) 
    modified = models.DateTimeField(auto_now=True)
    
    transaction_id = models.CharField(max_length=255, blank=True)
    subscription_id = models.CharField(max_length=255, null=True, blank=True) # creating stripe subscription
    customer_id = models.CharField(max_length=255, null=True, blank=True) # for creating stripe subscription

    subscription_status = models.PositiveSmallIntegerField(choices=SUBSCRIPTION_STATUS.choices, default=SUBSCRIPTION_STATUS.INACTIVE)
```
Now go to view and lets start by listing out plan/product
```py
def pricing(request):

    plans = Plan.objects.all()

    return render(request, "payment/pricing.html", {
        'plans': plans
    })

def payment_success(request):

    return render(request, "payment/success.html")


def payment_failed(request):

    return render(request, "payment/failure.html")
```
Now the payment/pricing.html
```html
{% extends "base.html" %}

{% block title %}Pricing{% endblock title %}
{% block description %}Pricing for the SAAS{% endblock description %}

{% block content %}
<div>
    <h1>Plans and pricing</h1>
    <div>
        This is a sample pricing, the purchase won't be made.
    </div>
</div>

<section >
    {% for plan in plans %}
        <form action="{% url "create-payment" %}" method="POST">
            {% csrf_token %}
            <div>
                <h2 >{{plan.name}}</h2>
                <h3 >$ {{plan.price|stringformat:'d'}}</h3>
                
                <input type="hidden" name="plan" value="{{plan.id}}">
                <button type="submit"">
                    Get started
                </button>
            </div>
        </form>
    {% endfor %}
</section>

{% endblock content %}
```
Now add a success page and a failure page, so if the transaction is successful it can be redirected to success page.

success.html
```html
{% extends "base.html" %}
{% load static %}

{% block content %}
<div class="">

    <div class="">
        
        <i class="bi bi-check-circle tw-text-9xl tw-text-green-600"></i>
        <div class="">Success</div>
    </div>

</div>
{% endblock content %}
```
Similarly failure.html page
```html
{% extends "base.html" %}
{% load static %}

{% block content %}
<div class="">

    <div class="">
        <i class="bi bi-x-circle tw-text-9xl tw-text-red-600"></i>
        <div class="tw-text-3xl">Payment failed</div>
    </div>

</div>
{% endblock content %}
``` 

Now lets start creating checkout view. So go back to views.py and add the following view.

```py
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods

stripe.api_key = settings.STRIPE_API_KEY


@login_required
@require_http_methods(['POST'])
def create_payment(request):

    plan = request.POST.get("plan")

    try:
        plan = Plan.objects.get(id=int(plan))
    
    except (Plan.DoesNotExist, ValueError):
        return render(request, "404.html", status=404)

    amount = plan.price 

    payment = Payment.objects.create(
        total=amount,
        billing_email=request.user.email,
        user=request.user,
        plan=plan
    )

    pay_data = {
                'price_data' :{
                        'product_data': {
                            'name': f'{plan.name}',
                            'description': plan.description or '',
                            },
                        'unit_amount': plan.get_total_cents(), # get the currency in the smallest unit
                        'currency': 'usd', # set this to your currency
                        'recurring': {'interval': 'month'} # refer: https ://docs.stripe.com/api/checkout/sessions/create?lang=cli#create_checkout_session-line_items-price_data-recurring
                    },
                'quantity' : 1
            }


    checkout_session = stripe.checkout.Session.create(
            line_items=[
                pay_data
            ],
            mode='subscription',
            success_url=request.build_absolute_uri(payment.get_success_url()),
            cancel_url=request.build_absolute_uri(payment.get_failure_url()),
            customer=None,
            client_reference_id=request.user.id,
            customer_email=request.user.email,
            metadata={
                'customer': request.user.id,
                'payment_id': payment.id
            }
        )
    
    payment.transaction_id = checkout_session.id
    payment.save()

    return redirect(checkout_session.url) # redirect the user to stripe secure form
```

Now a customer might take time to fill in their details and submit the form to stripe, once its submitted stripe sends events via [webhook](https://www.redhat.com/en/topics/automation/what-is-a-webhook)

A webhook is a simple view, that accepts post request and returns a 200 ok status.

so lets add webhook to views.py
```py
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

@require_POST
@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, STRIPE_WEBHOOK_SECRET
        )
    except ValueError as e:
        # Invalid payload
        return JsonResponse({'error': str(e)}, status=400)
    except stripe.error.SignatureVerificationError as e:
        # Invalid signature
        return JsonResponse({'error': str(e)}, status=400)

    # print("Event: ", event)

    data = event['data']['object']

    # Handle the even
    if event['type'] == 'checkout.session.completed':
        subscription = Transaction.objects.get(transaction_id=data['id'])
        subscription.status = PAYMENT_STATUS.PAID
        subscription.subscription_status = SUBSCRIPTION_STATUS.ACTIVE
        subscription.subscription_id = data['subscription']
        subscription.customer_id = data['customer']
        subscription.save()

    if event['type'] == 'checkout.session.expired':
        subscription = Transaction.objects.get(transaction_id=data['id'])
        subscription.status = PAYMENT_STATUS.UNPAID
        subscription.save()

    elif event['type'] == 'customer.subscription.deleted':
        # Subscription deleted
        subscription = Transaction.objects.get(stripe_subscription_id=event['data']['object']['subscription'])
        subscription.subscription_status = SUBSCRIPTION_STATUS.CANCELLED
        subscription.save()

    elif event['type'] == "charge.failed":
        pass

    elif event['type'] == 'invoice.payment_succeeded':
        # Payment succeeded
        pass
    
    elif event['type'] == 'invoice.payment_failed':
        # Payment succeeded
        pass

    elif event['type'] == 'customer.subscription.trial_will_end':
        # print('Subscription trial will end')
        pass
    
    elif event['type'] == 'customer.subscription.created':
        # print('Subscription created %s', event.id)
        pass
    
    elif event['type'] == 'customer.subscription.updated':
        # print('Subscription created %s', event.id)
        pass

    return JsonResponse({'status': 'success'}, status=200)

```
You can read more about webhook events in stripe's page: [Stripe events](https://docs.stripe.com/api/events)

Now add your paths to your urls.py
```py
from django.urls import path

from .views import (create_payment, pricing, stripe_webhook,
                        payment_failed, payment_success)

urlpatterns = [
    path('pricing/', pricing, name='pricing'),
    path('create-payment/', create_payment, name='create-payment'),
    

    path('payment/failed/', payment_failed, name='payment-failed'),
    path('payment/success/', payment_success, name='payment-success'),
    path('stripe/webhook/', stripe_webhook, name='webhook'),
]
```

## Testing stripe webhook locally

To test stripe webhook locally, you'll need to install [stripe cli](https://docs.stripe.com/stripe-cli)
Once installed, login via
```cmd
stripe login
```
Now forward the events to localhost
```
stripe listen --forward-to localhost:8000/stripe/webhooks/
```
That's it now you can listen to webhook events locally.

You can checkout the source code at: https://github.com/PaulleDemon/Django-SAAS-Boilerplate

If you have question, drop a comment. Found it helpful? share this article.