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 __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import models
|
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 django.conf.urls import url
|
||||||
|
|
||||||
|
from registripe import views
|
||||||
|
|
||||||
from pinax.stripe.views import (
|
from pinax.stripe.views import (
|
||||||
Webhook,
|
Webhook,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
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"),
|
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
|
pinax-stripe==3.2.1
|
||||||
requests>=2.11.1
|
requests>=2.11.1
|
||||||
stripe==1.38.0
|
stripe==1.38.0
|
||||||
|
|
Loading…
Reference in a new issue