How to Integrate Paystack in Django Rest Framework

How to Integrate Paystack in Django Rest Framework

Introduction

Paystack is a payment gateway that helps individuals and businesses in Africa to accept payments from anywhere in the world. In this tutorial, I will be creating a wallet system API using Django Rest Framework that allows users to deposit funds in a wallet using Paystack as a third-party API for payments.

Prerequisites

You will need the Knowledge of the following to flow well with the tutorial:

  • A good understanding of Python
  • Knowledge of Django and Django Rest Framework
  • Basic knowledge of Postman to test endpoints
  • Usage of the Python request library
  • A Paystack account

Now, let's dive into the tutorial, all codes can be found on Github in this repository.

Project Setup

Create a Folder and activate a virtual environment in it.

env.png

Install Django, Djangorestframework, and the requests library in your environment using pipenv install django, pipenv install djangorestframework and pipenv install requests respectively..

install.png

Create a new Django project, go to the project directory and create a new app

$ django-admin startproject wallet

$ cd wallet

$ python manage.py startapp walletApp

Add the installed app and rest_framework in settings.py

INSTALLED_APPS = [
             .......
             'walletApp',
             'rest_framework',
             'rest_framework.authtoken'

]

Ensure the server is up and running by running the command python manage.py runserver

Run all current migrations by running the command python manage.py migrate

Create a urls.py file for the app and include the file in the project urls.py

from django.contrib import admin
from django.urls import path, include 

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('walletApp.urls')),
]

Then, include the code below in the app's urls.py

from django.urls import path


urlpatterns = [

]

Your project files structure should look like this:

files.png

Creating models

We will be creating Wallet and WalletTransaction models to store the user and transactions info.

Add the following codes in your models.py

from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone

# Create your models here.


class Wallet(models.Model):
    user = models.OneToOneField(
        User, null=True, on_delete=models.CASCADE)
    currency = models.CharField(max_length=50, default='NGN')
    created_at = models.DateTimeField(default=timezone.now, null=True)


    def __str__(self):
        return self.user.__str__()

class WalletTransaction(models.Model):

    TRANSACTION_TYPES = (
        ('deposit', 'deposit'),
        ('transfer', 'transfer'),
        ('withdraw', 'withdraw'),
    )
    wallet = models.ForeignKey(Wallet, null=True, on_delete=models.CASCADE)
    transaction_type = models.CharField(
        max_length=200, null=True,  choices=TRANSACTION_TYPES)
    amount = models.DecimalField(max_digits=100, null=True, decimal_places=2)
    timestamp = models.DateTimeField(default=timezone.now, null=True)
    status = models.CharField(max_length=100, default="pending")
    paystack_payment_reference = models.CharField(max_length=100, default='', blank=True)

    def __str__(self):
        return self.wallet.user.__str__()

Note: We are using the default User model provided by Django.

After the creation of the models run python manage.py makemigrations and python manage.py migrate to migrate the models into the database. Then register the models on the admin page by adding the codes below in the admin.py file.

from django.contrib import admin
from .models import Wallet, WalletTransaction
# Register your models here.

admin.site.register(Wallet)
admin.site.register(WalletTransaction)

Implementing Login and Register Functionality

We will be implementing authentication using the Rest framework Default Token Authentication. We will also be generating auth tokens for each registered user using rest_framework.authtoken.

Add the following to the settings.py file:

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
),
    'DEFAULT_PERMISSION_CLASSES': (
    'rest_framework.permissions.IsAuthenticated',
)
}

Create a serializers.py file in the app directory Add the codes below:

from rest_framework import serializers
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import User



class UserSerializer(serializers.ModelSerializer):
    """
    Serializer to validate and create a new user
    """
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'password']
        extra_kwargs = {'password': {'write_only': True}}

    def create(self, validated_data):
        user = User(
            username=validated_data['username'],
            email=validated_data['email']
        )
        user.set_password(validated_data['password'])
        user.save()
        Token.objects.create(user=user)
        return user

in views.py, add the codes below for the Login and Register Endpoints

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.contrib.auth import authenticate

from .serializers import UserSerializer


class Login(APIView):
    permission_classes = ()

    def post(self, request):
        username = request.data.get("username")
        password = request.data.get("password")
        user = authenticate(username=username, password=password)
        if user:
            return Response({"token": user.auth_token.key, "username": username})
        else:
            return Response({"error": "Wrong Credentials"}, status=status.HTTP_400_BAD_REQUEST)


class Register(APIView):
    authentication_classes = ()
    permission_classes = ()

    def post(self, request):

        serializer = UserSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()

        return Response(serializer.data)

Then, create a URL path for the views in the app's urls.py file

from django.urls import path
from .views import Login, Register


urlpatterns = [
    path('register/', Register.as_view()),
    path('login/', Login.as_view()),
]

Run the server and let's use Postman to test the endpoints.

I will be adding a new user running localhost:8000/register/

post1.png

using the newly created user credentials, I will be logging in the user with localhost:8000/login/

post2.png

Implementing the Deposit Funds endpoint

We will be implementing the deposit endpoint which will allow users to deposit funds using Paystack. A Paystack account is needed and the secret_key can be gotten from Paystack Dashoard.

stack.png

Note: The dashboard is in test mode which means any transactions made will be void.

in serializers.py, add the codes below to validate the user's wallet and the payload entry when depositing funds into the wallet. The get_balance method aggregates all the successful transactions carried out by the user and sums up the amount.

from .models import Wallet, WalletTransaction
from rest_framework import serializers
from django.db.models import Sum
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import User
from django.conf import settings
import requests

class WalletSerializer(serializers.ModelSerializer):
    """
    Serializers to validate the user's wallet 
    """
    balance = serializers.SerializerMethodField()

    def get_balance(self, obj):
        bal = WalletTransaction.objects.filter(
            wallet=obj, status="success").aggregate(Sum('amount'))['amount__sum']
        return bal

    class Meta:
        model = Wallet
        fields = ['id', 'currency', 'balance']


def is_amount(value):
    if value <= 0:
        raise serializers.ValidationError({"detail": "Invalid Amount"})
    return value


class DepositSerializer(serializers.Serializer):

    amount = serializers.IntegerField(validators=[is_amount])
    email = serializers.EmailField()

    def validate_email(self, value):
        if User.objects.filter(email=value).exists():
            return value
        raise serializers.ValidationError({"detail": "Email not found"})

    def save(self):
        user = self.context['request'].user
        wallet = Wallet.objects.get(user=user)
        data = self.validated_data
        url = 'https://api.paystack.co/transaction/initialize'
        headers = {
            {"authorization": f"Bearer {settings.PAYSTACK_SECRET_KEY}"}
        r = requests.post(url, headers=headers, data=data)
        response = r.json()
        WalletTransaction.objects.create(
            wallet=wallet,
            transaction_type="deposit",
            amount= data["amount"],
            paystack_payment_reference=response['data']['reference'],
            status="pending",
        )

        return response

I validated the amount and the email in the serializer, you can check out ways to validate data in serializers in this article.

in views.py file, add the following codes for the user's wallet info, deposit funds, and verify deposit endpoints

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.decorators import api_view
from django.conf import settings
from .models import Wallet
from .serializers import WalletSerializer, DepositSerializer

class WalletInfo(APIView):

    def get(self, request):
        wallet = Wallet.objects.get(user=request.user)
        data = WalletSerializer(wallet).data
        return Response(data)


class DepositFunds(APIView):

    def post(self, request):
        serializer = DepositSerializer(
            data=request.data, context={"request": request})
        serializer.is_valid(raise_exception=True)

        resp = serializer.save()
        return Response(resp)

class VerifyDeposit(APIView):

    def get(self, request, reference):
        transaction = WalletTransaction.objects.get(
        paystack_payment_reference=reference, wallet__user=request.user)
        reference = transaction.paystack_payment_reference
        url = 'https://api.paystack.co/transaction/verify/{}'.format(reference)
        headers = {
            {"authorization": f"Bearer {settings.PAYSTACK_SECRET_KEY}"}
        r = requests.get(url, headers=headers)
        resp = r.json()
        if resp['data']['status'] == 'success':
            status = resp['data']['status']
            amount = resp['data']['amount']
            WalletTransaction.objects.filter(paystack_payment_reference=reference).update(status=status,
                                                                                        amount=amount)
            return Response(resp)
        return Response(resp)

The DeposiFund allows the deposit of funds, it takes the amount and the user email as payload while the VerifyDeposit class verifies the deposit whether it is successful before updating the status of the transaction to "success".

let's create a URL path for the views in the app's urls.py file

from django.urls import path
from .views import Login, Register, WalletInfo, DepositFunds

urlpatterns = [
    path('register/', Register.as_view()),
    path('login/', Login.as_view()),
    path('wallet_info/', WalletInfo.as_view()),
    path('deposit/', DepositFunds.as_view()),
    path('deposit/verify/<str:reference>/', verify_deposit),
]

We will be testing the endpoints in Postman for the test user. I will deposit an amount in the user's account.

dep1.png

Note: Ensure you add the authorization header above which is the token displayed after logging in to avoid an error.

Deposit an amount, you will get the Paystack checkout link in the response, copy the link in your browser and proceed to checkout the payment.

dep2.png

You will see an interface shown below:

stack2.png

Then verify the deposit by copying the reference in the deposit endpoint response and adding it as a parameter in the deposit/verify/<str:reference> endpoint.

dep3.png

The status shows success which depicts the amount that was deposited successfully, let's check the wallet info for our balance.

dep 4.png

This shows the amount was successfully added to the user's balance.

Note: In production, it is best to use a webhook to receive the response from Paystack before populating the database to a successful transaction instead of the VerifyDeposit endpoint. You can read more on Paystack Webhooks

Conclusion

We have successfully integrated Paystack into our API and deposited an amount successfully.

All codes can be found in this repository

Reference

Paystack docs

Connect with me on Linkedin | Twitter | Github

Thanks for reading.

Did you find this article valuable?

Support Ubaydah Abdulwasiu by becoming a sponsor. Any amount is appreciated!