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…
	
	Add table
		
		Reference in a new issue
	
	 Christopher Neugebauer
						Christopher Neugebauer