20 Minutes With Stripe and Django

If you use this, make sure you are PCI compliant, otherwise explore stripe.js…
In case you haven’t heard, payment gateways, merchant accounts and all that jazz are now obsolete thanks to Stripe. Stripe offers a simple to set up payment service with an absolutely wonderful API. Instead of comparing and contrasting dozens of merchant accounts and struggling with arcane API’s, with Stripe you input your name, address, SSN, and bank account information (among a few other things) and… you’re done.
In addition to a sane API, they offer great libraries for PHP, Python and Ruby along with wonderful documentation. Kudos to the developer team at Stripe, they’ve really outdone themselves.
Enough praising Stripe, let’s learn how to integrate it with Django. This should only take 20 minutes or so, and you shouldn’t have a problem bending this method to your will.
Things we need:
  1. Some way to store the sale in our database.
  2. A form that will validate the card details.
  3. A template to display the form with proper, human readable errors.
  4. The Stripe Python library:
    sudo pip install --index-url https://code.stripe.com --upgrade stripe
Luckily, we need not start from scratch on everything, as there is a very useful snippet for a credit card form we can start with. With that in mind, let’s lay out our project. I’ve decided to create a sales app that will contain: a Sale model, a SalePaymentForm, and a single view for displaying and parsing the form. First up, the Sale model in mystore/sales/models.py:
from django.db import models
 
import settings
 
class Sale(models.Model):
    def __init__(self, *args, **kwargs):
        super(Sale, self).__init__(*args, **kwargs)
 
        # bring in stripe, and get the api key from settings.py
        import stripe
        stripe.api_key = settings.STRIPE_API_KEY
 
        self.stripe = stripe
 
    # store the stripe charge id for this sale
    charge_id = models.CharField(max_length=32)
 
    # you could also store other information about the sale
    # but I'll leave that to you!
 
    def charge(self, price_in_cents, number, exp_month, exp_year, cvc):
        """
        Takes a the price and credit card details: number, exp_month,
        exp_year, cvc.
 
        Returns a tuple: (Boolean, Class) where the boolean is if
        the charge was successful, and the class is response (or error)
        instance.
        """
 
        if self.charge_id: # don't let this be charged twice!
            return False, Exception(message="Already charged.")
 
        try:
            response = self.stripe.Charge.create(
                amount = price_in_cents,
                currency = "usd",
                card = {
                    "number" : number,
                    "exp_month" : exp_month,
                    "exp_year" : exp_year,
                    "cvc" : cvc,
 
                    #### it is recommended to include the address!
                    #"address_line1" : self.address1,
                    #"address_line2" : self.address2,
                    #"daddress_zip" : self.zip_code,
                    #"address_state" : self.state,
                },
                description='Thank you for your purchase!')
 
            self.charge_id = response.id
 
        except self.stripe.CardError, ce:
            # charge failed
            return False, ce
 
        return True, response
A couple things to point out before moving on. One, we initialize the Stripe API in the __init__ method and use the API key you should set in settings.py as STRIPE_API_KEY = “somelongstripefromstripe”. Two, we wrap the actual charge API call in a try/except block so we can catch any errors Stripe might throw. Notice that we return that error as it is important to show the real, human readable error to the end user. You’ll see how in a second. Three, we make sure to set the charge_id on the mode, but we’ll need to remember to call the save() method.
Next, we need a form to handle the user’s data. Luckily, that snippet from before will come in handy, as the only thing we need to do is switch out the payment implementation. So, without further ado, here is the SalePaymentForm in mystore/sales/forms.py:
from datetime import date, datetime
from calendar import monthrange
 
from django import forms
 
from sales.models import Sale
 
class CreditCardField(forms.IntegerField):
    def clean(self, value):
        """Check if given CC number is valid and one of the
           card types we accept"""
        if value and (len(value) < 13 or len(value) > 16):
            raise forms.ValidationError("Please enter in a valid "+\
                "credit card number.")
        return super(CreditCardField, self).clean(value)
 
class CCExpWidget(forms.MultiWidget):
    """ Widget containing two select boxes for selecting the month and year"""
    def decompress(self, value):
        return [value.month, value.year] if value else [None, None]
 
    def format_output(self, rendered_widgets):
        html = u' / '.join(rendered_widgets)
        return u'<span style="white-space: nowrap;">%s</span>' % html
 
class CCExpField(forms.MultiValueField):
    EXP_MONTH = [(x, x) for x in xrange(1, 13)]
    EXP_YEAR = [(x, x) for x in xrange(date.today().year,
                                       date.today().year + 15)]
    default_error_messages = {
        'invalid_month': u'Enter a valid month.',
        'invalid_year': u'Enter a valid year.',
    }
 
    def __init__(self, *args, **kwargs):
        errors = self.default_error_messages.copy()
        if 'error_messages' in kwargs:
            errors.update(kwargs['error_messages'])
        fields = (
            forms.ChoiceField(choices=self.EXP_MONTH,
                error_messages={'invalid': errors['invalid_month']}),
            forms.ChoiceField(choices=self.EXP_YEAR,
                error_messages={'invalid': errors['invalid_year']}),
        )
        super(CCExpField, self).__init__(fields, *args, **kwargs)
        self.widget = CCExpWidget(widgets =
            [fields[0].widget, fields[1].widget])
 
    def clean(self, value):
        exp = super(CCExpField, self).clean(value)
        if date.today() &gt; exp:
            raise forms.ValidationError(
            "The expiration date you entered is in the past.")
        return exp
 
    def compress(self, data_list):
        if data_list:
            if data_list[1] in forms.fields.EMPTY_VALUES:
                error = self.error_messages['invalid_year']
                raise forms.ValidationError(error)
            if data_list[0] in forms.fields.EMPTY_VALUES:
                error = self.error_messages['invalid_month']
                raise forms.ValidationError(error)
            year = int(data_list[1])
            month = int(data_list[0])
            # find last day of the month
            day = monthrange(year, month)[1]
            return date(year, month, day)
        return None
 
class SalePaymentForm(forms.Form):
    number = CreditCardField(required=True, label="Card Number")
    expiration = CCExpField(required=True, label="Expiration")
    cvc = forms.IntegerField(required=True, label="CCV Number",
        max_value=9999, widget=forms.TextInput(attrs={'size': '4'}))
 
    def clean(self):
        """
        The clean method will effectively charge the card and create a new
        Sale instance. If it fails, it simply raises the error given from
        Stripe's library as a standard ValidationError for proper feedback.
        """
        cleaned = super(SalePaymentForm, self).clean()
 
        if not self.errors:
            number = self.cleaned_data["number"]
            exp_month = self.cleaned_data["expiration"].month
            exp_year = self.cleaned_data["expiration"].year
            cvc = self.cleaned_data["cvc"]
 
            sale = Sale()
 
            # let's charge $10.00 for this particular item
            success, instance = sale.charge(1000, number, exp_month,
                                                exp_year, cvc)
 
            if not success:
                raise forms.ValidationError("Error: %s" % instance.message)
            else:
                instance.save()
                # we were successful! do whatever you will here...
                # perhaps you'd like to send an email...
                pass
 
        return cleaned
I’ve removed a few of the bits from the snippet, mainly to simplify things, as we don’t need to filter out Discover cards or the like. The primary thing to notice is how if the charge isn’t successful, we raise a ValidationError and pass in the message from Stripe’s exception. This will allow us to display it as a normal error on the form. But before we we move on to the view, notice how we initialize a blank Sale? The charge() method doesn’t require a saved model instance, but we make sure to save it if it is successful. It might be more appropriate to make this a ModelForm, but for now, it works.
Let’s take a look at mystore/sales/urls.py:
from django.conf.urls.defaults import *
from sales import views
 
urlpatterns = patterns('',
    url(r'^charge/$', views.charge, name="charge"),
)
Nothing fancy here. Let’s take a look at and mystore/sales/views.py:
from django.shortcuts import render_to_response
from django.http import HttpResponse
from django.template import RequestContext
 
from sales.models import Sale
from sales.forms import SalePaymentForm
 
def charge(request):
    if request.method == "POST":
        form = SalePaymentForm(request.POST)
 
        if form.is_valid(): # charges the card
            return HttpResponse("Success! We've charged your card!")
    else:
        form = SalePaymentForm()
 
    return render_to_response("sales/charge.html",
                        RequestContext( request, {'form': form} ) )
Once again, nothing very fancy here either. Let’s take a look at templates/sales/charge.html:
<html>
<head>
  <title>Stripe Example</title>
</head>
<body>
 
<div class="wrapper">
 
  {% for key, value in form.errors.items %}
      <p>{{ value }}</p>
  {% endfor %}
 
  <form action="" method="post">{% csrf_token %}
 
    {% for field in form %}
      <div class="field-wrapper">
 
        <div class="field-label">
          {{ field.label_tag }}:
        </div>
 
        <div class="field-field">
          {{ field }}
          {{ field.errors }}
        </div>
 
      </div>
    {% endfor %}
 
    <br>
    <input type="submit" value="Charge Me!" />
  </form>
 
</div>
 
</body>
</html>
This should come as no surprise to you, but there isn’t much going on here. Probably the only slightly odd thing is the for loop over the form.errors dictionary, as this is where we will grab the ValidationError placed on the form itself as we caught it during the charge phase.
Besides for setting up the standard Django stuff like the database and admin bits (mystore/sales/admin.py), that pretty much covers it.

No comments:

Post a Comment