Changeset - 8334d40fe931
[Not reviewed]
0 4 2
Christopher Neugebauer - 8 years ago 2016-09-21 08:52:31
chrisjrn@gmail.com
Adds stripe.js-based form for processing credit card payments
6 files changed with 307 insertions and 3 deletions:
0 comments (0 inline, 0 general)
registripe/forms.py
Show inline comments
 
new file 100644
 
from functools import partial
 

	
 
from django import forms
 
from django.core.urlresolvers import reverse
 
from django.core.exceptions import ValidationError
 
from django.forms import widgets
 

	
 
from django_countries import countries
 
from django_countries.fields import LazyTypedChoiceField
 
from django_countries.widgets import CountrySelectWidget
 

	
 

	
 
class NoRenderWidget(forms.widgets.HiddenInput):
 

	
 
    def render(self, name, value, attrs=None):
 
        return "<!-- no widget: " + name + " -->"
 

	
 

	
 
def secure_striped(widget):
 
    ''' Calls stripe() with secure=True. '''
 
    return striped(widget, True)
 

	
 

	
 
def striped(WidgetClass, secure=False):
 
    ''' Takes a given widget and overrides the render method to be suitable
 
    for stripe.js.
 

	
 
    Arguments:
 
        widget: The widget class
 

	
 
        secure: if True, only the `data-stripe` attribute will be set. Name
 
            will be set to None.
 

	
 
    '''
 

	
 
    class StripedWidget(WidgetClass):
 

	
 
        def render(self, name, value, attrs=None):
 

	
 
            if not attrs:
 
                attrs = {}
 

	
 
            attrs["data-stripe"] = name
 

	
 
            if secure:
 
                name = ""
 

	
 
            return super(StripedWidget, self).render(
 
                name, value, attrs=attrs
 
            )
 

	
 
    return StripedWidget
 

	
 

	
 
class CreditCardForm(forms.Form):
 

	
 
    def _media(self):
 
        js = (
 
            'https://js.stripe.com/v2/',
 
            reverse("registripe_pubkey"),
 
        )
 

	
 
        return forms.Media(js=js)
 

	
 
    media = property(_media)
 

	
 
    number = forms.CharField(
 
        required=False,
 
        max_length=255,
 
        widget=secure_striped(widgets.TextInput)(),
 
    )
 
    exp_month = forms.CharField(
 
        required=False,
 
        max_length=2,
 
        widget=secure_striped(widgets.TextInput)(),
 
    )
 
    exp_year = forms.CharField(
 
        required=False,
 
        max_length=4,
 
        widget=secure_striped(widgets.TextInput)(),
 
    )
 
    cvc = forms.CharField(
 
        required=False,
 
        max_length=4,
 
        widget=secure_striped(widgets.TextInput)(),
 
    )
 

	
 
    stripe_token = forms.CharField(
 
        max_length=255,
 
        #required=True,
 
        widget=NoRenderWidget(),
 
    )
 

	
 
    name = forms.CharField(
 
        required=True,
 
        max_length=255,
 
        widget=striped(widgets.TextInput),
 
    )
 
    address_line1 = forms.CharField(
 
        required=True,
 
        max_length=255,
 
        widget=striped(widgets.TextInput),
 
    )
 
    address_line2 = forms.CharField(
 
        required=False,
 
        max_length=255,
 
        widget=striped(widgets.TextInput),
 
    )
 
    address_city = forms.CharField(
 
        required=True,
 
        max_length=255,
 
        widget=striped(widgets.TextInput),
 
    )
 
    address_state = forms.CharField(
 
        required=True, max_length=255,
 
        widget=striped(widgets.TextInput),
 
    )
 
    address_zip = forms.CharField(
 
        required=True,
 
        max_length=255,
 
        widget=striped(widgets.TextInput),
 
    )
 
    address_country = LazyTypedChoiceField(
 
        choices=countries,
 
        widget=striped(CountrySelectWidget),
 
    )
 

	
 

	
 
'''{
 
From stripe.js details:
 

	
 
Card details:
 

	
 
The first argument to createToken is a JavaScript object containing credit card data entered by the user. It should contain the following required members:
 

	
 
number: card number as a string without any separators (e.g., "4242424242424242")
 
exp_month: two digit number representing the card's expiration month (e.g., 12)
 
exp_year: two or four digit number representing the card's expiration year (e.g., 2017)
 
(The expiration date can also be passed as a single string.)
 

	
 
cvc: optional, but we highly recommend you provide it to help prevent fraud. This is the card's security code, as a string (e.g., "123").
 
The following fields are entirely optional and cannot result in a token creation failure:
 

	
 
name: cardholder name
 
address_line1: billing address line 1
 
address_line2: billing address line 2
 
address_city: billing address city
 
address_state: billing address state
 
address_zip: billing postal code as a string (e.g., "94301")
 
address_country: billing address country
 
}
 
'''
registripe/migrations/0001_initial.py
Show inline comments
 
new file 100644
 
# -*- coding: utf-8 -*-
 
# Generated by Django 1.9.2 on 2016-09-21 06:19
 
from __future__ import unicode_literals
 

	
 
from django.db import migrations, models
 
import django.db.models.deletion
 

	
 

	
 
class Migration(migrations.Migration):
 

	
 
    initial = True
 

	
 
    dependencies = [
 
        ('pinax_stripe', '0003_make_cvc_check_blankable'),
 
        ('registrasion', '0005_auto_20160905_0945'),
 
    ]
 

	
 
    operations = [
 
        migrations.CreateModel(
 
            name='StripePayment',
 
            fields=[
 
                ('paymentbase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registrasion.PaymentBase')),
 
                ('charge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Charge')),
 
            ],
 
            bases=('registrasion.paymentbase',),
 
        ),
 
    ]
registripe/models.py
Show inline comments
 
from __future__ import unicode_literals
 

	
 
from django.db import models
 
from registrasion.models import commerce
 
from pinax.stripe.models import Charge
 

	
 
# Create your models here.
 

	
 
class StripePayment(commerce.PaymentBase):
 

	
 
    charge = models.ForeignKey(Charge)
registripe/urls.py
Show inline comments
 
from django.conf.urls import url
 

	
 
from registripe import views
 

	
 
from pinax.stripe.views import (
 
    Webhook,
 
)
 

	
 

	
 
urlpatterns = [
 
    url(r"^card/([0-9]*)/$", views.card, name="registripe_card"),
 
    url(r"^pubkey/$", views.pubkey_script, name="registripe_pubkey"),
 
    url(r"^webhook/$", Webhook.as_view(), name="pinax_stripe_webhook"),
 
]
registripe/views.py
Show inline comments
 
from django.shortcuts import render
 
import forms
 
import models
 

	
 
# Create your views here.
 
from django.core.exceptions import ValidationError
 
from django.conf import settings
 
from django.contrib import messages
 
from django.db import transaction
 
from django.http import HttpResponse
 
from django.shortcuts import redirect, render
 

	
 
from registrasion.controllers.invoice import InvoiceController
 
from registrasion.models import commerce
 

	
 
from pinax.stripe import actions
 
from stripe.error import StripeError
 

	
 
from symposion.conference.models import Conference
 

	
 
CURRENCY = settings.INVOICE_CURRENCY
 
CONFERENCE_ID = settings.CONFERENCE_ID
 

	
 

	
 
def pubkey_script(request):
 
    ''' Returns a JS snippet that sets the Stripe public key for Stripe.js. '''
 

	
 
    script_template = "Stripe.setPublishableKey('%s');"
 
    script = script_template % settings.PINAX_STRIPE_PUBLIC_KEY
 

	
 
    return HttpResponse(script, content_type="text/javascript")
 

	
 

	
 
def card(request, invoice_id):
 

	
 
    form = forms.CreditCardForm(request.POST or None)
 

	
 
    inv = InvoiceController.for_id_or_404(str(invoice_id))
 

	
 
    if not inv.can_view(user=request.user):
 
        raise Http404()
 

	
 
    to_invoice = redirect("invoice", inv.invoice.id)
 

	
 
    if request.POST and form.is_valid():
 
        try:
 
            inv.validate_allowed_to_pay()  # Verify that we're allowed to do this.
 
            process_card(request, form, inv)
 
            return to_invoice
 
        except StripeError as e:
 
            form.add_error(None, ValidationError(e))
 
        except ValidationError as ve:
 
            form.add_error(None, ve)
 

	
 
    data = {
 
        "invoice": inv.invoice,
 
        "form": form,
 
    }
 

	
 
    return render(
 
        request, "registrasion/stripe/credit_card_payment.html", data
 
    )
 

	
 

	
 
@transaction.atomic
 
def process_card(request, form, inv):
 
    ''' Processes the given credit card form
 

	
 
    Arguments:
 
        request: the current request context
 
        form: a CreditCardForm
 
        inv: an InvoiceController
 
    '''
 

	
 
    conference = Conference.objects.get(id=CONFERENCE_ID)
 
    amount_to_pay = inv.invoice.balance_due()
 

	
 
    token = form.cleaned_data["stripe_token"]
 

	
 
    customer = actions.customers.get_customer_for_user(request.user)
 

	
 
    if not customer:
 
        customer = actions.customers.create(request.user)
 

	
 
    card = actions.sources.create_card(customer, token)
 

	
 
    description="Payment for %s invoice #%s" % (
 
        conference.title, inv.invoice.id
 
    )
 

	
 
    try:
 
        charge = actions.charges.create(
 
            amount_to_pay,
 
            customer,
 
            currency=CURRENCY,
 
            description=description,
 
            capture=False,
 
        )
 

	
 
        receipt = charge.stripe_charge.receipt_number
 
        if not receipt:
 
            receipt = charge.stripe_charge.id
 
        reference = "Paid with Stripe receipt number: " + receipt
 

	
 
        # Create the payment object
 
        models.StripePayment.objects.create(
 
            invoice=inv.invoice,
 
            reference=reference,
 
            amount=charge.amount,
 
            charge=charge,
 
        )
 
    except StripeError as e:
 
        raise e
 
    finally:
 
        # Do not actually charge the account until we've reconciled locally.
 
        actions.charges.capture(charge)
 

	
 
    inv.update_status()
 

	
 
    messages.success(request, "This invoice was successfully paid.")
requirements.txt
Show inline comments
 
django-countries==4.0
 
pinax-stripe==3.2.1
 
requests>=2.11.1
 
stripe==1.38.0
0 comments (0 inline, 0 general)