Implementing PayPal in Django
Recently I had to build a website with Django that needed to integrate PayPal Payments. This is how it can be done.
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
- Django 1.2 or later (might work with older versions, not tested)
- django-paypal (the dcramer fork @ github)
- A PayPal Sandbox developer account
- A PayPal Merchant account
- Something that's worth selling!
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/"
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:
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.
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).