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)