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:
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:
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:
Nothing fancy here. Let’s take a look at and mystore/sales/views.py:
Once again, nothing very fancy here either. Let’s take a look at templates/sales/charge.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.
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:
- Some way to store the sale in our database.
- A form that will validate the card details.
- A template to display the form with proper, human readable errors.
- The Stripe Python library:
sudo pip install --index-url https://code.stripe.com --upgrade stripe
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 |
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() > 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 |
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"), ) |
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} ) ) |
<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> |
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