Adds stripe.js-based form for processing credit card payments

This commit is contained in:
Christopher Neugebauer 2016-09-21 13:16:59 +10:00
parent 79fa80ea33
commit 8334d40fe9
6 changed files with 307 additions and 3 deletions

152
registripe/forms.py Normal file
View file

@ -0,0 +1,152 @@
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
}
'''

View file

@ -0,0 +1,27 @@
# -*- 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',),
),
]

View file

@ -1,5 +1,10 @@
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)

View file

@ -1,10 +1,14 @@
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"),
]

View file

@ -1,3 +1,118 @@
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.")

View file

@ -1,3 +1,4 @@
django-countries==4.0
pinax-stripe==3.2.1
requests>=2.11.1
stripe==1.38.0