diff --git a/src/pretix/plugins/stripe/migrations/0001_initial.py b/src/pretix/plugins/stripe/migrations/0001_initial.py new file mode 100644 index 0000000000..b3dcdc6b21 --- /dev/null +++ b/src/pretix/plugins/stripe/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2017-07-23 09:37 +from __future__ import unicode_literals + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('pretixbase', '0070_auto_20170719_0910'), + ] + + operations = [ + migrations.CreateModel( + name='ReferencedStripeObject', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('reference', models.CharField(db_index=True, max_length=190, unique=True)), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Order')), + ], + ), + ] diff --git a/src/pretix/plugins/stripe/migrations/__init__.py b/src/pretix/plugins/stripe/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/pretix/plugins/stripe/models.py b/src/pretix/plugins/stripe/models.py new file mode 100644 index 0000000000..0e3e6e90dd --- /dev/null +++ b/src/pretix/plugins/stripe/models.py @@ -0,0 +1,6 @@ +from django.db import models + + +class ReferencedStripeObject(models.Model): + reference = models.CharField(max_length=190, db_index=True, unique=True) + order = models.ForeignKey('pretixbase.Order') diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py index 55a625d95f..8918c75f2a 100644 --- a/src/pretix/plugins/stripe/payment.py +++ b/src/pretix/plugins/stripe/payment.py @@ -14,7 +14,9 @@ from pretix.base.payment import BasePaymentProvider, PaymentException from pretix.base.services.mail import SendMailException from pretix.base.services.orders import mark_order_paid, mark_order_refunded from pretix.base.settings import SettingsSandbox +from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.multidomain.urlreverse import build_absolute_uri +from pretix.plugins.stripe.models import ReferencedStripeObject logger = logging.getLogger('pretix.plugins.stripe') @@ -33,7 +35,7 @@ class StripeSettingsHolder(BasePaymentProvider): _('Please configure a Stripe Webhook to ' 'the following endpoint in order to automatically cancel orders when charges are refunded externally ' 'and to process asynchronous payment methods like SOFORT.'), - build_absolute_uri(self.event, 'plugins:stripe:webhook') + build_global_uri('plugins:stripe:webhook') ) @property @@ -182,6 +184,7 @@ class StripeMethod(BasePaymentProvider): raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch ' 'with us if this problem persists.')) else: + ReferencedStripeObject.objects.get_or_create(order=order, reference=charge.id) if charge.status == 'succeeded' and charge.paid: try: mark_order_paid(order, self.identifier, str(charge)) @@ -197,8 +200,9 @@ class StripeMethod(BasePaymentProvider): except SendMailException: raise PaymentException(_('There was an error sending the confirmation mail.')) elif charge.status == 'pending': - messages.warning(request, _('Your payment is pending completion. We will inform you as soon as the ' - 'payment completed.')) + if request: + messages.warning(request, _('Your payment is pending completion. We will inform you as soon as the ' + 'payment completed.')) order.payment_info = str(charge) order.save(update_fields=['payment_info']) return @@ -303,6 +307,7 @@ class StripeMethod(BasePaymentProvider): raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch ' 'with us if this problem persists.')) + ReferencedStripeObject.objects.get_or_create(order=order, reference=source.id) order.payment_info = str(source) order.save(update_fields=['payment_info']) request.session['payment_stripe_order_secret'] = order.secret diff --git a/src/pretix/plugins/stripe/urls.py b/src/pretix/plugins/stripe/urls.py index e3c010bafc..f5d7b7c879 100644 --- a/src/pretix/plugins/stripe/urls.py +++ b/src/pretix/plugins/stripe/urls.py @@ -1,10 +1,10 @@ from django.conf.urls import include, url -from .views import ReturnView, refund, webhook +from .views import ReturnView, event_webbook, refund, webhook event_patterns = [ url(r'^stripe/', include([ - url(r'^webhook/$', webhook, name='webhook'), + url(r'^webhook/$', event_webbook, name='webhook'), url(r'^return/(?P[^/]+)/(?P[^/]+)/$', ReturnView.as_view(), name='return'), ])), ] @@ -12,4 +12,5 @@ event_patterns = [ urlpatterns = [ url(r'^control/event/(?P[^/]+)/(?P[^/]+)/stripe/refund/(?P\d+)/', refund, name='refund'), + url(r'^_stripe/webhook/$', webhook, name='webhook'), ] diff --git a/src/pretix/plugins/stripe/views.py b/src/pretix/plugins/stripe/views.py index 0f0604f2ad..b01c15f094 100644 --- a/src/pretix/plugins/stripe/views.py +++ b/src/pretix/plugins/stripe/views.py @@ -20,6 +20,7 @@ from pretix.base.payment import PaymentException from pretix.base.services.orders import mark_order_paid, mark_order_refunded from pretix.control.permissions import event_permission_required from pretix.multidomain.urlreverse import eventreverse +from pretix.plugins.stripe.models import ReferencedStripeObject from pretix.plugins.stripe.payment import StripeCC from pretix.presale.utils import event_view @@ -28,7 +29,6 @@ logger = logging.getLogger('pretix.plugins.stripe') @csrf_exempt @require_POST -@event_view(require_live=False) def webhook(request, *args, **kwargs): event_json = json.loads(request.body.decode('utf-8')) @@ -38,17 +38,32 @@ def webhook(request, *args, **kwargs): # come from anywhere. if event_json['data']['object']['object'] == "charge": - return charge_webhook(request, event_json, event_json['data']['object']['id']) + func = charge_webhook + objid = event_json['data']['object']['id'] elif event_json['data']['object']['object'] == "dispute": - return charge_webhook(request, event_json, event_json['data']['object']['charge']) + func = charge_webhook + objid = event_json['data']['object']['charge'] elif event_json['data']['object']['object'] == "source": - return source_webhook(request, event_json, event_json['data']['object']['id']) + func = source_webhook + objid = event_json['data']['object']['id'] else: return HttpResponse("Not interested in this data type", status=200) + try: + rso = ReferencedStripeObject.objects.select_related('order', 'order__event').get(reference=objid) + return func(rso.order.event, event_json, objid) + except ReferencedStripeObject.DoesNotExist: + if hasattr(request, 'event'): + return func(request.event, event_json, objid) + else: + return HttpResponse("Unable to detect event", status=200) -def charge_webhook(request, event_json, charge_id): - prov = StripeCC(request.event) + +event_webbook = csrf_exempt(event_view(require_live=False)(webhook)) + + +def charge_webhook(event, event_json, charge_id): + prov = StripeCC(event) prov._init_api() try: charge = stripe.Charge.retrieve(charge_id) @@ -60,16 +75,16 @@ def charge_webhook(request, event_json, charge_id): if 'event' not in metadata: return HttpResponse('Event not given in charge metadata', status=200) - if int(metadata['event']) != request.event.pk: + if int(metadata['event']) != event.pk: return HttpResponse('Not interested in this event', status=200) try: - order = request.event.orders.get(id=metadata['order'], payment_provider__startswith='stripe') + order = event.orders.get(id=metadata['order'], payment_provider__startswith='stripe') except Order.DoesNotExist: return HttpResponse('Order not found', status=200) if order.payment_provider != prov.identifier: - prov = request.event.get_payment_providers()[order.payment_provider] + prov = event.get_payment_providers()[order.payment_provider] prov._init_api() order.log_action('pretix.plugins.stripe.event', data=event_json) @@ -77,7 +92,7 @@ def charge_webhook(request, event_json, charge_id): is_refund = charge['refunds']['total_count'] or charge['dispute'] if order.status == Order.STATUS_PAID and is_refund: RequiredAction.objects.create( - event=request.event, action_type='pretix.plugins.stripe.refund', data=json.dumps({ + event=event, action_type='pretix.plugins.stripe.refund', data=json.dumps({ 'order': order.code, 'charge': charge_id }) @@ -86,10 +101,10 @@ def charge_webhook(request, event_json, charge_id): try: mark_order_paid(order, user=None) except Quota.QuotaExceededException: - if not RequiredAction.objects.filter(event=request.event, action_type='pretix.plugins.stripe.overpaid', + if not RequiredAction.objects.filter(event=event, action_type='pretix.plugins.stripe.overpaid', data__icontains=order.code).exists(): RequiredAction.objects.create( - event=request.event, + event=event, action_type='pretix.plugins.stripe.overpaid', data=json.dumps({ 'order': order.code, @@ -100,8 +115,8 @@ def charge_webhook(request, event_json, charge_id): return HttpResponse(status=200) -def source_webhook(request, event_json, source_id): - prov = StripeCC(request.event) +def source_webhook(event, event_json, source_id): + prov = StripeCC(event) prov._init_api() try: src = stripe.Source.retrieve(source_id) @@ -113,17 +128,17 @@ def source_webhook(request, event_json, source_id): if 'event' not in metadata: return HttpResponse('Event not given in charge metadata', status=200) - if int(metadata['event']) != request.event.pk: + if int(metadata['event']) != event.pk: return HttpResponse('Not interested in this event', status=200) with transaction.atomic(): try: - order = request.event.orders.get(id=metadata['order'], payment_provider__startswith='stripe') + order = event.orders.get(id=metadata['order'], payment_provider__startswith='stripe') except Order.DoesNotExist: return HttpResponse('Order not found', status=200) if order.payment_provider != prov.identifier: - prov = request.event.get_payment_providers()[order.payment_provider] + prov = event.get_payment_providers()[order.payment_provider] prov._init_api() order.log_action('pretix.plugins.stripe.event', data=event_json) @@ -131,7 +146,7 @@ def source_webhook(request, event_json, source_id): src.status == 'chargeable') if go: try: - prov._charge_source(request, source_id, order) + prov._charge_source(None, source_id, order) except PaymentException: logger.exception('Webhook error') @@ -189,6 +204,7 @@ class ReturnView(StripeOrderView, View): prov = self.pprov prov._init_api() src = stripe.Source.retrieve(request.GET.get('source')) + print(src.client_secret, request.GET.get('client_secret')) if src.client_secret != request.GET.get('client_secret'): messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link ' 'in your emails to continue.')) @@ -197,7 +213,8 @@ class ReturnView(StripeOrderView, View): with transaction.atomic(): self.order.refresh_from_db() if self.order.status == Order.STATUS_PAID: - del request.session['payment_stripe_token'] + if 'payment_stripe_token' in request.session: + del request.session['payment_stripe_token'] return self._redirect_to_order() if src.status == 'chargeable': @@ -215,6 +232,7 @@ class ReturnView(StripeOrderView, View): return self._redirect_to_order() def _redirect_to_order(self): + print(self.request.session.get('payment_stripe_order_secret'), self.order.secret) if self.request.session.get('payment_stripe_order_secret') != self.order.secret: messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link ' 'in your emails to continue.')) diff --git a/src/tests/plugins/stripe/test_webhook.py b/src/tests/plugins/stripe/test_webhook.py index e9ae8070a1..b805855bf5 100644 --- a/src/tests/plugins/stripe/test_webhook.py +++ b/src/tests/plugins/stripe/test_webhook.py @@ -8,6 +8,7 @@ from django.utils.timezone import now from pretix.base.models import ( Event, Order, Organizer, RequiredAction, Team, User, ) +from pretix.plugins.stripe.models import ReferencedStripeObject @pytest.fixture @@ -215,3 +216,38 @@ def test_webhook_partial_refund(env, client, monkeypatch): order = env[1] order.refresh_from_db() assert order.status == Order.STATUS_REFUNDED + + +@pytest.mark.django_db +def test_webhook_organizer_level(env, client, monkeypatch): + order = env[1] + order.status = Order.STATUS_PENDING + order.save() + + charge = get_test_charge(env[1]) + monkeypatch.setattr("stripe.Charge.retrieve", lambda *args: charge) + + ReferencedStripeObject.objects.create(order=order, reference="ch_18TY6GGGWE2Ias8TZHanef25") + + client.post('/_stripe/webhook/', json.dumps( + { + "id": "evt_18otImGGWE2Ias8TUyVRDB1G", + "object": "event", + "api_version": "2016-03-07", + "created": 1472729052, + "data": { + "object": { + "id": "ch_18TY6GGGWE2Ias8TZHanef25", + "object": "charge", + # Rest of object is ignored anway + } + }, + "livemode": True, + "pending_webhooks": 1, + "request": "req_977XOWC8zk51Z9", + "type": "charge.succeeded" + } + ), content_type='application_json') + + order.refresh_from_db() + assert order.status == Order.STATUS_PAID