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