Fix #177 - Allow to change the payment method

This commit is contained in:
Raphael Michel
2016-08-31 13:20:00 +02:00
parent b21ed4d99f
commit 022e02d913
8 changed files with 209 additions and 11 deletions

View File

@@ -314,20 +314,42 @@ class BasePaymentProvider:
"""
raise NotImplementedError() # NOQA
def order_change_allowed(self, order: Order) -> bool:
"""
Will be called to check whether it is allowed to change the payment method of
an order to this one.
The default implementation always returns ``True``.
:param order: The order object
"""
return True
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.
The retry workflow is also used if a user switches to this payment method for an existing
order! Therefore, they can only switch to your p
:param order: The order object
"""
return False
def retry_prepare(self, request: HttpRequest, order: Order) -> "bool|str":
"""
Deprecated, use order_prepare instead
"""
raise DeprecationWarning('retry_prepare is deprecated, use order_prepare instead')
return self.order_prepare(request, order)
def order_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`).
e.g. the form returned by :py:meth:`payment_form`) or if the user changes the payment
method.
It should return and report errors the same way as :py:meth:`checkout_prepare`, but
receives an ``Order`` object instead of a cart object.
@@ -469,6 +491,9 @@ class FreeOrderProvider(BasePaymentProvider):
cart_id=request.session.session_key, event=request.event
).aggregate(sum=Sum('price'))['sum'] == 0
def order_change_allowed(self, order: Order) -> bool:
return False
@receiver(register_payment_providers, dispatch_uid="payment_free")
def register_payment_provider(sender, **kwargs):

View File

@@ -246,7 +246,7 @@ class Paypal(BasePaymentProvider):
def order_can_retry(self, order):
return True
def retry_prepare(self, request, order):
def order_prepare(self, request, order):
self.init_api()
payment = paypalrestsdk.Payment({
'intent': 'sale',

View File

@@ -46,7 +46,7 @@ class Stripe(BasePaymentProvider):
def payment_is_valid_session(self, request):
return request.session.get('payment_stripe_token') != ''
def retry_prepare(self, request, order):
def order_prepare(self, request, order):
return self.checkout_prepare(request, None)
def checkout_prepare(self, request, cart):

View File

@@ -33,6 +33,12 @@
{% if order.status == "n" %}
<div class="panel panel-danger">
<div class="panel-heading">
<div class="pull-right">
<a href="{% eventurl event "presale:event.order.pay.change" secret=order.secret order=order.code %}">
<span class="fa fa-edit"></span>
{% trans "Use different payment method" %}
</a>
</div>
<h3 class="panel-title">
{% trans "Payment" %}
</h3>

View File

@@ -0,0 +1,65 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n %}
{% load eventurl %}
{% block title %}{% trans "Change payment method" %}{% endblock %}
{% block content %}
<h2>
{% blocktrans trimmed with code=order.code %}
Change payment method: {{ code }}
{% endblocktrans %}
</h2>
<p>
{% blocktrans trimmed %}
Please note: If you change your payment method, your order total will change by the
amount displayed to the right of each method.
{% endblocktrans %}
</p>
<form method="post">
{% csrf_token %}
<div class="panel-group" id="payment_accordion">
{% for p in providers %}
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<label class="radio">
<strong class="pull-right">{% if p.fee_diff >= 0 %}+{% else %}-{% endif %} {{ p.fee_diff_abs|floatformat:2 }} {{ event.currency }}</strong>
<input type="radio" name="payment" value="{{ p.provider.identifier }}"
data-parent="#payment_accordion"
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}" />
<strong>{{ p.provider.verbose_name }}</strong>
</label>
</h4>
</div>
<div id="payment_{{ p.provider.identifier }}"
class="panel-collapse collapsed {% if selected == p.provider.identifier %}in{% endif %}">
<div class="panel-body form-horizontal">
{{ p.form }}
</div>
</div>
</div>
{% empty %}
<div class="alert alert-info">
{% trans "There are no alternative payment providers available for this order." %}
</div>
{% endfor %}
</div>
<div class="row checkout-button-row">
<div class="col-md-4">
<a class="btn btn-block btn-default btn-lg"
href="{% eventurl request.event "presale:event.order" secret=order.secret order=order.code %}">
{% trans "Cancel" %}
</a>
</div>
<div class="col-md-4 col-md-offset-4">
{% if providers %}
<button class="btn btn-block btn-primary btn-lg" type="submit">
{% trans "Continue" %}
</button>
{% endif %}
</div>
<div class="clearfix"></div>
</div>
</form>
{% endblock %}

View File

@@ -40,6 +40,9 @@ event_patterns = [
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/pay/complete$',
pretix.presale.views.order.OrderPayComplete.as_view(),
name='event.order.pay.complete'),
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/pay/change',
pretix.presale.views.order.OrderPayChangeMethod.as_view(),
name='event.order.pay.change'),
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/download/(?P<output>[^/]+)$',
pretix.presale.views.order.OrderDownload.as_view(),
name='event.order.download'),

View File

@@ -2,6 +2,8 @@ from datetime import timedelta
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.db import transaction
from django.db.models import Sum
from django.http import Http404
from django.shortcuts import redirect
from django.utils.functional import cached_property
@@ -14,7 +16,7 @@ from pretix.base.models import (
)
from pretix.base.models.orders import InvoiceAddress
from pretix.base.services.invoices import (
generate_invoice, invoice_pdf, invoice_qualified,
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
)
from pretix.base.services.orders import OrderError, cancel_order
from pretix.base.services.tickets import generate
@@ -125,9 +127,9 @@ class OrderPay(EventViewMixin, OrderDetailMixin, TemplateView):
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
resp = self.payment_provider.retry_prepare(
request, self.order
)
resp = self.payment_provider.order_prepare(request, self.order)
if 'payment_change_{}'.format(self.order.pk) in request.session:
del request.session['payment_change_{}'.format(self.order.pk)]
if isinstance(resp, str):
return redirect(resp)
elif resp is True:
@@ -181,7 +183,8 @@ class OrderPayDo(EventViewMixin, OrderDetailMixin, TemplateView):
self.request = request
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if not self.payment_provider.order_can_retry(self.order) or not self.payment_provider.is_enabled:
can_do = self.payment_provider.order_can_retry(self.order) or 'payment_change_{}'.format(self.order.pk) in request.session
if not can_do or not self.payment_provider.is_enabled:
messages.error(request, _('The payment for this order cannot be continued.'))
return redirect(self.get_order_url())
if (not self.payment_provider.payment_is_valid_session(request)
@@ -192,6 +195,8 @@ class OrderPayDo(EventViewMixin, OrderDetailMixin, TemplateView):
def post(self, request, *args, **kwargs):
resp = self.payment_provider.payment_perform(request, self.order)
if 'payment_change_{}'.format(self.order.pk) in request.session:
del request.session['payment_change_{}'.format(self.order.pk)]
return redirect(resp or self.get_order_url())
def get_context_data(self, **kwargs):
@@ -213,8 +218,8 @@ class OrderPayComplete(EventViewMixin, OrderDetailMixin, View):
self.request = request
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if (not self.payment_provider.payment_is_valid_session(request)
or not self.payment_provider.is_enabled):
if (not self.payment_provider.payment_is_valid_session(request) or
not self.payment_provider.is_enabled):
messages.error(request, _('The payment information you entered was incomplete.'))
return redirect(self.get_payment_url())
return super().dispatch(request, *args, **kwargs)
@@ -230,6 +235,96 @@ class OrderPayComplete(EventViewMixin, OrderDetailMixin, View):
})
class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
template_name = 'pretixpresale/event/order_pay_change.html'
def dispatch(self, request, *args, **kwargs):
self.request = request
if not self.order:
raise Http404(_('Unknown order code or not authorized to access this order.'))
if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_EXPIRED):
messages.error(request, _('The payment method for this order cannot be changed.'))
return redirect(self.get_order_url() + '?paid=yes')
return super().dispatch(request, *args, **kwargs)
def get_payment_url(self):
return eventreverse(self.request.event, 'presale:event.order.pay', kwargs={
'order': self.order.code,
'secret': self.order.secret
})
@cached_property
def _total_order_value(self):
return self.order.positions.aggregate(sum=Sum('price'))['sum']
@cached_property
def provider_forms(self):
providers = []
responses = register_payment_providers.send(self.request.event)
for receiver, response in responses:
provider = response(self.request.event)
if provider.identifier == self.order.payment_provider:
continue
if not provider.is_enabled or not provider.order_change_allowed(self.order):
continue
fee = provider.calculate_fee(self._total_order_value)
providers.append({
'provider': provider,
'fee': fee,
'fee_diff': fee - self.order.payment_fee,
'fee_diff_abs': abs(fee - self.order.payment_fee),
'form': provider.payment_form_render(self.request)
})
return providers
def post(self, request, *args, **kwargs):
self.request = request
for p in self.provider_forms:
if p['provider'].identifier == request.POST.get('payment', ''):
request.session['payment'] = p['provider'].identifier
request.session['payment_change_{}'.format(self.order.pk)] = '1'
resp = p['provider'].order_prepare(request, self.order)
if resp:
with transaction.atomic():
new_fee = p['provider'].calculate_fee(self._total_order_value)
self.order.log_action('pretix.event.order.payment.changed', {
'old_fee': self.order.payment_fee,
'new_fee': new_fee,
'old_provider': self.order.payment_provider,
'new_provider': p['provider'].identifier
})
self.order.payment_provider = p['provider'].identifier
self.order.payment_fee = new_fee
self.order._calculate_tax()
self.order.save()
i = self.order.invoices.filter(is_cancellation=False).last()
if i:
generate_cancellation(i)
generate_invoice(self.order)
if isinstance(resp, str):
return redirect(resp)
elif resp is True:
return redirect(self.get_confirm_url())
else:
return self.get(request, *args, **kwargs)
messages.error(self.request, _("Please select a payment method."))
return self.get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['order'] = self.order
ctx['providers'] = self.provider_forms
return ctx
def get_confirm_url(self):
return eventreverse(self.request.event, 'presale:event.order.pay.confirm', kwargs={
'order': self.order.code,
'secret': self.order.secret
})
class OrderModify(EventViewMixin, OrderDetailMixin, QuestionsViewMixin, TemplateView):
template_name = "pretixpresale/event/order_modify.html"