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 %} +