Payments via Stripe (#30)

This commit is contained in:
Raphael Michel
2015-03-16 01:32:32 +01:00
parent e20ecb67d8
commit 91087d0e1a
12 changed files with 341 additions and 9 deletions

View File

@@ -1,20 +1,84 @@
from collections import OrderedDict from collections import OrderedDict
import logging
from django.contrib import messages
from django.template import Context
from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django import forms from django import forms
import stripe
from pretix.base.payment import BasePaymentProvider from pretix.base.payment import BasePaymentProvider
logger = logging.getLogger('pretix.plugins.stripe')
class Stripe(BasePaymentProvider): class Stripe(BasePaymentProvider):
identifier = 'stripe' identifier = 'stripe'
verbose_name = _('Credit Card via Stripe') verbose_name = _('Credit Card via Stripe')
checkout_form_fields = OrderedDict([
('cc_number', @property
forms.CharField( def settings_form_fields(self):
label=_('Credit card number'), return OrderedDict(
required=False list(super().settings_form_fields.items()) + [
)) ('secret_key',
]) forms.CharField(
label=_('Secret key'),
required=False
)),
('publishable_key',
forms.CharField(
label=_('Publishable key'),
required=False
))
]
)
def checkout_is_valid_session(self, request): def checkout_is_valid_session(self, request):
return False return request.session.get('payment_stripe_token') != ''
def checkout_prepare(self, request, cart):
token = request.POST.get('stripe_token', '')
request.session['payment_stripe_token'] = token
request.session['payment_stripe_brand'] = request.POST.get('stripe_card_brand', '')
request.session['payment_stripe_last4'] = request.POST.get('stripe_card_last4', '')
if token == '':
messages.error(request, _('You may need to enable JavaScript for Stripe payments.'))
return False
return True
def checkout_form_render(self, request) -> str:
template = get_template('pretixplugins/stripe/checkout_payment_form.html')
ctx = Context({'request': request, 'event': self.event, 'settings': self.settings})
return template.render(ctx)
def _init_api(self):
stripe.api_key = self.settings.get('secret_key')
def checkout_confirm_render(self, request) -> str:
template = get_template('pretixplugins/stripe/checkout_payment_confirm.html')
ctx = Context({'request': request, 'event': self.event, 'settings': self.settings})
return template.render(ctx)
def checkout_perform(self, request, order) -> str:
self._init_api()
charge = stripe.Charge.create(
amount=int(order.total * 100),
currency=request.event.currency.lower(),
source=request.session['payment_stripe_token'],
idempotency_key=self.event.identity + order.code # TODO: Use something better
)
logging.info(charge)
if charge.status == 'succeeded' and charge.paid:
order.mark_paid('stripe', str(charge))
messages.success(request, _('We successfully received your payment. Thank you!'))
else:
messages.warning(request, _('Stripe reported an error: %s' % charge.failure_message))
order = order.clone()
order.payment_info = str(charge)
order.save()
def order_pending_render(self, request, order) -> str:
template = get_template('pretixplugins/stripe/pending.html')
ctx = Context({'request': request, 'event': self.event, 'settings': self.settings,
'order': order})
return template.render(ctx)

View File

@@ -1,10 +1,26 @@
from django.core.urlresolvers import resolve
from django.dispatch import receiver from django.dispatch import receiver
from django.template import Context
from django.template.loader import get_template
from pretix.base.signals import register_payment_providers from pretix.base.signals import register_payment_providers
from .payment import Stripe from .payment import Stripe
from pretix.presale.signals import html_head
@receiver(register_payment_providers) @receiver(register_payment_providers)
def register_payment_provider(sender, **kwargs): def register_payment_provider(sender, **kwargs):
return Stripe return Stripe
@receiver(html_head)
def html_head_presale(sender, request=None, **kwargs):
provider = Stripe(sender)
url = resolve(request.path_info)
if provider.is_enabled and "checkout.payment" in url.url_name:
template = get_template('pretixplugins/stripe/presale_head.html')
ctx = Context({'event': sender, 'settings': provider.settings})
return template.render(ctx)
else:
return ""

View File

@@ -0,0 +1,94 @@
'use strict';
var pretixstripe = {
'validate_number': function () {
var numb = $("#stripe_number").val();
$(".stripe-number").addClass("has-feedback");
if (Stripe.card.validateCardNumber(numb)) {
$(".stripe-number").addClass("has-success").removeClass("has-error");
$(".stripe-number .form-control-feedback").addClass("fa-check")
.removeClass("fa-remove").removeClass("sr-only");
} else {
$(".stripe-number").removeClass("has-success").addClass("has-error");
$(".stripe-number .form-control-feedback").addClass("fa-remove")
.removeClass("fa-ok").removeClass("sr-only");
}
},
'validate_expire': function () {
var month = $("#stripe_exp_month").val();
var year = $("#stripe_exp_year").val();
$(".stripe-exp").addClass("has-feedback");
if (Stripe.card.validateExpiry(month, year)) {
$(".stripe-exp").addClass("has-success").removeClass("has-error");
$(".stripe-exp .form-control-feedback").addClass("fa-check")
.removeClass("fa-remove").removeClass("sr-only");
} else {
$(".stripe-exp").removeClass("has-success").addClass("has-error");
$(".stripe-exp .form-control-feedback").addClass("fa-remove")
.removeClass("fa-ok").removeClass("sr-only");
}
},
'validate_cvc': function () {
var cvc = $("#stripe_cvc").val();
$(".stripe-cvc").addClass("has-feedback");
if (Stripe.card.validateCVC(cvc)) {
$(".stripe-cvc").addClass("has-success").removeClass("has-error");
$(".stripe-cvc .form-control-feedback").addClass("fa-check")
.removeClass("fa-remove").removeClass("sr-only");
} else {
$(".stripe-cvc").removeClass("has-success").addClass("has-error");
$(".stripe-cvc .form-control-feedback").addClass("fa-remove")
.removeClass("fa-ok").removeClass("sr-only");
}
},
'request': function () {
waitingDialog.show(stripe_loading_message);
$(".stripe-errors").hide();
Stripe.card.createToken(
{
number: $('#stripe_number').val(),
cvc: $('#stripe_cvc').val(),
exp_month: $('#stripe_exp_month').val(),
exp_year: $('#stripe_exp_year').val(),
name: $('#stripe_name').val(),
},
pretixstripe.response
);
},
'response': function (status, response) {
var $form = $("#stripe_number").parents("form");
waitingDialog.hide();
if (response.error) {
$(".stripe-errors").stop().hide();
$(".stripe-errors").html("<div class='alert alert-danger'>" + response.error.message + "</div>");
$(".stripe-errors").slideDown();
} else {
var token = response.id;
// Insert the token into the form so it gets submitted to the server
$("#stripe_token").val(token);
$("#stripe_card_brand").val(response.card.brand);
$("#stripe_card_last4").val(response.card.last4);
// and submit
$form.get(0).submit();
}
}
};
$(function() {
if (!$("#stripe_number").length) // Not on the checkout page
return;
$("#stripe_number").change(pretixstripe.validate_number).keydown(pretixstripe.validate_number)
.keyup(pretixstripe.validate_number);
$(".stripe-exp input").change(pretixstripe.validate_expire).keydown(pretixstripe.validate_expire)
.keyup(pretixstripe.validate_expire)
$("#stripe_cvc").change(pretixstripe.validate_cvc).keydown(pretixstripe.validate_cvc)
.keyup(pretixstripe.validate_cvc)
$("#stripe_number").parents("form").submit(
function () {
if ($("#stripe_token").val() == "") {
pretixstripe.request();
return false;
}
}
);
});

View File

@@ -0,0 +1,12 @@
{% load i18n %}
<p>{% blocktrans trimmed %}
The total amount will be withdrawn from your credit card.
{% endblocktrans %}</p>
<dl class="dl-horizontal">
<dt>{% trans "Card type" %}</dt>
<dd>{{ request.session.payment_stripe_brand }}</dd>
<dt>{% trans "Card number" %}</dt>
<dd>**** **** **** {{ request.session.payment_stripe_last4 }}</dd>
</dl>

View File

@@ -0,0 +1,59 @@
{% load i18n %}
<div class="form-horizontal">
<div class="stripe-errors sr-only">
</div>
<div class="form-group stripe-number">
<label class="control-label col-sm-2">
{% trans "Credit card number" %}
</label>
<div class="col-sm-4">
<input type="text" id="stripe_number" class="form-control" placeholder="4242 4242 4242 4242">
<span class="fa form-control-feedback sr-only"></span>
</div>
</div>
<div class="form-group stripe-exp">
<label class="control-label col-sm-2">
{% trans "Expiration date" %}
</label>
<div class="col-sm-2">
<input type="number" min="1" max="12" id="stripe_exp_month" class="form-control" placeholder=
"{% trans "Month" %}">
<span class="fa form-control-feedback sr-only"></span>
</div>
<div class="col-sm-2">
<input type="number" min="{% now "Y" %}" id="stripe_exp_year" class="form-control" placeholder=
"{% trans "Year" %}">
<span class="fa form-control-feedback sr-only"></span>
</div>
</div>
<div class="form-group stripe-cvc">
<label class="control-label col-sm-2">
{% trans "Security code (CVC)" %}
</label>
<div class="col-sm-2">
<input type="text" maxlength="3" id="stripe_cvc" class="form-control" placeholder=
"{% trans "123" %}">
<span class="fa form-control-feedback sr-only"></span>
</div>
</div>
<div class="form-group stripe-name">
<label class="control-label col-sm-2">
{% trans "Cardholder name" %}
</label>
<div class="col-sm-4">
<input type="text" id="stripe_name" class="form-control">
</div>
</div>
<p>
<em>{% blocktrans trimmed %}
Your payment will be processed by Stripe, Inc. Your credit card data will be transmitted directly to
Stripe and never touches our servers.
{% endblocktrans %}</em>
<input type="hidden" name="stripe_token" value="" id="stripe_token" />
<input type="hidden" name="stripe_card_last4" value="" id="stripe_card_last4" />
<input type="hidden" name="stripe_card_brand" value="" id="stripe_card_brand" />
</p>
</div>

View File

@@ -0,0 +1,5 @@
{% load i18n %}
<p>{% blocktrans trimmed %}
The credit card transaction could not be completed. Please contact us.
{% endblocktrans %}</p>

View File

@@ -0,0 +1,12 @@
{% load staticfiles %}
{% load compress %}
{% load i18n %}
{% compress js %}
<script type="text/javascript" src="{% static "pretixplugins/stripe/pretix-stripe.js" %}"></script>
{% endcompress %}
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
<script type="text/javascript">
Stripe.setPublishableKey('{{ settings.publishable_key }}');
var stripe_loading_message = '{% trans "Contacting Stripe…" %}';
</script>

View File

@@ -6,3 +6,64 @@ $(function () {
$($(this).attr("data-target")).collapse('show'); $($(this).attr("data-target")).collapse('show');
}); });
}); });
/**
* Module for displaying "Waiting for..." dialog using Bootstrap
*
* @author Eugene Maslovich <ehpc@em42.ru>
* MIT License
*/
var waitingDialog = (function ($) {
// Creating modal dialog's DOM
var $dialog = $(
'<div class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1" role="dialog" aria-hidden="true" style="padding-top:15%; overflow-y:visible;">' +
'<div class="modal-dialog modal-m">' +
'<div class="modal-content">' +
'<div class="modal-header"><h3 style="margin:0;"></h3></div>' +
'<div class="modal-body">' +
'<div class="progress progress-striped active" style="margin-bottom:0;"><div class="progress-bar" style="width: 100%"></div></div>' +
'</div>' +
'</div></div></div>');
return {
/**
* Opens our dialog
* @param message Custom message
* @param options Custom options:
* options.dialogSize - bootstrap postfix for dialog size, e.g. "sm", "m";
* options.progressType - bootstrap postfix for progress bar type, e.g. "success", "warning".
*/
show: function (message, options) {
// Assigning defaults
var settings = $.extend({
dialogSize: 'm',
progressType: ''
}, options);
if (typeof message === 'undefined') {
message = 'Loading';
}
if (typeof options === 'undefined') {
options = {};
}
// Configuring dialog
$dialog.find('.modal-dialog').attr('class', 'modal-dialog').addClass('modal-' + settings.dialogSize);
$dialog.find('.progress-bar').attr('class', 'progress-bar');
if (settings.progressType) {
$dialog.find('.progress-bar').addClass('progress-bar-' + settings.progressType);
}
$dialog.find('h3').text(message);
// Opening dialog
$dialog.modal();
},
/**
* Closes dialog
*/
hide: function () {
$dialog.modal('hide');
}
}
})(jQuery);

View File

@@ -1,3 +1,7 @@
.panel-title .radio { .panel-title .radio {
margin-left: 20px; margin-left: 20px;
}
.form-control + .form-control-feedback {
/* Fix for https://github.com/FortAwesome/Font-Awesome/issues/4313 */
.form-control-feedback;
} }

View File

@@ -1,2 +1,4 @@
{% load bootstrap3 %} {% load bootstrap3 %}
{% bootstrap_form form layout='horizontal' %} <div class="form-horizontal">
{% bootstrap_form form layout='horizontal' %}
</div>

View File

@@ -3,3 +3,4 @@
-r requirements/dev.txt -r requirements/dev.txt
-r requirements/testing.txt -r requirements/testing.txt
-r requirements/paypal.txt -r requirements/paypal.txt
-r requirements/stripe.txt

View File

@@ -0,0 +1,2 @@
stripe