Fixed #132 -- Reverse payment fee calculation

This commit is contained in:
Raphael Michel
2016-03-13 19:10:45 +01:00
parent 0bff010ea0
commit 9efce8f203
8 changed files with 199 additions and 4 deletions

View File

@@ -25,6 +25,9 @@ class BasePaymentProvider:
def __init__(self, event: Event): def __init__(self, event: Event):
self.event = event self.event = event
self.settings = SettingsSandbox('payment', self.identifier, event) self.settings = SettingsSandbox('payment', self.identifier, event)
# Default values
if self.settings.get('_fee_reverse_calc') is None:
self.settings.set('_fee_reverse_calc', True)
def __str__(self): def __str__(self):
return self.identifier return self.identifier
@@ -48,7 +51,11 @@ class BasePaymentProvider:
""" """
fee_abs = self.settings.get('_fee_abs', as_type=Decimal, default=0) fee_abs = self.settings.get('_fee_abs', as_type=Decimal, default=0)
fee_percent = self.settings.get('_fee_percent', as_type=Decimal, default=0) fee_percent = self.settings.get('_fee_percent', as_type=Decimal, default=0)
return round_decimal(price * fee_percent / 100) + fee_abs fee_reverse_calc = self.settings.get('_fee_reverse_calc', as_type=bool, default=True)
if fee_reverse_calc:
return round_decimal((price + fee_abs) * (1 / (1 - fee_percent / 100)) - price)
else:
return round_decimal(price * fee_percent / 100) + fee_abs
@property @property
def verbose_name(self) -> str: def verbose_name(self) -> str:
@@ -118,6 +125,14 @@ class BasePaymentProvider:
help_text=_('Percentage'), help_text=_('Percentage'),
required=False required=False
)), )),
('_fee_reverse_calc',
forms.BooleanField(
label=_('Calculate the fee from the total value including the fee.'),
help_text=_('We recommend you to enable this if you want your users to pay the payment fees of your '
'payment provider. <a href="/control/help/payment/fee_reverse" target="_blank">Click here '
'for detailled information on what this does.</a>'),
required=False
)),
]) ])
def settings_content_render(self, request: HttpRequest) -> str: def settings_content_render(self, request: HttpRequest) -> str:

View File

@@ -0,0 +1,7 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Help center" %}</h1>
{% block inner %}
{% endblock %}
{% endblock %}

View File

@@ -0,0 +1,68 @@
{% extends "pretixcontrol/help/base.html" %}
{% block title %}Payment fee calculation{% endblock %}
{% block inner %}
<h2>Payment fee calculation</h2>
<p>
If you configure a fee for a payment method, there are two possible ways for us to calculate this. Let's
assume that your payment provider, e.g. PayPal, charges you 5 % fees and you want to charge your users the
same 5 %, such that for a ticket with a list price of 100 € you will get your full 100 €.
</p>
<ul>
<li>
<strong>Method A: Calculate the fee from the subtotal and add it to the bill.</strong> For a ticket price of
100 €, this will lead to the following calculation:
<table class="table" style="width: auto;">
<tr>
<td>Ticket price</td>
<td class="text-right">100.00 €</td>
</tr>
<tr>
<td>pretix calculates the fee as 5% of 100 €</td>
<td class="text-right">+ 5.00 €</td>
</tr>
<tr>
<td>Subtotal that will be paid by the customer</td>
<td class="text-right">105.00 €</td>
</tr>
<tr>
<td>PayPal calculates its fee as 5% of 105 €</td>
<td class="text-right">- 5.25 €</td>
</tr>
<tr>
<td>End total that is on your bank account</td>
<td class="text-right"><strong>99.75 €</strong></td>
</tr>
</table>
</li>
<li>
<strong>Method B (default): Calculate the fee from the total value including the fee.</strong> For a ticket
price of 100 €, this will lead to the following calculation:
<table class="table" style="width: auto;">
<tr>
<td>Ticket price</td>
<td class="text-right">100.00 €</td>
</tr>
<tr>
<td>pretix calculates the fee as 100/(100 - 5)% of 100 €</td>
<td class="text-right">+ 5.26 €</td>
</tr>
<tr>
<td>Subtotal that will be paid by the customer</td>
<td class="text-right">105.26 €</td>
</tr>
<tr>
<td>PayPal calculates its fee as 5% of 105.26 €</td>
<td class="text-right">- 5.26 €</td>
</tr>
<tr>
<td>End total that is on your bank account</td>
<td class="text-right"><strong>100.00 €</strong></td>
</tr>
</table>
<div class="alert-warning alert">
Due to the various rounding steps performed by us and by the payment provider, the end total on
your bank account might stil vary by one cent.
</div>
</li>
</ul>
{% endblock %}

View File

@@ -1,8 +1,8 @@
from django.conf.urls import include, url from django.conf.urls import include, url
from pretix.control.views import ( from pretix.control.views import (
attendees, auth, dashboards, event, item, main, orders, organizer, user, attendees, auth, dashboards, event, help, item, main, orders, organizer,
vouchers, user, vouchers,
) )
urlpatterns = [ urlpatterns = [
@@ -78,4 +78,5 @@ urlpatterns = [
url(r'^orders/$', orders.OrderList.as_view(), name='event.orders'), url(r'^orders/$', orders.OrderList.as_view(), name='event.orders'),
url(r'^attendees/$', attendees.AttendeeList.as_view(), name='event.attendees'), url(r'^attendees/$', attendees.AttendeeList.as_view(), name='event.attendees'),
])), ])),
url(r'^help/(?P<topic>[^.]+)$', help.HelpView.as_view(), name='help'),
] ]

View File

@@ -0,0 +1,23 @@
from django import template
from django.http import Http404
from django.shortcuts import render
from django.views.generic import View
from pretix.base.models import Organizer
class HelpView(View):
model = Organizer
context_object_name = 'organizers'
template_name = 'pretixcontrol/organizers/index.html'
paginate_by = 30
def get(self, request, *args, **kwargs):
try:
locale = request.LANGUAGE_CODE
return render(request, 'pretixcontrol/help/%s.%s.html' % (kwargs.get('topic'), locale), {})
except template.TemplateDoesNotExist:
try:
return render(request, 'pretixcontrol/help/%s.html' % kwargs.get('topic'), {})
except template.TemplateDoesNotExist:
raise Http404('')

View File

@@ -0,0 +1,54 @@
import time
from decimal import Decimal
import pytest
from django.utils.timezone import now
from tests.testdummy.payment import DummyPaymentProvider
from pretix.base.models import Event, EventLock, Organizer
from pretix.base.services import locking
@pytest.fixture
def event():
o = Organizer.objects.create(name='Dummy', slug='dummy')
event = Event.objects.create(
organizer=o, name='Dummy', slug='dummy',
date_from=now()
)
return event
@pytest.mark.django_db
def test_payment_fee_forward(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_fee_abs', Decimal('0.30'))
prov.settings.set('_fee_percent', Decimal('5.00'))
prov.settings.set('_fee_reverse_calc', False)
assert prov.calculate_fee(Decimal('100.00')) == Decimal('5.30')
@pytest.mark.django_db
def test_payment_fee_reverse_percent(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_fee_abs', Decimal('0.00'))
prov.settings.set('_fee_percent', Decimal('5.00'))
prov.settings.set('_fee_reverse_calc', True)
assert prov.calculate_fee(Decimal('100.00')) == Decimal('5.26')
@pytest.mark.django_db
def test_payment_fee_reverse_percent_and_abs(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_fee_abs', Decimal('0.30'))
prov.settings.set('_fee_percent', Decimal('2.90'))
prov.settings.set('_fee_reverse_calc', True)
assert prov.calculate_fee(Decimal('100.00')) == Decimal('3.30')
@pytest.mark.django_db
def test_payment_fee_reverse_percent_and_abs_default(event):
prov = DummyPaymentProvider(event)
prov.settings.set('_fee_abs', Decimal('0.30'))
prov.settings.set('_fee_percent', Decimal('2.90'))
assert prov.calculate_fee(Decimal('100.00')) == Decimal('3.30')

View File

@@ -0,0 +1,19 @@
import logging
from pretix.base.payment import BasePaymentProvider
logger = logging.getLogger('tests.testdummy.ticketoutput')
class DummyPaymentProvider(BasePaymentProvider):
identifier = 'testdummy'
verbose_name = 'Test dummy'
def order_pending_render(self, request, order) -> str:
pass
def payment_is_valid_session(self, request) -> bool:
pass
def checkout_confirm_render(self, request) -> str:
pass

View File

@@ -1,9 +1,17 @@
from django.dispatch import receiver from django.dispatch import receiver
from pretix.base.signals import register_ticket_outputs from pretix.base.signals import (
register_payment_providers, register_ticket_outputs,
)
@receiver(register_ticket_outputs, dispatch_uid="output_dummy") @receiver(register_ticket_outputs, dispatch_uid="output_dummy")
def register_ticket_outputs(sender, **kwargs): def register_ticket_outputs(sender, **kwargs):
from .ticketoutput import DummyTicketOutput from .ticketoutput import DummyTicketOutput
return DummyTicketOutput return DummyTicketOutput
@receiver(register_payment_providers, dispatch_uid="payment_dummy")
def register_ticket_outputs(sender, **kwargs):
from .payment import DummyPaymentProvider
return DummyPaymentProvider