From 640b9c876d3799636c73be5434b38afa4483bda1 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 13 May 2020 16:43:30 +0200 Subject: [PATCH] Stripe: Lock payment object while processing refund --- src/pretix/plugins/stripe/payment.py | 3 + src/pretix/plugins/stripe/views.py | 103 ++++++++++++++------------- 2 files changed, 55 insertions(+), 51 deletions(-) diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py index d887d22014..c2616d0a93 100644 --- a/src/pretix/plugins/stripe/payment.py +++ b/src/pretix/plugins/stripe/payment.py @@ -11,6 +11,7 @@ from django import forms from django.conf import settings from django.contrib import messages from django.core import signing +from django.db import transaction from django.http import HttpRequest from django.template.loader import get_template from django.urls import reverse @@ -491,10 +492,12 @@ class StripeMethod(BasePaymentProvider): } return template.render(ctx) + @transaction.atomic() def execute_refund(self, refund: OrderRefund): self._init_api() payment_info = refund.payment.info_data + OrderPayment.objects.select_for_update().get(pk=refund.payment.pk) if not payment_info: raise PaymentException(_('No payment information found.')) diff --git a/src/pretix/plugins/stripe/views.py b/src/pretix/plugins/stripe/views.py index b874b7af9e..61d9162cfb 100644 --- a/src/pretix/plugins/stripe/views.py +++ b/src/pretix/plugins/stripe/views.py @@ -238,66 +238,67 @@ def charge_webhook(event, event_json, charge_id, rso): return HttpResponse('Order not found', status=200) payment = None - if not payment: - payment = order.payments.filter( - info__icontains=charge['id'], - provider__startswith='stripe', - amount=prov._amount_to_decimal(charge['amount']), - ).last() - if not payment: - payment = order.payments.create( - state=OrderPayment.PAYMENT_STATE_CREATED, - provider=SOURCE_TYPES.get(charge['source'].get('type', charge['source'].get('object', 'card')), 'stripe'), - amount=prov._amount_to_decimal(charge['amount']), - info=str(charge), - ) + with transaction.atomic(): + if not payment: + payment = order.payments.filter( + info__icontains=charge['id'], + provider__startswith='stripe', + amount=prov._amount_to_decimal(charge['amount']), + ).select_for_update().last() + if not payment: + payment = order.payments.create( + state=OrderPayment.PAYMENT_STATE_CREATED, + provider=SOURCE_TYPES.get(charge['source'].get('type', charge['source'].get('object', 'card')), 'stripe'), + amount=prov._amount_to_decimal(charge['amount']), + info=str(charge), + ) - if payment.provider != prov.identifier: - prov = payment.payment_provider - prov._init_api() + if payment.provider != prov.identifier: + prov = payment.payment_provider + prov._init_api() - order.log_action('pretix.plugins.stripe.event', data=event_json) + order.log_action('pretix.plugins.stripe.event', data=event_json) - is_refund = charge['refunds']['total_count'] or charge['dispute'] - if is_refund: - known_refunds = [r.info_data.get('id') for r in payment.refunds.all()] - migrated_refund_amounts = [r.amount for r in payment.refunds.all() if not r.info_data.get('id')] - for r in charge['refunds']['data']: - a = prov._amount_to_decimal(r['amount']) - if r['status'] in ('failed', 'canceled'): - continue + is_refund = charge['refunds']['total_count'] or charge['dispute'] + if is_refund: + known_refunds = [r.info_data.get('id') for r in payment.refunds.all()] + migrated_refund_amounts = [r.amount for r in payment.refunds.all() if not r.info_data.get('id')] + for r in charge['refunds']['data']: + a = prov._amount_to_decimal(r['amount']) + if r['status'] in ('failed', 'canceled'): + continue - if a in migrated_refund_amounts: - migrated_refund_amounts.remove(a) - continue - - if r['id'] not in known_refunds: - payment.create_external_refund( - amount=a, - info=str(r) - ) - if charge['dispute']: - if charge['dispute']['status'] != 'won' and charge['dispute']['id'] not in known_refunds: - a = prov._amount_to_decimal(charge['dispute']['amount']) if a in migrated_refund_amounts: migrated_refund_amounts.remove(a) - else: + continue + + if r['id'] not in known_refunds: payment.create_external_refund( amount=a, - info=str(charge['dispute']) + info=str(r) ) - elif charge['status'] == 'succeeded' and payment.state in (OrderPayment.PAYMENT_STATE_PENDING, - OrderPayment.PAYMENT_STATE_CREATED, - OrderPayment.PAYMENT_STATE_CANCELED, - OrderPayment.PAYMENT_STATE_FAILED): - try: - payment.confirm() - except LockTimeoutException: - return HttpResponse("Lock timeout, please try again.", status=503) - except Quota.QuotaExceededException: - pass - elif charge['status'] == 'failed' and payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED): - payment.fail(info=str(charge)) + if charge['dispute']: + if charge['dispute']['status'] != 'won' and charge['dispute']['id'] not in known_refunds: + a = prov._amount_to_decimal(charge['dispute']['amount']) + if a in migrated_refund_amounts: + migrated_refund_amounts.remove(a) + else: + payment.create_external_refund( + amount=a, + info=str(charge['dispute']) + ) + elif charge['status'] == 'succeeded' and payment.state in (OrderPayment.PAYMENT_STATE_PENDING, + OrderPayment.PAYMENT_STATE_CREATED, + OrderPayment.PAYMENT_STATE_CANCELED, + OrderPayment.PAYMENT_STATE_FAILED): + try: + payment.confirm() + except LockTimeoutException: + return HttpResponse("Lock timeout, please try again.", status=503) + except Quota.QuotaExceededException: + pass + elif charge['status'] == 'failed' and payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED): + payment.fail(info=str(charge)) return HttpResponse(status=200)