From 8334d40fe931966d9a2112f65dc5a5a0e8131bfe 2016-09-21 08:52:31 From: Christopher Neugebauer Date: 2016-09-21 08:52:31 Subject: [PATCH] Adds stripe.js-based form for processing credit card payments --- diff --git a/registripe/forms.py b/registripe/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..c45b6e980085213620a78a2b3042ace6eb24209a --- /dev/null +++ b/registripe/forms.py @@ -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 "" + + +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 +} +''' diff --git a/registripe/migrations/0001_initial.py b/registripe/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..23276400b4c9da22d1751a9ad5820bb2701162c4 --- /dev/null +++ b/registripe/migrations/0001_initial.py @@ -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',), + ), + ] diff --git a/registripe/models.py b/registripe/models.py index bd4b2abe9e8520ac85fee31e2fd7ed02997b4eaa..668558e37fd57730851c723afdd122f17ea0da99 100644 --- a/registripe/models.py +++ b/registripe/models.py @@ -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) diff --git a/registripe/urls.py b/registripe/urls.py index c1e4970264bddd208e8752fa72dd1ead347c6cdd..e595890291ac53e0e16c59dea002c6f89ff674b2 100644 --- a/registripe/urls.py +++ b/registripe/urls.py @@ -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"), ] diff --git a/registripe/views.py b/registripe/views.py index 91ea44a218fbd2f408430959283f0419c921093e..d5083fa09a78787178faaf836f4b1890027a44c8 100644 --- a/registripe/views.py +++ b/registripe/views.py @@ -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.") diff --git a/requirements.txt b/requirements.txt index a407153415fb3d5f2c896c53a53f0ff672a3b1e2..13955911f2df520ee207852326e70b7e93e4fe87 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +django-countries==4.0 pinax-stripe==3.2.1 requests>=2.11.1 stripe==1.38.0