mirror of
https://github.com/pretix/pretix.git
synced 2026-04-22 23:12:31 +00:00
Compare commits
3 Commits
v3.2.0
...
stripe-con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02c81e0fa7 | ||
|
|
b9a911dd97 | ||
|
|
361488f3e6 |
@@ -785,8 +785,9 @@ Creating orders
|
||||
* ``locale``
|
||||
* ``sales_channel``
|
||||
* ``payment_provider`` (optional) – The identifier of the payment provider set for this order. This needs to be an
|
||||
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
|
||||
for all orders you create as paid.
|
||||
existing payment provider. You should use ``"free"`` for free orders, and we strongly advise to use ``"manual"``
|
||||
for all orders you create as paid. This field is optional when the order status is ``"n"`` or the order total is
|
||||
zero, otherwise it is required.
|
||||
* ``payment_info`` (optional) – You can pass a nested JSON object that will be set as the internal ``info``
|
||||
value of the payment object that will be created. How this value is handled is up to the payment provider and you
|
||||
should only use this if you know the specific payment provider in detail. Please keep in mind that the payment
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.2.0"
|
||||
__version__ = "3.3.0.dev0"
|
||||
|
||||
@@ -858,6 +858,9 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
order.total = sum([p.price for p in order.positions.all()]) + sum([f.value for f in order.fees.all()])
|
||||
order.save(update_fields=['total'])
|
||||
|
||||
if order.total == Decimal('0.00') and validated_data.get('status') == Order.STATUS_PAID and not payment_provider:
|
||||
payment_provider = 'free'
|
||||
|
||||
if order.total == Decimal('0.00') and validated_data.get('status') != Order.STATUS_PAID:
|
||||
order.status = Order.STATUS_PAID
|
||||
order.save()
|
||||
|
||||
@@ -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):
|
||||
</div>
|
||||
""".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
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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 %}
|
||||
<h1>
|
||||
{% trans "Stripe Connect" %}
|
||||
</h1>
|
||||
|
||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||
{% 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 %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -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<organizer>[^/]+)/(?P<event>[^/]+)/stripe/disconnect/',
|
||||
oauth_disconnect, name='oauth.disconnect'),
|
||||
url(r'^control/organizer/(?P<organizer>[^/]+)/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'),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1656,6 +1656,25 @@ def test_order_email_optional(token_client, organizer, event, item, quota, quest
|
||||
assert not o.email
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_payment_provider_optional_free(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
res['positions'][0]['price'] = '0.00'
|
||||
res['positions'][0]['status'] = 'p'
|
||||
del res['payment_provider']
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
assert not o.payments.exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_payment_info_optional(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
|
||||
Reference in New Issue
Block a user