diff --git a/doc/development/api/payment.rst b/doc/development/api/payment.rst
index 9003cbd2b2..61fbf8df33 100644
--- a/doc/development/api/payment.rst
+++ b/doc/development/api/payment.rst
@@ -61,6 +61,8 @@ The provider class
.. autoattribute:: settings_form_fields
+ .. automethod:: settings_content_render
+
.. automethod:: checkout_form_render
.. automethod:: checkout_form
diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py
index e6dadc0860..886ed7f038 100644
--- a/src/pretix/base/payment.py
+++ b/src/pretix/base/payment.py
@@ -117,6 +117,13 @@ class BasePaymentProvider:
)),
])
+ def settings_content_render(self, request: HttpRequest) -> str:
+ """
+ When the event's administrator administrator visits the event configuration
+ page, this method is called. It may return HTML containing additional information
+ that is displayed below the form fields configured in ``settings_form_fields``.
+ """
+
@property
def checkout_form_fields(self) -> dict:
"""
diff --git a/src/pretix/control/templates/pretixcontrol/event/payment.html b/src/pretix/control/templates/pretixcontrol/event/payment.html
index dec5bf91bd..aafdfa9326 100644
--- a/src/pretix/control/templates/pretixcontrol/event/payment.html
+++ b/src/pretix/control/templates/pretixcontrol/event/payment.html
@@ -22,6 +22,9 @@
{% bootstrap_form provider.form layout='horizontal' %}
+ {% with c=provider.settings_content %}
+ {% if c %}{{ c|safe }}{% endif %}
+ {% endwith %}
{% empty %}
diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py
index e672e21d8f..8dea6bab9f 100644
--- a/src/pretix/control/views/event.py
+++ b/src/pretix/control/views/event.py
@@ -258,6 +258,7 @@ class PaymentSettings(EventPermissionRequiredMixin, TemplateView, SingleObjectMi
for k, v in provider.settings_form_fields.items()
]
)
+ provider.settings_content = provider.settings_content_render(self.request)
provider.form.prepare_fields()
providers.append(provider)
return providers
diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py
index 31f7ab65c6..37476fb9cd 100644
--- a/src/pretix/plugins/stripe/payment.py
+++ b/src/pretix/plugins/stripe/payment.py
@@ -2,6 +2,7 @@ from collections import OrderedDict
import json
import logging
from django.contrib import messages
+from django.core.urlresolvers import reverse
from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _
from django import forms
@@ -32,6 +33,13 @@ class Stripe(BasePaymentProvider):
]
)
+ def settings_content_render(self, request):
+ return "%s
%s
" % (
+ _('Please configure a Stripe Webhook to '
+ 'the following endpoint in order to automatically cancel orders when a charges are refunded externally.'),
+ request.build_absolute_uri(reverse('plugins:stripe:webhook'))
+ )
+
def checkout_is_valid_session(self, request):
return request.session.get('payment_stripe_token') != ''
@@ -51,6 +59,7 @@ class Stripe(BasePaymentProvider):
return template.render(ctx)
def _init_api(self):
+ stripe.api_version = '2015-04-07'
stripe.api_key = self.settings.get('secret_key')
def checkout_confirm_render(self, request) -> str:
@@ -65,6 +74,11 @@ class Stripe(BasePaymentProvider):
amount=int(order.total * 100),
currency=request.event.currency.lower(),
source=request.session['payment_stripe_token'],
+ metadata={
+ 'order': order.identity,
+ 'event': self.event.identity,
+ 'code': order.code
+ },
idempotency_key=self.event.identity + order.code # TODO: Use something better
)
except stripe.error.CardError as e:
@@ -83,16 +97,15 @@ class Stripe(BasePaymentProvider):
'in touch with us if this problem persists.'))
logger.error('Stripe error: %s' % str(err))
else:
- logger.info(charge)
if charge.status == 'succeeded' and charge.paid:
try:
order.mark_paid('paypal', str(charge))
messages.success(request, _('We successfully received your payment. Thank you!'))
except Quota.QuotaExceededException as e:
messages.error(request, str(e))
- messages.success(request, _('We successfully received your payment. Thank you!'))
else:
messages.warning(request, _('Stripe reported an error: %s' % charge.failure_message))
+ logger.info('Charge failed: %s' % str(charge))
order = order.clone()
order.payment_info = str(charge)
order.save()
diff --git a/src/pretix/plugins/stripe/urls.py b/src/pretix/plugins/stripe/urls.py
new file mode 100644
index 0000000000..34110a67a2
--- /dev/null
+++ b/src/pretix/plugins/stripe/urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls import url, include
+
+from .views import webhook
+
+
+urlpatterns = [
+ url(r'^stripe/', include([
+ url(r'^webhook/$', webhook, name='webhook'),
+ ])),
+]
diff --git a/src/pretix/plugins/stripe/views.py b/src/pretix/plugins/stripe/views.py
new file mode 100644
index 0000000000..e7e470ea39
--- /dev/null
+++ b/src/pretix/plugins/stripe/views.py
@@ -0,0 +1,53 @@
+import json
+import logging
+from django.http import HttpResponse
+from django.views.decorators.csrf import csrf_exempt
+from django.views.decorators.http import require_POST
+from pretix.base.models import Order, Event
+from pretix.plugins.stripe.payment import Stripe
+import stripe
+
+
+logger = logging.getLogger('pretix.plugins.stripe')
+
+
+@csrf_exempt
+@require_POST
+def webhook(request):
+ event_json = json.loads(request.body.decode('utf-8'))
+ event_type = event_json['type']
+ if event_type != 'charge.refunded':
+ # Not interested
+ return HttpResponse('Event is not a refund', status=200)
+
+ charge = event_json['data']['object']
+ if charge['object'] != 'charge':
+ return HttpResponse('Object is not a charge', status=200)
+
+ metadata = charge['metadata']
+ if 'event' not in metadata:
+ return HttpResponse('Event not given', status=200)
+
+ try:
+ event = Event.objects.current.get(identity=metadata['event'])
+ except Event.DoesNotExist:
+ return HttpResponse('Event not found', status=200)
+
+ try:
+ order = Order.objects.current.get(identity=metadata['order'])
+ except Order.DoesNotExist:
+ return HttpResponse('Order not found', status=200)
+
+ prov = Stripe(event)
+ prov._init_api()
+
+ try:
+ charge = stripe.Charge.retrieve(charge['id'])
+ except stripe.error.StripeError as err:
+ logger.error('Stripe error on webhook: %s Event data: %s' % (str(err), str(event_json)))
+ return HttpResponse('StripeError', status=500)
+
+ if charge['refunds']['total_count'] > 0 and order.status == Order.STATUS_PAID:
+ order.mark_refunded()
+
+ return HttpResponse(status=200)