Stripe: Lock payment object while processing refund

This commit is contained in:
Raphael Michel
2020-05-13 16:43:30 +02:00
parent 25ad2ea475
commit 640b9c876d
2 changed files with 55 additions and 51 deletions

View File

@@ -11,6 +11,7 @@ from django import forms
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core import signing from django.core import signing
from django.db import transaction
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import get_template from django.template.loader import get_template
from django.urls import reverse from django.urls import reverse
@@ -491,10 +492,12 @@ class StripeMethod(BasePaymentProvider):
} }
return template.render(ctx) return template.render(ctx)
@transaction.atomic()
def execute_refund(self, refund: OrderRefund): def execute_refund(self, refund: OrderRefund):
self._init_api() self._init_api()
payment_info = refund.payment.info_data payment_info = refund.payment.info_data
OrderPayment.objects.select_for_update().get(pk=refund.payment.pk)
if not payment_info: if not payment_info:
raise PaymentException(_('No payment information found.')) raise PaymentException(_('No payment information found.'))

View File

@@ -238,66 +238,67 @@ def charge_webhook(event, event_json, charge_id, rso):
return HttpResponse('Order not found', status=200) return HttpResponse('Order not found', status=200)
payment = None payment = None
if not payment: with transaction.atomic():
payment = order.payments.filter( if not payment:
info__icontains=charge['id'], payment = order.payments.filter(
provider__startswith='stripe', info__icontains=charge['id'],
amount=prov._amount_to_decimal(charge['amount']), provider__startswith='stripe',
).last() amount=prov._amount_to_decimal(charge['amount']),
if not payment: ).select_for_update().last()
payment = order.payments.create( if not payment:
state=OrderPayment.PAYMENT_STATE_CREATED, payment = order.payments.create(
provider=SOURCE_TYPES.get(charge['source'].get('type', charge['source'].get('object', 'card')), 'stripe'), state=OrderPayment.PAYMENT_STATE_CREATED,
amount=prov._amount_to_decimal(charge['amount']), provider=SOURCE_TYPES.get(charge['source'].get('type', charge['source'].get('object', 'card')), 'stripe'),
info=str(charge), amount=prov._amount_to_decimal(charge['amount']),
) info=str(charge),
)
if payment.provider != prov.identifier: if payment.provider != prov.identifier:
prov = payment.payment_provider prov = payment.payment_provider
prov._init_api() 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'] is_refund = charge['refunds']['total_count'] or charge['dispute']
if is_refund: if is_refund:
known_refunds = [r.info_data.get('id') for r in payment.refunds.all()] 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')] migrated_refund_amounts = [r.amount for r in payment.refunds.all() if not r.info_data.get('id')]
for r in charge['refunds']['data']: for r in charge['refunds']['data']:
a = prov._amount_to_decimal(r['amount']) a = prov._amount_to_decimal(r['amount'])
if r['status'] in ('failed', 'canceled'): if r['status'] in ('failed', 'canceled'):
continue 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: if a in migrated_refund_amounts:
migrated_refund_amounts.remove(a) migrated_refund_amounts.remove(a)
else: continue
if r['id'] not in known_refunds:
payment.create_external_refund( payment.create_external_refund(
amount=a, amount=a,
info=str(charge['dispute']) info=str(r)
) )
elif charge['status'] == 'succeeded' and payment.state in (OrderPayment.PAYMENT_STATE_PENDING, if charge['dispute']:
OrderPayment.PAYMENT_STATE_CREATED, if charge['dispute']['status'] != 'won' and charge['dispute']['id'] not in known_refunds:
OrderPayment.PAYMENT_STATE_CANCELED, a = prov._amount_to_decimal(charge['dispute']['amount'])
OrderPayment.PAYMENT_STATE_FAILED): if a in migrated_refund_amounts:
try: migrated_refund_amounts.remove(a)
payment.confirm() else:
except LockTimeoutException: payment.create_external_refund(
return HttpResponse("Lock timeout, please try again.", status=503) amount=a,
except Quota.QuotaExceededException: info=str(charge['dispute'])
pass )
elif charge['status'] == 'failed' and payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED): elif charge['status'] == 'succeeded' and payment.state in (OrderPayment.PAYMENT_STATE_PENDING,
payment.fail(info=str(charge)) 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) return HttpResponse(status=200)