From f04c43abdccf721de52c2946b03f5af66db5364e Mon Sep 17 00:00:00 2001
From: Raphael Michel
Date: Thu, 25 Jun 2015 15:49:14 +0200
Subject: [PATCH] Refs #33 -- Added UI and Stripe support for retrying failed
payments
---
doc/development/api/payment.rst | 14 ++-
src/pretix/base/payment.py | 48 +++++--
src/pretix/plugins/banktransfer/payment.py | 6 +-
src/pretix/plugins/paypal/payment.py | 8 +-
src/pretix/plugins/stripe/payment.py | 42 +++++--
src/pretix/plugins/stripe/signals.py | 4 +-
.../pretixplugins/stripe/pretix-stripe.js | 3 +-
.../pretixplugins/stripe/pending.html | 11 +-
.../templates/pretixpresale/event/order.html | 5 +
.../pretixpresale/event/order_pay.html | 32 +++++
.../event/order_pay_confirm.html | 43 +++++++
src/pretix/presale/urls.py | 4 +
src/pretix/presale/views/checkout.py | 6 +-
src/pretix/presale/views/order.py | 118 +++++++++++++++---
14 files changed, 287 insertions(+), 57 deletions(-)
create mode 100644 src/pretix/presale/templates/pretixpresale/event/order_pay.html
create mode 100644 src/pretix/presale/templates/pretixpresale/event/order_pay_confirm.html
diff --git a/doc/development/api/payment.rst b/doc/development/api/payment.rst
index 681688ddf..49deb850a 100644
--- a/doc/development/api/payment.rst
+++ b/doc/development/api/payment.rst
@@ -62,23 +62,23 @@ The provider class
.. automethod:: settings_content_render
- .. automethod:: checkout_form_render
+ .. automethod:: payment_form_render
- .. automethod:: checkout_form
+ .. automethod:: payment_form
.. automethod:: is_allowed
- .. autoattribute:: checkout_form_fields
+ .. autoattribute:: payment_form_fields
.. automethod:: checkout_prepare
- .. automethod:: checkout_is_valid_session
+ .. automethod:: payment_is_valid_session
.. automethod:: checkout_confirm_render
This is an abstract method, you **must** override this!
- .. automethod:: checkout_perform
+ .. automethod:: payment_perform
.. automethod:: order_pending_mail_render
@@ -86,6 +86,10 @@ The provider class
This is an abstract method, you **must** override this!
+ .. automethod:: order_can_retry
+
+ .. automethod:: retry_prepare
+
.. automethod:: order_paid_render
.. automethod:: order_control_render
diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py
index 259dec1aa..ca7818c29 100644
--- a/src/pretix/base/payment.py
+++ b/src/pretix/base/payment.py
@@ -129,7 +129,7 @@ class BasePaymentProvider:
pass
@property
- def checkout_form_fields(self) -> dict:
+ def payment_form_fields(self) -> dict:
"""
This is used by the default implementation of :py:meth:`checkout_form`.
It should return an object similar to :py:attr:`settings_form_fields`.
@@ -138,7 +138,7 @@ class BasePaymentProvider:
"""
return {}
- def checkout_form(self, request: HttpRequest) -> Form:
+ def payment_form(self, request: HttpRequest) -> Form:
"""
This is called by the default implementation of :py:meth:`checkout_form_render`
to obtain the form that is displayed to the user during the checkout
@@ -155,7 +155,7 @@ class BasePaymentProvider:
if k.startswith('payment_%s_' % self.identifier)
}
)
- form.fields = self.checkout_form_fields
+ form.fields = self.payment_form_fields
return form
def is_allowed(self, request: HttpRequest) -> bool:
@@ -168,7 +168,7 @@ class BasePaymentProvider:
"""
return True
- def checkout_form_render(self, request: HttpRequest) -> str:
+ def payment_form_render(self, request: HttpRequest) -> str:
"""
When the user selects this provider as his prefered payment method,
he will be shown the HTML you return from this method.
@@ -178,7 +178,7 @@ class BasePaymentProvider:
the user to fill out form fields, you should just return a paragraph
of explainatory text.
"""
- form = self.checkout_form(request)
+ form = self.payment_form(request)
template = get_template('pretixpresale/event/checkout_payment_form_default.html')
ctx = {'request': request, 'form': form}
return template.render(ctx)
@@ -209,7 +209,7 @@ class BasePaymentProvider:
to the user (or the normal form validation error messages).
The default implementation stores the input into the form returned by
- :py:meth:`checkout_form` in the user's session.
+ :py:meth:`payment_form` in the user's session.
If your payment method requires you to redirect the user to an external provider,
this might be the place to do so.
@@ -233,7 +233,7 @@ class BasePaymentProvider:
payment_fee:
The fee for the payment method.
"""
- form = self.checkout_form(request)
+ form = self.payment_form(request)
if form.is_valid():
for k, v in form.cleaned_data.items():
request.session['payment_%s_%s' % (self.identifier, k)] = v
@@ -241,7 +241,7 @@ class BasePaymentProvider:
else:
return False
- def checkout_is_valid_session(self, request: HttpRequest) -> bool:
+ def payment_is_valid_session(self, request: HttpRequest) -> bool:
"""
This is called at the time the user tries to place the order. It should return
``True``, if the user's session is valid and all data your payment provider requires
@@ -249,7 +249,7 @@ class BasePaymentProvider:
"""
raise NotImplementedError() # NOQA
- def checkout_perform(self, request: HttpRequest, order: Order) -> str:
+ def payment_perform(self, request: HttpRequest, order: Order) -> str:
"""
After the user confirmed his purchase, this method will be called to complete
the payment process. This is the place to actually move the money, if applicable.
@@ -299,6 +299,32 @@ class BasePaymentProvider:
"""
raise NotImplementedError() # NOQA
+ def order_can_retry(self, order: Order) -> bool:
+ """
+ Will be called if the user views the detail page of an unpaid order to determine
+ whether the user should be presented with an option to retry the payment. The default
+ implementation always returns False.
+
+ :param order: The order object
+ """
+ return False
+
+ def retry_prepare(self, request: HttpRequest, order: Order) -> "bool|str":
+ """
+ Will be called if the user retries to pay an unpaid order (after the user filled in
+ e.g. the form returned by :py:meth:`payment_form`).
+
+ It should return and report errors the same way as :py:meth:`checkout_prepare`, but
+ receives an ``Order`` object instead of a cart object.
+ """
+ form = self.payment_form(request)
+ if form.is_valid():
+ for k, v in form.cleaned_data.items():
+ request.session['payment_%s_%s' % (self.identifier, k)] = v
+ return True
+ else:
+ return False
+
def order_paid_render(self, request: HttpRequest, order: Order) -> str:
"""
Will be called if the user views the detail page of an paid order which is
@@ -377,14 +403,14 @@ class FreeOrderProvider(BasePaymentProvider):
def order_pending_render(self, request: HttpRequest, order: Order) -> str:
pass
- def checkout_is_valid_session(self, request: HttpRequest) -> bool:
+ def payment_is_valid_session(self, request: HttpRequest) -> bool:
return True
@property
def verbose_name(self) -> str:
return _("Free of charge")
- def checkout_perform(self, request: HttpRequest, order: Order):
+ def payment_perform(self, request: HttpRequest, order: Order):
mark_order_paid(order, 'free')
@property
diff --git a/src/pretix/plugins/banktransfer/payment.py b/src/pretix/plugins/banktransfer/payment.py
index 02ba1485e..6dec27403 100644
--- a/src/pretix/plugins/banktransfer/payment.py
+++ b/src/pretix/plugins/banktransfer/payment.py
@@ -23,7 +23,7 @@ class BankTransfer(BasePaymentProvider):
]
)
- def checkout_form_render(self, request) -> str:
+ def payment_form_render(self, request) -> str:
template = get_template('pretixplugins/banktransfer/checkout_payment_form.html')
ctx = {'request': request, 'event': self.event, 'settings': self.settings}
return template.render(ctx)
@@ -31,11 +31,11 @@ class BankTransfer(BasePaymentProvider):
def checkout_prepare(self, request, total):
return True
- def checkout_is_valid_session(self, request):
+ def payment_is_valid_session(self, request):
return True
def checkout_confirm_render(self, request):
- form = self.checkout_form(request)
+ form = self.payment_form(request)
template = get_template('pretixplugins/banktransfer/checkout_payment_confirm.html')
ctx = {'request': request, 'form': form, 'settings': self.settings}
return template.render(ctx)
diff --git a/src/pretix/plugins/paypal/payment.py b/src/pretix/plugins/paypal/payment.py
index 95a0509b1..5c48944d2 100644
--- a/src/pretix/plugins/paypal/payment.py
+++ b/src/pretix/plugins/paypal/payment.py
@@ -22,7 +22,7 @@ class Paypal(BasePaymentProvider):
identifier = 'paypal'
verbose_name = _('PayPal')
- checkout_form_fields = OrderedDict([
+ payment_form_fields = OrderedDict([
])
@property
@@ -55,11 +55,11 @@ class Paypal(BasePaymentProvider):
client_id=self.settings.get('client_id'),
client_secret=self.settings.get('secret'))
- def checkout_is_valid_session(self, request):
+ def payment_is_valid_session(self, request):
return (request.session.get('payment_paypal_id', '') != ''
and request.session.get('payment_paypal_payer', '') != '')
- def checkout_form_render(self, request) -> str:
+ def payment_form_render(self, request) -> str:
template = get_template('pretixplugins/paypal/checkout_payment_form.html')
ctx = {'request': request, 'event': self.event, 'settings': self.settings}
return template.render(ctx)
@@ -135,7 +135,7 @@ class Paypal(BasePaymentProvider):
ctx = {'request': request, 'event': self.event, 'settings': self.settings}
return template.render(ctx)
- def checkout_perform(self, request, order) -> str:
+ def payment_perform(self, request, order) -> str:
"""
Will be called if the user submitted his order successfully to initiate the
payment process.
diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py
index 73de287de..695546ce5 100644
--- a/src/pretix/plugins/stripe/payment.py
+++ b/src/pretix/plugins/stripe/payment.py
@@ -3,7 +3,6 @@ import json
import logging
from django.contrib import messages
-from django.core.urlresolvers import reverse
from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _
from django import forms
@@ -43,9 +42,12 @@ class Stripe(BasePaymentProvider):
build_absolute_uri('plugins:stripe:webhook')
)
- def checkout_is_valid_session(self, request):
+ def payment_is_valid_session(self, request):
return request.session.get('payment_stripe_token') != ''
+ def retry_prepare(self, request, order):
+ return self.checkout_prepare(request, None)
+
def checkout_prepare(self, request, cart):
token = request.POST.get('stripe_token', '')
request.session['payment_stripe_token'] = token
@@ -56,7 +58,7 @@ class Stripe(BasePaymentProvider):
return False
return True
- def checkout_form_render(self, request) -> str:
+ def payment_form_render(self, request) -> str:
template = get_template('pretixplugins/stripe/checkout_payment_form.html')
ctx = {'request': request, 'event': self.event, 'settings': self.settings}
return template.render(ctx)
@@ -70,7 +72,10 @@ class Stripe(BasePaymentProvider):
ctx = {'request': request, 'event': self.event, 'settings': self.settings}
return template.render(ctx)
- def checkout_perform(self, request, order) -> str:
+ def order_can_retry(self, order):
+ return True
+
+ def payment_perform(self, request, order) -> str:
self._init_api()
try:
charge = stripe.Charge.create(
@@ -82,23 +87,31 @@ class Stripe(BasePaymentProvider):
'event': self.event.identity,
'code': order.code
},
- idempotency_key=self.event.identity + order.code # TODO: Use something better
+ # TODO: Is this sufficient?
+ idempotency_key=self.event.identity + order.code + request.session['payment_stripe_token']
)
except stripe.error.CardError as e:
err = e.json_body['error']
messages.error(request, _('Stripe reported an error with your card: %s' % err['message']))
logger.info('Stripe card error: %s' % str(err))
+ order = order.clone()
+ order.payment_info = json.dumps({
+ 'error': True,
+ 'message': err['message'],
+ })
+ order.save()
except stripe.error.InvalidRequestError or stripe.error.AuthenticationError or stripe.error.APIConnectionError \
- as e:
+ or stripe.error.StripeError as e:
err = e.json_body['error']
messages.error(request, _('We had trouble communicating with Stripe. Please try again and get in touch '
'with us if this problem persists.'))
logger.error('Stripe error: %s' % str(err))
- except stripe.error.StripeError as e:
- err = e.json_body['error']
- messages.error(request, _('We had trouble processing the payment. Please try again and get '
- 'in touch with us if this problem persists.'))
- logger.error('Stripe error: %s' % str(err))
+ order = order.clone()
+ order.payment_info = json.dumps({
+ 'error': True,
+ 'message': err['message'],
+ })
+ order.save()
else:
if charge.status == 'succeeded' and charge.paid:
try:
@@ -112,11 +125,16 @@ class Stripe(BasePaymentProvider):
order = order.clone()
order.payment_info = str(charge)
order.save()
+ del request.session['payment_stripe_token']
def order_pending_render(self, request, order) -> str:
+ if order.payment_info:
+ payment_info = json.loads(order.payment_info)
+ else:
+ payment_info = None
template = get_template('pretixplugins/stripe/pending.html')
ctx = {'request': request, 'event': self.event, 'settings': self.settings,
- 'order': order}
+ 'order': order, 'payment_info': payment_info}
return template.render(ctx)
def order_control_render(self, request, order) -> str:
diff --git a/src/pretix/plugins/stripe/signals.py b/src/pretix/plugins/stripe/signals.py
index 442398f13..eeb0d403f 100644
--- a/src/pretix/plugins/stripe/signals.py
+++ b/src/pretix/plugins/stripe/signals.py
@@ -11,15 +11,17 @@ from pretix.presale.signals import html_head
@receiver(register_payment_providers)
def register_payment_provider(sender, **kwargs):
from .payment import Stripe
+
return Stripe
@receiver(html_head)
def html_head_presale(sender, request=None, **kwargs):
from .payment import Stripe
+
provider = Stripe(sender)
url = resolve(request.path_info)
- if provider.is_enabled and "checkout.payment" in url.url_name:
+ if provider.is_enabled and ("checkout.payment" in url.url_name or "order.pay" in url.url_name):
template = get_template('pretixplugins/stripe/presale_head.html')
ctx = Context({'event': sender, 'settings': provider.settings})
return template.render(ctx)
diff --git a/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js b/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js
index 093684a0a..c029c6127 100644
--- a/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js
+++ b/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js
@@ -85,7 +85,8 @@ $(function() {
.keyup(pretixstripe.validate_cvc)
$("#stripe_number").parents("form").submit(
function () {
- if ($("input[name=payment][value=stripe]").prop('checked') && $("#stripe_token").val() == "") {
+ if (($("input[name=payment][value=stripe]").prop('checked') || $("input[name=payment]").length === 0)
+ && $("#stripe_token").val() == "") {
pretixstripe.request();
return false;
}
diff --git a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html
index 8e9e2c12a..d9919cf37 100644
--- a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html
+++ b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html
@@ -1,5 +1,12 @@
{% load i18n %}
{% blocktrans trimmed %}
- The credit card transaction could not be completed. Please contact us.
-{% endblocktrans %}
+ The credit card transaction could not be completed for the following reason:
+{% endblocktrans %}
+
+ {% if payment_info and payment_info.error %}
+ {{ payment_info.message }}
+ {% else %}
+ {% trans "Unknown reason" %}
+ {% endif %}
+
diff --git a/src/pretix/presale/templates/pretixpresale/event/order.html b/src/pretix/presale/templates/pretixpresale/event/order.html
index 8d2bf4063..1c30c0fee 100644
--- a/src/pretix/presale/templates/pretixpresale/event/order.html
+++ b/src/pretix/presale/templates/pretixpresale/event/order.html
@@ -17,10 +17,15 @@
+ {% if can_retry %}
+
{% trans "Complete payment" %}
+ {% endif %}
{{ payment }}
{% blocktrans trimmed with date=order.expires|date:"SHORT_DATE_FORMAT" %}
Please complete your payment before {{ date }}
{% endblocktrans %}
+
{% endif %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/order_pay.html b/src/pretix/presale/templates/pretixpresale/event/order_pay.html
new file mode 100644
index 000000000..c5648270a
--- /dev/null
+++ b/src/pretix/presale/templates/pretixpresale/event/order_pay.html
@@ -0,0 +1,32 @@
+{% extends "pretixpresale/event/base.html" %}
+{% load i18n %}
+{% block title %}{% trans "Cancel order" %}{% endblock %}
+{% block content %}
+
+ {% blocktrans trimmed with code=order.code %}
+ Pay order: {{ code }}
+ {% endblocktrans %}
+
+
+
+
+{% endblock %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/order_pay_confirm.html b/src/pretix/presale/templates/pretixpresale/event/order_pay_confirm.html
new file mode 100644
index 000000000..61ec667c1
--- /dev/null
+++ b/src/pretix/presale/templates/pretixpresale/event/order_pay_confirm.html
@@ -0,0 +1,43 @@
+{% extends "pretixpresale/event/base.html" %}
+{% load i18n %}
+{% block title %}{% trans "Cancel order" %}{% endblock %}
+{% block content %}
+
+ {% blocktrans trimmed with code=order.code %}
+ Pay order: {{ code }}
+ {% endblocktrans %}
+
+
+
+
+{% endblock %}
diff --git a/src/pretix/presale/urls.py b/src/pretix/presale/urls.py
index 0d4b9c504..24ed45276 100644
--- a/src/pretix/presale/urls.py
+++ b/src/pretix/presale/urls.py
@@ -23,6 +23,10 @@ urlpatterns = [
name='event.order.cancel'),
url(r'^order/(?P[^/]+)/modify$', pretix.presale.views.order.OrderModify.as_view(),
name='event.order.modify'),
+ url(r'^order/(?P[^/]+)/pay$', pretix.presale.views.order.OrderPay.as_view(),
+ name='event.order.pay'),
+ url(r'^order/(?P[^/]+)/pay/confirm$', pretix.presale.views.order.OrderPayDo.as_view(),
+ name='event.order.pay.confirm'),
url(r'^order/(?P[^/]+)/download/(?P