Adds stripe.js-based form for processing credit card payments
This commit is contained in:
parent
79fa80ea33
commit
8334d40fe9
6 changed files with 307 additions and 3 deletions
152
registripe/forms.py
Normal file
152
registripe/forms.py
Normal 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
|
||||
}
|
||||
'''
|
27
registripe/migrations/0001_initial.py
Normal file
27
registripe/migrations/0001_initial.py
Normal 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',),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
django-countries==4.0
|
||||
pinax-stripe==3.2.1
|
||||
requests>=2.11.1
|
||||
stripe==1.38.0
|
||||
|
|
Loading…
Reference in a new issue