mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Payments via Stripe (#30)
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<p>{% blocktrans trimmed %}
|
||||||
|
The credit card transaction could not be completed. Please contact us.
|
||||||
|
{% endblocktrans %}</p>
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% bootstrap_form form layout='horizontal' %}
|
<div class="form-horizontal">
|
||||||
|
{% bootstrap_form form layout='horizontal' %}
|
||||||
|
</div>
|
||||||
@@ -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
|
||||||
|
|||||||
2
src/requirements/stripe.txt
Normal file
2
src/requirements/stripe.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
stripe
|
||||||
|
|
||||||
Reference in New Issue
Block a user