diff --git a/src/pretix/control/templatetags/hierarkey_form.py b/src/pretix/control/templatetags/hierarkey_form.py index f1c91b59b1..faede40d1e 100644 --- a/src/pretix/control/templatetags/hierarkey_form.py +++ b/src/pretix/control/templatetags/hierarkey_form.py @@ -2,6 +2,8 @@ from django import template from django.template import Node from django.utils.translation import ugettext as _ +from pretix.base.models import Event + register = template.Library() @@ -39,15 +41,19 @@ class PropagatedNode(Node): """.format( body=body, - text_inh=_("Organizer-level settings"), + text_inh=_("Organizer-level settings") if isinstance(event, Event) else _('Site-level settings'), fnames=','.join(self.field_names), text_expl=_( 'These settings are currently set on organizer level. This way, you can easily change them for ' 'all of your events at the same time. You can either go to the organizer settings to change them ' 'or decouple them from the organizer account to change them for this event individually.' + ) if isinstance(event, Event) else _( + 'These settings are currently set on global level. This way, you can easily change them for ' + 'all organizers at the same time. You can either go to the global settings to change them ' + 'or decouple them from the global settings to change them for this event individually.' ), - text_unlink=_('Change only for this event'), - text_orga=_('Change for all events'), + text_unlink=_('Change only for this event') if isinstance(event, Event) else _('Change only for this organizer'), + text_orga=_('Change for all events') if isinstance(event, Event) else _('Change for all organizers'), url=url ) diff --git a/src/pretix/plugins/stripe/forms.py b/src/pretix/plugins/stripe/forms.py index 92d45d2958..82bb386cfc 100644 --- a/src/pretix/plugins/stripe/forms.py +++ b/src/pretix/plugins/stripe/forms.py @@ -1,6 +1,8 @@ from django import forms from django.utils.translation import ugettext_lazy as _ +from pretix.base.forms import SettingsForm + class StripeKeyValidator: def __init__(self, prefix): @@ -21,3 +23,18 @@ class StripeKeyValidator: 'prefix': self._prefixes[0], }, ) + + +class OrganizerStripeSettingsForm(SettingsForm): + payment_stripe_connect_app_fee_percent = forms.DecimalField( + label=_('Stripe Connect: App fee (percent)'), + required=False, + ) + payment_stripe_connect_app_fee_max = forms.DecimalField( + label=_('Stripe Connect: App fee (max)'), + required=False, + ) + payment_stripe_connect_app_fee_min = forms.DecimalField( + label=_('Stripe Connect: App fee (min)'), + required=False, + ) diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py index 291e22f1eb..f7e4ee5bb7 100644 --- a/src/pretix/plugins/stripe/payment.py +++ b/src/pretix/plugins/stripe/payment.py @@ -3,6 +3,7 @@ import json import logging import urllib.parse from collections import OrderedDict +from decimal import Decimal import stripe from django import forms @@ -256,6 +257,20 @@ class StripeMethod(BasePaymentProvider): def _get_amount(self, payment): return self._decimal_to_int(payment.amount) + def _connect_kwargs(self, payment): + d = {} + if self.settings.connect_client_id and self.settings.connect_user_id: + fee = Decimal('0.00') + if self.settings.get('connect_app_fee_percent', as_type=Decimal): + fee = round_decimal(self.settings.get('connect_app_fee_percent', as_type=Decimal) * payment.amount / Decimal('100.00'), self.event.currency) + if self.settings.connect_app_fee_max: + fee = min(fee, self.settings.get('connect_app_fee_max', as_type=Decimal)) + if self.settings.get('connect_app_fee_min', as_type=Decimal): + fee = max(fee, self.settings.get('connect_app_fee_min', as_type=Decimal)) + if fee: + d['application_fee_amount'] = self._decimal_to_int(fee) + return d + @property def api_kwargs(self): if self.settings.connect_client_id and self.settings.connect_user_id: @@ -301,6 +316,7 @@ class StripeMethod(BasePaymentProvider): code=payment.order.code )[:22] params.update(self.api_kwargs) + params.update(self._connect_kwargs(payment)) charge = stripe.Charge.create( amount=self._get_amount(payment), currency=self.event.currency.lower(), @@ -612,6 +628,9 @@ class StripeCC(StripeMethod): try: if self.payment_is_valid_session(request): + params = {} + params.update(self._connect_kwargs(payment)) + params.update(self.api_kwargs) intent = stripe.PaymentIntent.create( amount=self._get_amount(payment), currency=self.event.currency.lower(), @@ -638,7 +657,7 @@ class StripeCC(StripeMethod): 'payment': payment.pk, 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), }), - **self.api_kwargs + **params ) else: payment_info = json.loads(payment.info) diff --git a/src/pretix/plugins/stripe/signals.py b/src/pretix/plugins/stripe/signals.py index e1c05b29b8..200de8a525 100644 --- a/src/pretix/plugins/stripe/signals.py +++ b/src/pretix/plugins/stripe/signals.py @@ -4,7 +4,7 @@ from collections import OrderedDict from django import forms from django.dispatch import receiver from django.template.loader import get_template -from django.urls import resolve +from django.urls import resolve, reverse from django.utils.translation import ugettext_lazy as _ from pretix.base.settings import settings_hierarkey @@ -12,6 +12,7 @@ from pretix.base.signals import ( logentry_display, register_global_settings, register_payment_providers, requiredaction_display, ) +from pretix.control.signals import nav_organizer from pretix.plugins.stripe.forms import StripeKeyValidator from pretix.presale.signals import html_head @@ -121,6 +122,18 @@ def register_global_settings(sender, **kwargs): StripeKeyValidator('pk_test_'), ), )), + ('payment_stripe_connect_app_fee_percent', forms.DecimalField( + label=_('Stripe Connect: App fee (percent)'), + required=False, + )), + ('payment_stripe_connect_app_fee_max', forms.DecimalField( + label=_('Stripe Connect: App fee (max)'), + required=False, + )), + ('payment_stripe_connect_app_fee_min', forms.DecimalField( + label=_('Stripe Connect: App fee (min)'), + required=False, + )), ]) @@ -141,3 +154,20 @@ def pretixcontrol_action_display(sender, action, request, **kwargs): ctx = {'data': data, 'event': sender, 'action': action} return template.render(ctx, request) + + +@receiver(nav_organizer, dispatch_uid="stripe_nav_organizer") +def nav_o(sender, request, organizer, **kwargs): + if request.user.has_active_staff_session(request.session.session_key): + url = resolve(request.path_info) + return [{ + 'label': _('Stripe Connect'), + 'url': reverse('plugins:stripe:settings.connect', kwargs={ + 'organizer': request.organizer.slug + }), + 'parent': reverse('control:organizer.edit', kwargs={ + 'organizer': request.organizer.slug + }), + 'active': 'settings.connect' in url.url_name, + }] + return [] diff --git a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/organizer_stripe.html b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/organizer_stripe.html new file mode 100644 index 0000000000..e42a60fe24 --- /dev/null +++ b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/organizer_stripe.html @@ -0,0 +1,24 @@ +{% extends "pretixcontrol/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% load hierarkey_form %} +{% load formset_tags %} +{% block title %}{% trans "Stripe Connect" %}{% endblock %} +{% block content %} +

+ {% trans "Stripe Connect" %} +

+ +
+ {% csrf_token %} + {% url "control:global.settings" as g_url %} + {% propagated request.organizer g_url "payment_stripe_connect_app_fee_percent" "payment_stripe_connect_app_fee_min" "payment_stripe_connect_app_fee_max" %} + {% bootstrap_form form layout="control" %} + {% endpropagated %} +
+ +
+
+{% endblock %} diff --git a/src/pretix/plugins/stripe/urls.py b/src/pretix/plugins/stripe/urls.py index bad2fd9ffb..e0c3475758 100644 --- a/src/pretix/plugins/stripe/urls.py +++ b/src/pretix/plugins/stripe/urls.py @@ -3,8 +3,9 @@ from django.conf.urls import include, url from pretix.multidomain import event_url from .views import ( - ReturnView, ScaReturnView, ScaView, applepay_association, oauth_disconnect, - oauth_return, redirect_view, webhook, + OrganizerSettingsFormView, ReturnView, ScaReturnView, ScaView, + applepay_association, oauth_disconnect, oauth_return, redirect_view, + webhook, ) event_patterns = [ @@ -24,6 +25,8 @@ organizer_patterns = [ urlpatterns = [ url(r'^control/event/(?P[^/]+)/(?P[^/]+)/stripe/disconnect/', oauth_disconnect, name='oauth.disconnect'), + url(r'^control/organizer/(?P[^/]+)/stripeconnect/', + OrganizerSettingsFormView.as_view(), name='settings.connect'), url(r'^_stripe/webhook/$', webhook, name='webhook'), url(r'^_stripe/oauth_return/$', oauth_return, name='oauth.return'), url(r'^.well-known/apple-developer-merchantid-domain-association$', applepay_association, name='applepay.association'), diff --git a/src/pretix/plugins/stripe/views.py b/src/pretix/plugins/stripe/views.py index 3f02b701f7..ee4a0043f9 100644 --- a/src/pretix/plugins/stripe/views.py +++ b/src/pretix/plugins/stripe/views.py @@ -17,14 +17,20 @@ from django.views import View from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST +from django.views.generic import FormView from django_scopes import scopes_disabled -from pretix.base.models import Event, Order, OrderPayment, Quota +from pretix.base.models import Event, Order, OrderPayment, Organizer, Quota from pretix.base.payment import PaymentException from pretix.base.services.locking import LockTimeoutException from pretix.base.settings import GlobalSettingsObject -from pretix.control.permissions import event_permission_required +from pretix.control.permissions import ( + AdministratorPermissionRequiredMixin, event_permission_required, +) +from pretix.control.views.event import DecoupleMixin +from pretix.control.views.organizer import OrganizerDetailViewMixin from pretix.multidomain.urlreverse import eventreverse +from pretix.plugins.stripe.forms import OrganizerStripeSettingsForm from pretix.plugins.stripe.models import ReferencedStripeObject from pretix.plugins.stripe.payment import StripeCC, StripeSettingsHolder from pretix.plugins.stripe.tasks import ( @@ -580,3 +586,37 @@ class ScaReturnView(StripeOrderView, View): self.order.refresh_from_db() return render(request, 'pretixplugins/stripe/sca_return.html', {'order': self.order}) + + +class OrganizerSettingsFormView(DecoupleMixin, OrganizerDetailViewMixin, AdministratorPermissionRequiredMixin, FormView): + model = Organizer + permission = 'can_change_organizer_settings' + form_class = OrganizerStripeSettingsForm + template_name = 'pretixplugins/stripe/organizer_stripe.html' + + def get_success_url(self): + return reverse('plugins:stripe:settings.connect', kwargs={ + 'organizer': self.request.organizer.slug, + }) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['obj'] = self.request.organizer + return kwargs + + @transaction.atomic + def post(self, request, *args, **kwargs): + form = self.get_form() + if form.is_valid(): + form.save() + if form.has_changed(): + self.request.organizer.log_action( + 'pretix.organizer.settings', user=self.request.user, data={ + k: form.cleaned_data.get(k) for k in form.changed_data + } + ) + messages.success(self.request, _('Your changes have been saved.')) + return redirect(self.get_success_url()) + else: + messages.error(self.request, _('We could not save your changes. See below for details.')) + return self.get(request)