forked from CGM_Public/pretix_original
Fixed #132 -- Reverse payment fee calculation
This commit is contained in:
@@ -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,6 +51,10 @@ 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)
|
||||||
|
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
|
return round_decimal(price * fee_percent / 100) + fee_abs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{% extends "pretixcontrol/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>{% trans "Help center" %}</h1>
|
||||||
|
{% block inner %}
|
||||||
|
{% endblock %}
|
||||||
|
{% endblock %}
|
||||||
@@ -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 %}
|
||||||
@@ -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'),
|
||||||
]
|
]
|
||||||
|
|||||||
23
src/pretix/control/views/help.py
Normal file
23
src/pretix/control/views/help.py
Normal 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('')
|
||||||
54
src/tests/base/test_payment.py
Normal file
54
src/tests/base/test_payment.py
Normal 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')
|
||||||
19
src/tests/testdummy/payment.py
Normal file
19
src/tests/testdummy/payment.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user