Fork Me
webcloud / log / Implementing PayPal in Django

The process is actually quite simple, though it took some effort to get all the pieces together due to the lack of good documentation / information on where to begin. Hopefully this little article will serve as a good starting point for others.

This article focuses specifically on a single product checkout flow, thought it serves as a good starting point if you later want to learn how to build a "shopping cart" implementation.

Prerequisites

Assumptions

To be able to follow through: I assume you've built at least a few Django websites and know the basic workings of a Django Application (models, views, urls).

Installing django-paypal

You can install it with PIP (and if you're not using virtualenv you should!)

pip install -e git://github.com/dcramer/django-paypal.git#egg=django-paypal

Next up edit the settings.py and add paypal.standard.ipn to your INSTALLED_APPS.

# settings.py
INSTALLED_APPS = (... 'paypal.standard.ipn', ...)
PAYPAL_RECEIVER_EMAIL = "your_paypal_email@example.com"
SITE_DOMAIN = "http://my-website.com/"

As seen above you'll also need to add a PAYPAL_RECEIVER_EMAIL that is your PayPal merchant account email.

It'll be a good idea to add your sandbox email inside a local settings file, which you should have obtained after creating a preconfigured business test account inside your PayPal Developer account. (fig. 1)

# local_settings.py
PAYPAL_RECEIVER_EMAIL = "sandbox_paypal_email@example.com"
SITE_DOMAIN = "http://localhost:8000/"
A paypal test account
Fig. 1. Preconfigured PayPal sandbox business test account

The product checkout flow

In this scenario we'll be building a single product checkout flow. We'll start by setting up a very simple app called "products" and a basic model for storing a product:

# products/models.py
from django.db import models

class Product(models.Model):
    title = models.CharField(max_length=128)
    slug = models.SlugField(max_length=128)
    price = models.PositiveIntegerField()

Displaying the product

Next up we want to render the product with the classic "buy now button" that takes the user to PayPal.

# products/views.py
import uuid
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.shortcuts import get_object_or_404
from django.core.urlresolvers import reverse
from django.conf import settings
from paypal.standard.forms import PayPalPaymentsForm
from products.models import Product

def product_detail(request, slug):
    product = get_object_or_404(Product, slug=slug)
    paypal = {
        'amount': product.price,
        'item_name': product.title,
        'item_number': product.slug,
        
        # PayPal wants a unique invoice ID
        'invoice': str(uuid.uuid1()), 
        
        # It'll be a good idea to setup a SITE_DOMAIN inside settings
        # so you don't need to hardcode these values.
        'return_url': settings.SITE_DOMAIN + reverse('return_url'),
        'cancel_return': settings.SITE_DOMAIN + reverse('cancel_url'),
    }
    form = PayPalPaymentsForm(initial=paypal)
    if settings.DEBUG:
        rendered_form = form.sandbox()
    else:
        rendered_form = form.render()
    return render_to_response('product_detail.html', {
        'product' : product,
        'form' : rendered_form,
    }, RequestContext(request))

Note that we use different't forms depending if we're in debug mode or not. The template displaying the product could be as simple as this:

{# product_detail.html #}
<h1>{{ product.title }}</h1>
<p>Price: ${{ product.price }}</p>
{{ form }}

Next up hook up the Django admin, add a few products, and configure an URL pointing to the product_detail view. If you got everything right so far, you should see something like this:

Standard PayPal button
Fig. 2. Product displayed with standard PayPal buy button

If you've configured a sandbox consumer test account, you can now go ahead and make a purchase, if successful, you'll be redirected to your "RETURN_URL" as specified earlier.

Managing orders and customers

To handle purchases: we'll set up a very simple "orders" application with a Customer and Order model.

# orders/models.py
from django.db import models
from products.models import Product

class Customer(models.Model):
    email = models.EmailField(max_length=255, unique=True)
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255)

class Order(models.Model):
    customer = models.ForeignKey(Customer)
    product = models.ForeignKey(Product)
    time_of_purchase = models.DateTimeField(auto_now_add=True)

Setting up signals

Now we are ready to setup a few signals that will "listen" for incoming PayPal payments. Some people like to signals inside models.py but I prefer to put them inside a signals.py file

# orders/signals.py
from customers.models import Customer, Order
from paypal.standard.ipn.signals import payment_was_successful
from products.models import Product


def confirm_payment(sender, **kwargs):
    # it's important to check that the product exists
    try:
        product = Product.objects.get(slug=sender.item_number)
    except Product.DoesNotExist:
        return
    # And that someone didn't tamper with the price
    if int(product.price) != int(sender.mc_gross):
        return
    # Check to see if it's an existing customer
    try:
        customer = Customer.objects.get(email=sender.payer_email)
    except Customer.DoesNotExist:
        customer = Customer.objects.create(
            email=sender.payer_email,
            first_name=sender.first_name,
            last_name=sender.last_name
        )
    # Add a new order
    Order.objects.create(customer=customer, product=product)

payment_was_successful.connect(confirm_payment)

An additional idea here is to send the customer an email with a "confirmation" or a link to a product download or if the payment was successful.

You might also want to configure some kind of logging if something goes wrong. This can be easily done with Django 1.3 which comes with built in logging capabitiles.

Testing IPN from PayPal Sandbox

Now that we've got the signal setup, let's test it with the PayPal IPN test tool. First of all, you need to add a "secret" URL which to where paypal should send it's payment notifications.

(r'^something-hard-to-guess/', include('paypal.standard.ipn.urls')),

If you're running the Django Development server, you'll need to re-route port 8000 (or whichever port you are using) to port 80, and make it available to the outside world. Point a web browser to your public IP address to make sure your project is accessible to the outside world.

Fig 3. The PayPal IPN Test tool

You'll need to supply the IPN handler URL (your IP + secret hard to guess url) along with a item_number(=product.slug) and mc_gross(=product.price) to make a successful request.

Setting up for real payments

As a last step before going live, you'll need to setup your PayPal merchant account to handle IPN's.

Login at paypal.com and go to My Account → Profile - Selling Preferences → Instant Payment Notification Preferences .

Conclusion

To keep this article short and simple, I've left out some of the parts surrounding this topic, but hopefully this was enough to get you started. If you've got any further questions send me an email or a tweet (@roflwtfbqq).

Further Reading