diff --git a/pyproject.toml b/pyproject.toml index 67845ea858..f3f99bdc1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,7 +97,7 @@ dependencies = [ "sepaxml==2.6.*", "slimit", "static3==0.7.*", - "stripe==5.4.*", + "stripe==7.9.*", "text-unidecode==1.*", "tlds>=2020041600", "tqdm==4.*", diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py index c5dd225983..010a932166 100644 --- a/src/pretix/plugins/stripe/payment.py +++ b/src/pretix/plugins/stripe/payment.py @@ -75,7 +75,7 @@ from pretix.helpers import OF_SELF from pretix.helpers.countries import CachedCountries from pretix.helpers.http import get_client_ip from pretix.helpers.urls import build_absolute_uri as build_global_uri -from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse +from pretix.multidomain.urlreverse import build_absolute_uri from pretix.plugins.stripe.forms import StripeKeyValidator from pretix.plugins.stripe.models import ( ReferencedStripeObject, RegisteredApplePayDomain, @@ -481,6 +481,8 @@ class StripeSettingsHolder(BasePaymentProvider): class StripeMethod(BasePaymentProvider): identifier = '' method = '' + redirect_action_handling = 'iframe' # or redirect + confirmation_method = 'manual' def __init__(self, event: Event): super().__init__(event) @@ -585,7 +587,7 @@ class StripeMethod(BasePaymentProvider): return kwargs def _init_api(self): - stripe.api_version = '2022-08-01' + stripe.api_version = '2023-10-16' stripe.set_app_info( "pretix", partner_id="pp_partner_FSaz4PpKIur7Ox", @@ -593,11 +595,426 @@ class StripeMethod(BasePaymentProvider): url="https://pretix.eu" ) - def checkout_confirm_render(self, request) -> str: + def checkout_confirm_render(self, request, **kwargs) -> str: template = get_template('pretixplugins/stripe/checkout_payment_confirm.html') ctx = {'request': request, 'event': self.event, 'settings': self.settings, 'provider': self} return template.render(ctx) + def payment_pending_render(self, request, payment) -> str: + if payment.info: + payment_info = json.loads(payment.info) + else: + payment_info = None + template = get_template('pretixplugins/stripe/pending.html') + ctx = { + 'request': request, + 'event': self.event, + 'settings': self.settings, + 'provider': self, + 'order': payment.order, + 'payment': payment, + 'payment_info': payment_info, + 'payment_hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest() + } + return template.render(ctx) + + def matching_id(self, payment: OrderPayment): + return payment.info_data.get("id", None) + + def refund_matching_id(self, refund: OrderRefund): + return refund.info_data.get('id', None) + + def api_payment_details(self, payment: OrderPayment): + return { + "id": payment.info_data.get("id", None), + "payment_method": payment.info_data.get("payment_method", None) + } + + def api_refund_details(self, refund: OrderRefund): + try: + return { + "id": refund.info_data.get("id", None), + } + except JSONDecodeError: + return {} + + def payment_control_render(self, request, payment) -> str: + details = {} + if payment.info: + payment_info = json.loads(payment.info) + if 'amount' in payment_info: + payment_info['amount'] /= 10 ** settings.CURRENCY_PLACES.get(self.event.currency, 2) + if payment_info.get("latest_charge"): + details = payment_info["latest_charge"].get("payment_method_details", {}) + elif payment_info.get("charges") and payment_info["charges"]["data"]: + details = payment_info["charges"]["data"][0].get("payment_method_details", {}) + elif payment_info.get("source"): + details = payment_info["source"] + else: + payment_info = None + details.setdefault('owner', {}) + + template = get_template('pretixplugins/stripe/control.html') + ctx = { + 'request': request, + 'event': self.event, + 'settings': self.settings, + 'payment_info': payment_info, + 'payment': payment, + 'method': self.method, + 'details': details, + 'provider': self, + } + return template.render(ctx) + + def redirect(self, request, url): + if request.session.get('iframe_session', False): + return ( + build_absolute_uri(request.event, 'plugins:stripe:redirect') + + '?data=' + signing.dumps({ + 'url': url, + 'session': { + 'payment_stripe_order_secret': request.session['payment_stripe_order_secret'], + }, + }, salt='safe-redirect') + ) + else: + return str(url) + + @transaction.atomic() + def execute_refund(self, refund: OrderRefund): + self._init_api() + + payment_info = refund.payment.info_data + OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=refund.payment.pk) + + if not payment_info: + raise PaymentException(_('No payment information found.')) + + try: + if payment_info['id'].startswith('pi_'): + if 'latest_charge' in payment_info: + chargeid = payment_info['latest_charge']['id'] + else: + chargeid = payment_info['charges']['data'][0]['id'] + else: + chargeid = payment_info['id'] + + kwargs = {} + if self.settings.connect_destination: + kwargs['reverse_transfer'] = True + r = stripe.Refund.create( + charge=chargeid, + amount=self._get_amount(refund), + **self.api_kwargs, + **kwargs, + ) + except (stripe.error.InvalidRequestError, stripe.error.AuthenticationError, stripe.error.APIConnectionError) \ + as e: + if e.json_body and 'error' in e.json_body: + err = e.json_body['error'] + logger.exception('Stripe error: %s' % str(err)) + else: + err = {'message': str(e)} + logger.exception('Stripe error: %s' % str(e)) + + refund.info_data = err + refund.state = OrderRefund.REFUND_STATE_FAILED + refund.execution_date = now() + refund.save() + refund.order.log_action('pretix.event.order.refund.failed', { + 'local_id': refund.local_id, + 'provider': refund.provider, + 'error': str(e) + }) + raise PaymentException(_('We had trouble communicating with Stripe. Please try again and contact ' + 'support if the problem persists.')) + except stripe.error.StripeError as err: + logger.error('Stripe error: %s' % str(err)) + raise PaymentException(_('Stripe returned an error')) + else: + refund.info = str(r) + if r.status in ('succeeded', 'pending'): + refund.done() + elif r.status in ('failed', 'canceled'): + refund.state = OrderRefund.REFUND_STATE_FAILED + refund.execution_date = now() + refund.save() + + def shred_payment_info(self, obj: OrderPayment): + if not obj.info: + return + d = json.loads(obj.info) + + keys = ( + 'amount', 'currency', 'status', 'id', 'amount_capturable', 'amount_details', 'amount_received', + 'application', 'application_fee_amount', 'canceled_at', 'confirmation_method', 'created', 'description', + 'last_payment_error', 'payment_method', 'statement_descriptor', 'livemode' + ) + new = {k: v for k, v in d.items() if k in keys} + + if d.get("latest_charge"): + keys = ( + 'amount', 'amount_captured', 'amount_refunded', 'application', 'application_fee_amount', + 'balance_transaction', 'captured', 'created', 'currency', 'description', 'destination', + 'disputed', 'failure_balance_transaction', 'failure_code', 'failure_message', 'id', + 'livemode', 'metadata', 'object', 'on_behalf_of', 'outcome', 'paid', 'payment_intent', + 'payment_method', 'receipt_url', 'refunded', 'status', 'transfer_data', 'transfer_group', + ) + new["latest_charge"] = {k: v for k, v in d["latest_charge"].items() if k in keys} + + if d.get('source'): + new['source'] = { + 'id': d['source'].get('id'), + 'type': d['source'].get('type'), + 'brand': d['source'].get('brand'), + 'last4': d['source'].get('last4'), + 'bank_name': d['source'].get('bank_name'), + 'bank': d['source'].get('bank'), + 'bic': d['source'].get('bic'), + 'card': { + 'brand': d['source'].get('card', {}).get('brand'), + 'country': d['source'].get('card', {}).get('country'), + 'last4': d['source'].get('card', {}).get('last4'), + } + } + + new['_shredded'] = True + obj.info = json.dumps(new) + obj.save(update_fields=['info']) + + for le in obj.order.all_logentries().filter( + action_type="pretix.plugins.stripe.event" + ).exclude(data="", shredded=True): + d = le.parsed_data + if 'data' in d: + for k, v in list(d['data']['object'].items()): + if v not in ('reason', 'status', 'failure_message', 'object', 'id'): + d['data']['object'][k] = '█' + le.data = json.dumps(d) + le.shredded = True + le.save(update_fields=['data', 'shredded']) + + def payment_is_valid_session(self, request): + return request.session.get('payment_stripe_{}_payment_method_id'.format(self.method), '') != '' + + def checkout_prepare(self, request, cart): + payment_method_id = request.POST.get('stripe_{}_payment_method_id'.format(self.method), '') + request.session['payment_stripe_{}_payment_method_id'.format(self.method)] = payment_method_id + + if payment_method_id == '': + messages.warning(request, _('You may need to enable JavaScript for Stripe payments.')) + return False + return True + + def execute_payment(self, request: HttpRequest, payment: OrderPayment): + try: + return self._handle_payment_intent(request, payment) + finally: + if 'payment_stripe_{}_payment_method_id'.format(self.method) in request.session: + del request.session['payment_stripe_{}_payment_method_id'.format(self.method)] + + def is_moto(self, request, payment=None) -> bool: + return False + + def _payment_intent_kwargs(self, request, payment): + return {} + + def _handle_payment_intent(self, request, payment, intent=None): + self._init_api() + + try: + if self.payment_is_valid_session(request): + payment_method_id = request.session.get('payment_stripe_{}_payment_method_id'.format(self.method), None) + idempotency_key_seed = payment_method_id if payment_method_id is not None else payment.full_id + + params = {} + params.update(self._connect_kwargs(payment)) + params.update(self.api_kwargs) + params.update(self._payment_intent_kwargs(request, payment)) + + if self.is_moto(request, payment): + params.update({ + 'payment_method_options': { + 'card': { + 'moto': True + } + } + }) + + intent = stripe.PaymentIntent.create( + amount=self._get_amount(payment), + currency=self.event.currency.lower(), + payment_method=payment_method_id, + payment_method_types=[self.method], + confirmation_method=self.confirmation_method, + confirm=True, + description='{event}-{code}'.format( + event=self.event.slug.upper(), + code=payment.order.code + ), + statement_descriptor=self.statement_descriptor(payment), + metadata={ + 'order': str(payment.order.id), + 'event': self.event.id, + 'code': payment.order.code + }, + # TODO: Is this sufficient? + idempotency_key=str(self.event.id) + payment.order.code + idempotency_key_seed, + return_url=build_absolute_uri(self.event, 'plugins:stripe:sca.return', kwargs={ + 'order': payment.order.code, + 'payment': payment.pk, + 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), + }), + expand=['latest_charge'], + **params + ) + else: + payment_info = json.loads(payment.info) + + if 'id' in payment_info: + if not intent: + intent = stripe.PaymentIntent.retrieve( + payment_info['id'], + expand=["latest_charge"], + **self.api_kwargs + ) + else: + return + + except stripe.error.CardError as e: + if e.json_body: + err = e.json_body['error'] + logger.exception('Stripe error: %s' % str(err)) + else: + err = {'message': str(e)} + logger.exception('Stripe error: %s' % str(e)) + logger.info('Stripe card error: %s' % str(err)) + payment.fail(info={ + 'error': True, + 'message': err['message'], + }) + raise PaymentException(_('Stripe reported an error with your card: %s') % err['message']) + + except stripe.error.StripeError as e: + if e.json_body and 'error' in e.json_body: + err = e.json_body['error'] + logger.exception('Stripe error: %s' % str(err)) + + if err.get('code') == 'idempotency_key_in_use': + # Same thing happening twice – we don't want to record a failure, as that might prevent the + # other thread from succeeding. + return + else: + err = {'message': str(e)} + logger.exception('Stripe error: %s' % str(e)) + payment.fail(info={ + 'error': True, + 'message': err['message'], + }) + 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( + reference=intent.id, + defaults={'order': payment.order, 'payment': payment} + ) + if intent.status == 'requires_action': + payment.info = str(intent) + payment.state = OrderPayment.PAYMENT_STATE_CREATED + payment.save() + return build_absolute_uri(self.event, 'plugins:stripe:sca', kwargs={ + 'order': payment.order.code, + 'payment': payment.pk, + 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), + }) + + if intent.status == 'requires_confirmation': + payment.info = str(intent) + payment.state = OrderPayment.PAYMENT_STATE_CREATED + payment.save() + self._confirm_payment_intent(request, payment) + + elif intent.status == 'succeeded' and intent.latest_charge.paid: + try: + payment.info = str(intent) + payment.confirm() + except Quota.QuotaExceededException as e: + raise PaymentException(str(e)) + + except SendMailException: + raise PaymentException(_('There was an error sending the confirmation mail.')) + elif intent.status == 'processing': + if request: + messages.warning(request, _('Your payment is pending completion. We will inform you as soon as the ' + 'payment completed.')) + payment.info = str(intent) + payment.state = OrderPayment.PAYMENT_STATE_PENDING + payment.save() + return + elif intent.status == 'requires_payment_method': + if request: + messages.warning(request, _('Your payment failed. Please try again.')) + payment.fail(info=str(intent)) + return + else: + logger.info('Charge failed: %s' % str(intent)) + payment.fail(info=str(intent)) + raise PaymentException(_('Stripe reported an error: %s') % intent.last_payment_error.message) + + def _confirm_payment_intent(self, request, payment): + self._init_api() + + try: + payment_info = json.loads(payment.info) + + intent = stripe.PaymentIntent.confirm( + payment_info['id'], + return_url=build_absolute_uri(self.event, 'plugins:stripe:sca.return', kwargs={ + 'order': payment.order.code, + 'payment': payment.pk, + 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), + }), + expand=["latest_charge"], + **self.api_kwargs + ) + + payment.info = str(intent) + payment.save() + + self._handle_payment_intent(request, payment) + except stripe.error.CardError as e: + if e.json_body: + err = e.json_body['error'] + logger.exception('Stripe error: %s' % str(err)) + else: + err = {'message': str(e)} + logger.exception('Stripe error: %s' % str(e)) + logger.info('Stripe card error: %s' % str(err)) + payment.fail(info={ + 'error': True, + 'message': err['message'], + }) + raise PaymentException(_('Stripe reported an error with your card: %s') % err['message']) + except stripe.error.InvalidRequestError as e: + if e.json_body: + err = e.json_body['error'] + logger.exception('Stripe error: %s' % str(err)) + else: + err = {'message': str(e)} + logger.exception('Stripe error: %s' % str(e)) + payment.fail(info={ + 'error': True, + 'message': err['message'], + }) + raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch ' + 'with us if this problem persists.')) + + +class StripeSourceMethod(StripeMethod): + def payment_is_valid_session(self, request): + return True + def _charge_source(self, request, source, payment): try: params = {} @@ -688,120 +1105,6 @@ class StripeMethod(BasePaymentProvider): payment.fail(info=str(charge)) raise PaymentException(_('Stripe reported an error: %s') % charge.failure_message) - def payment_pending_render(self, request, payment) -> str: - if payment.info: - payment_info = json.loads(payment.info) - else: - payment_info = None - template = get_template('pretixplugins/stripe/pending.html') - ctx = { - 'request': request, - 'event': self.event, - 'settings': self.settings, - 'provider': self, - 'order': payment.order, - 'payment': payment, - 'payment_info': payment_info, - 'payment_hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest() - } - return template.render(ctx) - - def matching_id(self, payment: OrderPayment): - return payment.info_data.get("id", None) - - def refund_matching_id(self, refund: OrderRefund): - return refund.info_data.get('id', None) - - def api_payment_details(self, payment: OrderPayment): - return { - "id": payment.info_data.get("id", None), - "payment_method": payment.info_data.get("payment_method", None) - } - - def api_refund_details(self, refund: OrderRefund): - try: - return { - "id": refund.info_data.get("id", None), - } - except JSONDecodeError: - return {} - - def payment_control_render(self, request, payment) -> str: - if payment.info: - payment_info = json.loads(payment.info) - if 'amount' in payment_info: - payment_info['amount'] /= 10 ** settings.CURRENCY_PLACES.get(self.event.currency, 2) - else: - payment_info = None - template = get_template('pretixplugins/stripe/control.html') - ctx = { - 'request': request, - 'event': self.event, - 'settings': self.settings, - 'payment_info': payment_info, - 'payment': payment, - 'method': self.method, - 'provider': self, - } - 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(of=OF_SELF).get(pk=refund.payment.pk) - - if not payment_info: - raise PaymentException(_('No payment information found.')) - - try: - if payment_info['id'].startswith('pi_'): - chargeid = payment_info['charges']['data'][0]['id'] - else: - chargeid = payment_info['id'] - - ch = stripe.Charge.retrieve(chargeid, **self.api_kwargs) - kwargs = {} - if self.settings.connect_destination: - kwargs['reverse_transfer'] = True - r = ch.refunds.create( - amount=self._get_amount(refund), - **kwargs, - ) - ch.refresh() - except (stripe.error.InvalidRequestError, stripe.error.AuthenticationError, stripe.error.APIConnectionError) \ - as e: - if e.json_body and 'error' in e.json_body: - err = e.json_body['error'] - logger.exception('Stripe error: %s' % str(err)) - else: - err = {'message': str(e)} - logger.exception('Stripe error: %s' % str(e)) - - refund.info_data = err - refund.state = OrderRefund.REFUND_STATE_FAILED - refund.execution_date = now() - refund.save() - refund.order.log_action('pretix.event.order.refund.failed', { - 'local_id': refund.local_id, - 'provider': refund.provider, - 'error': str(e) - }) - raise PaymentException(_('We had trouble communicating with Stripe. Please try again and contact ' - 'support if the problem persists.')) - except stripe.error.StripeError as err: - logger.error('Stripe error: %s' % str(err)) - raise PaymentException(_('Stripe returned an error')) - else: - refund.info = str(r) - if r.status in ('succeeded', 'pending'): - refund.done() - elif r.status in ('failed', 'canceled'): - refund.state = OrderRefund.REFUND_STATE_FAILED - refund.execution_date = now() - refund.save() - def execute_payment(self, request: HttpRequest, payment: OrderPayment): self._init_api() try: @@ -836,284 +1139,41 @@ class StripeMethod(BasePaymentProvider): request.session['payment_stripe_order_secret'] = payment.order.secret return self.redirect(request, source.redirect.url) - def redirect(self, request, url): - if request.session.get('iframe_session', False): - return ( - build_absolute_uri(request.event, 'plugins:stripe:redirect') + - '?data=' + signing.dumps({ - 'url': url, - 'session': { - 'payment_stripe_order_secret': request.session['payment_stripe_order_secret'], - }, - }, salt='safe-redirect') - ) - else: - return str(url) - def shred_payment_info(self, obj: OrderPayment): - if not obj.info: - return - d = json.loads(obj.info) - new = {} - if d.get('source'): - new['source'] = { - 'id': d['source'].get('id'), - 'type': d['source'].get('type'), - 'brand': d['source'].get('brand'), - 'last4': d['source'].get('last4'), - 'bank_name': d['source'].get('bank_name'), - 'bank': d['source'].get('bank'), - 'bic': d['source'].get('bic'), - 'card': { - 'brand': d['source'].get('card', {}).get('brand'), - 'country': d['source'].get('card', {}).get('cuntry'), - 'last4': d['source'].get('card', {}).get('last4'), - } - } - if 'amount' in d: - new['amount'] = d['amount'] - if 'currency' in d: - new['currency'] = d['currency'] - if 'status' in d: - new['status'] = d['status'] - if 'id' in d: - new['id'] = d['id'] - - new['_shredded'] = True - obj.info = json.dumps(new) - obj.save(update_fields=['info']) - - for le in obj.order.all_logentries().filter( - action_type="pretix.plugins.stripe.event" - ).exclude(data="", shredded=True): - d = le.parsed_data - if 'data' in d: - for k, v in list(d['data']['object'].items()): - if v not in ('reason', 'status', 'failure_message', 'object', 'id'): - d['data']['object'][k] = '█' - le.data = json.dumps(d) - le.shredded = True - le.save(update_fields=['data', 'shredded']) - - -class StripePaymentIntentMethod(StripeMethod): - identifier = '' - method = '' - redirect_action_handling = 'iframe' # or redirect +class StripeRedirectMethod(StripeMethod): + redirect_action_handling = "redirect" def payment_is_valid_session(self, request): - return request.session.get('payment_stripe_{}_payment_method_id'.format(self.method), '') != '' - - def checkout_prepare(self, request, cart): - payment_method_id = request.POST.get('stripe_{}_payment_method_id'.format(self.method), '') - request.session['payment_stripe_{}_payment_method_id'.format(self.method)] = payment_method_id - - if payment_method_id == '': - messages.warning(request, _('You may need to enable JavaScript for Stripe payments.')) - return False - return True - - def execute_payment(self, request: HttpRequest, payment: OrderPayment): - try: - return self._handle_payment_intent(request, payment) - finally: - del request.session['payment_stripe_{}_payment_method_id'.format(self.method)] - - def is_moto(self, request, payment=None) -> bool: + # This does not have a payment_method_id, so we set it manually to None during checkout. + # But we still need to check for its presence here. + if "payment_stripe_{}_payment_method_id".format(self.method) in request.session: + return True return False + def checkout_prepare(self, request, cart): + # This does not have a payment_method_id, so we set it manually to None during checkout, so that we can + # verify later on if we are in or outside the checkout process. + request.session["payment_stripe_{}_payment_method_id".format(self.method)] = None + return True + def _payment_intent_kwargs(self, request, payment): - return {} + return { + "payment_method_data": { + "type": self.method, + } + } - def _handle_payment_intent(self, request, payment, intent=None): - self._init_api() - - try: - if self.payment_is_valid_session(request): - payment_method_id = request.session.get('payment_stripe_{}_payment_method_id'.format(self.method), None) - idempotency_key_seed = payment_method_id if payment_method_id is not None else payment.full_id - - params = {} - params.update(self._connect_kwargs(payment)) - params.update(self.api_kwargs) - params.update(self._payment_intent_kwargs(request, payment)) - - if self.is_moto(request, payment): - params.update({ - 'payment_method_options': { - 'card': { - 'moto': True - } - } - }) - - intent = stripe.PaymentIntent.create( - amount=self._get_amount(payment), - currency=self.event.currency.lower(), - payment_method=payment_method_id, - payment_method_types=[self.method], - confirmation_method='manual', - confirm=True, - description='{event}-{code}'.format( - event=self.event.slug.upper(), - code=payment.order.code - ), - statement_descriptor=self.statement_descriptor(payment), - metadata={ - 'order': str(payment.order.id), - 'event': self.event.id, - 'code': payment.order.code - }, - # TODO: Is this sufficient? - idempotency_key=str(self.event.id) + payment.order.code + idempotency_key_seed, - return_url=build_absolute_uri(self.event, 'plugins:stripe:sca.return', kwargs={ - 'order': payment.order.code, - 'payment': payment.pk, - 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), - }), - **params - ) - else: - payment_info = json.loads(payment.info) - - if 'id' in payment_info: - if not intent: - intent = stripe.PaymentIntent.retrieve( - payment_info['id'], - **self.api_kwargs - ) - else: - return - - except stripe.error.CardError as e: - if e.json_body: - err = e.json_body['error'] - logger.exception('Stripe error: %s' % str(err)) - else: - err = {'message': str(e)} - logger.exception('Stripe error: %s' % str(e)) - logger.info('Stripe card error: %s' % str(err)) - payment.fail(info={ - 'error': True, - 'message': err['message'], - }) - raise PaymentException(_('Stripe reported an error with your card: %s') % err['message']) - - except stripe.error.StripeError as e: - if e.json_body and 'error' in e.json_body: - err = e.json_body['error'] - logger.exception('Stripe error: %s' % str(err)) - - if err.get('code') == 'idempotency_key_in_use': - # Same thing happening twice – we don't want to record a failure, as that might prevent the - # other thread from succeeding. - return - else: - err = {'message': str(e)} - logger.exception('Stripe error: %s' % str(e)) - payment.fail(info={ - 'error': True, - 'message': err['message'], - }) - 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( - reference=intent.id, - defaults={'order': payment.order, 'payment': payment} - ) - if intent.status == 'requires_action': - payment.info = str(intent) - payment.state = OrderPayment.PAYMENT_STATE_CREATED - payment.save() - return build_absolute_uri(self.event, 'plugins:stripe:sca', kwargs={ - 'order': payment.order.code, - 'payment': payment.pk, - 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), - }) - - if intent.status == 'requires_confirmation': - payment.info = str(intent) - payment.state = OrderPayment.PAYMENT_STATE_CREATED - payment.save() - self._confirm_payment_intent(request, payment) - - elif intent.status == 'succeeded' and intent.charges.data[-1].paid: - try: - payment.info = str(intent) - payment.confirm() - except Quota.QuotaExceededException as e: - raise PaymentException(str(e)) - - except SendMailException: - raise PaymentException(_('There was an error sending the confirmation mail.')) - elif intent.status == 'processing': - if request: - messages.warning(request, _('Your payment is pending completion. We will inform you as soon as the ' - 'payment completed.')) - payment.info = str(intent) - payment.state = OrderPayment.PAYMENT_STATE_PENDING - payment.save() - return - elif intent.status == 'requires_payment_method': - if request: - messages.warning(request, _('Your payment failed. Please try again.')) - payment.fail(info=str(intent)) - return - else: - logger.info('Charge failed: %s' % str(intent)) - payment.fail(info=str(intent)) - raise PaymentException(_('Stripe reported an error: %s') % intent.last_payment_error.message) - - def _confirm_payment_intent(self, request, payment): - self._init_api() - - try: - payment_info = json.loads(payment.info) - - intent = stripe.PaymentIntent.confirm( - payment_info['id'], - return_url=build_absolute_uri(self.event, 'plugins:stripe:sca.return', kwargs={ - 'order': payment.order.code, - 'payment': payment.pk, - 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), - }), - **self.api_kwargs - ) - - payment.info = str(intent) - payment.save() - - self._handle_payment_intent(request, payment) - except stripe.error.CardError as e: - if e.json_body: - err = e.json_body['error'] - logger.exception('Stripe error: %s' % str(err)) - else: - err = {'message': str(e)} - logger.exception('Stripe error: %s' % str(e)) - logger.info('Stripe card error: %s' % str(err)) - payment.fail(info={ - 'error': True, - 'message': err['message'], - }) - raise PaymentException(_('Stripe reported an error with your card: %s') % err['message']) - except stripe.error.InvalidRequestError as e: - if e.json_body: - err = e.json_body['error'] - logger.exception('Stripe error: %s' % str(err)) - else: - err = {'message': str(e)} - logger.exception('Stripe error: %s' % str(e)) - payment.fail(info={ - 'error': True, - 'message': err['message'], - }) - raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch ' - 'with us if this problem persists.')) + def payment_form_render(self, request) -> str: + template = get_template('pretixplugins/stripe/checkout_payment_form_simple_noform.html') + ctx = { + 'request': request, + 'event': self.event, + 'settings': self.settings, + } + return template.render(ctx) -class StripeCC(StripePaymentIntentMethod): +class StripeCC(StripeMethod): identifier = 'stripe' verbose_name = _('Credit card via Stripe') public_name = _('Credit card') @@ -1187,8 +1247,8 @@ class StripeCC(StripePaymentIntentMethod): def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: - if "charges" in pi: - card = pi["charges"]["data"][0]["payment_method_details"]["card"] + if "latest_charge" in pi: + card = pi["latest_charge"]["payment_method_details"]["card"] else: card = pi["source"]["card"] except: @@ -1200,7 +1260,7 @@ class StripeCC(StripePaymentIntentMethod): f'{_("expires {month}/{year}").format(month=card.get("exp_month"), year=card.get("exp_year"))}' -class StripeSEPADirectDebit(StripePaymentIntentMethod): +class StripeSEPADirectDebit(StripeMethod): identifier = 'stripe_sepa_debit' verbose_name = _('SEPA Debit via Stripe') public_name = _('SEPA Debit') @@ -1317,7 +1377,7 @@ class StripeSEPADirectDebit(StripePaymentIntentMethod): def execute_payment(self, request: HttpRequest, payment: OrderPayment): try: - super().execute_payment(request, payment) + return super().execute_payment(request, payment) finally: fields = ['accountname', 'line1', 'postal_code', 'city', 'country'] for field in fields: @@ -1325,7 +1385,7 @@ class StripeSEPADirectDebit(StripePaymentIntentMethod): del request.session['payment_stripe_sepa_debit_{}'.format(field)] -class StripeAffirm(StripePaymentIntentMethod): +class StripeAffirm(StripeMethod): identifier = 'stripe_affirm' verbose_name = _('Affirm via Stripe') public_name = _('Affirm') @@ -1369,29 +1429,13 @@ class StripeAffirm(StripePaymentIntentMethod): return template.render(ctx) -class StripeKlarna(StripePaymentIntentMethod): +class StripeKlarna(StripeRedirectMethod): identifier = "stripe_klarna" verbose_name = _("Klarna via Stripe") public_name = _("Klarna") method = "klarna" - redirect_action_handling = "redirect" allowed_countries = {"US", "CA", "AU", "NZ", "GB", "IE", "FR", "ES", "DE", "AT", "BE", "DK", "FI", "IT", "NL", "NO", "SE"} - def payment_is_valid_session(self, request): - # Klarna does not have a payment_method_id, so we set it manually to None during checkout. - # But we still need to check for its presence here. - if "payment_stripe_{}_payment_method_id".format(self.method) in request.session: - return True - return False - - def checkout_prepare(self, request, cart): - # Klarna does not have a payment_method_id, so we set it manually to None during checkout, so that we can - # verify later on if we are in or outside the checkout process. - request.session[ - "payment_stripe_{}_payment_method_id".format(self.method) - ] = None - return True - def _detect_country(self, request, order=None): def get_invoice_address(): if order and getattr(order, 'invoice_address', None): @@ -1465,11 +1509,7 @@ class StripeKlarna(StripePaymentIntentMethod): return None -class StripeGiropay(StripeMethod): - identifier = 'stripe_giropay' - verbose_name = _('giropay via Stripe') - public_name = _('giropay') - method = 'giropay' +class StripeRedirectWithAccountNamePaymentIntentMethod(StripeRedirectMethod): def payment_form_render(self, request) -> str: template = get_template('pretixplugins/stripe/checkout_payment_form_simple.html') @@ -1487,218 +1527,107 @@ class StripeGiropay(StripeMethod): ('account', forms.CharField(label=_('Account holder'))), ]) - def _create_source(self, request, payment): + def execute_payment(self, request: HttpRequest, payment: OrderPayment): try: - source = stripe.Source.create( - type='giropay', - amount=self._get_amount(payment), - currency=self.event.currency.lower(), - metadata={ - 'order': str(payment.order.id), - 'event': self.event.id, - 'code': payment.order.code - }, - owner={ - 'name': request.session.get('payment_stripe_giropay_account') or gettext('unknown name') - }, - statement_descriptor=self.statement_descriptor(payment, 35), - redirect={ - 'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={ - 'order': payment.order.code, - 'payment': payment.pk, - 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), - }) - }, - **self.api_kwargs - ) - return source + return super().execute_payment(request, payment) finally: - if 'payment_stripe_giropay_account' in request.session: - del request.session['payment_stripe_giropay_account'] - - def payment_is_valid_session(self, request): - return ( - request.session.get('payment_stripe_giropay_account', '') != '' - ) + if f'payment_stripe_{self.method}_account' in request.session: + del request.session[f'payment_stripe_{self.method}_account'] def checkout_prepare(self, request, cart): form = self.payment_form(request) if form.is_valid(): - request.session['payment_stripe_giropay_account'] = form.cleaned_data['account'] + request.session[f"payment_stripe_{self.method}_payment_method_id"] = None + request.session[f'payment_stripe_{self.method}_account'] = form.cleaned_data['account'] return True return False + +class StripeGiropay(StripeRedirectWithAccountNamePaymentIntentMethod): + identifier = 'stripe_giropay' + verbose_name = _('giropay via Stripe') + public_name = _('giropay') + method = 'giropay' + + def _payment_intent_kwargs(self, request, payment): + return { + "payment_method_data": { + "type": "giropay", + "giropay": {}, + "billing_details": { + "name": request.session.get(f"payment_stripe_{self.method}_account") or gettext("unknown name") + }, + } + } + def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: - return gettext('Bank account at {bank}').format(bank=pi["source"]["giropay"]["bank_name"]) + return gettext('Bank account at {bank}').format( + bank=( + pi.get("latest_charge", {}).get("payment_method_details", {}).get("giropay", {}).get("bank_name") or + pi.get("source", {}).get("giropay", {}).get("bank_name", "?") + ) + ) except: logger.exception('Could not parse payment data') return super().payment_presale_render(payment) -class StripeIdeal(StripeMethod): +class StripeIdeal(StripeRedirectMethod): identifier = 'stripe_ideal' verbose_name = _('iDEAL via Stripe') public_name = _('iDEAL') method = 'ideal' - def payment_form_render(self, request) -> str: - template = get_template('pretixplugins/stripe/checkout_payment_form_simple_noform.html') - ctx = { - 'request': request, - 'event': self.event, - 'settings': self.settings, - } - return template.render(ctx) - - def _create_source(self, request, payment): - source = stripe.Source.create( - type='ideal', - amount=self._get_amount(payment), - currency=self.event.currency.lower(), - metadata={ - 'order': str(payment.order.id), - 'event': self.event.id, - 'code': payment.order.code - }, - statement_descriptor=self.statement_descriptor(payment), - redirect={ - 'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={ - 'order': payment.order.code, - 'payment': payment.pk, - 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), - }) - }, - **self.api_kwargs - ) - return source - - def payment_is_valid_session(self, request): - return True - - def checkout_prepare(self, request, cart): - return True - def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: - return gettext('Bank account at {bank}').format(bank=pi["source"]["ideal"]["bank"]) + return gettext('Bank account at {bank}').format( + bank=( + pi.get("latest_charge", {}).get("payment_method_details", {}).get("ideal", {}).get("bank") or + pi.get("source", {}).get("ideal", {}).get("bank", "?") + ).replace("_", " ").title() + ) except: logger.exception('Could not parse payment data') return super().payment_presale_render(payment) -class StripeAlipay(StripeMethod): +class StripeAlipay(StripeRedirectMethod): identifier = 'stripe_alipay' verbose_name = _('Alipay via Stripe') public_name = _('Alipay') method = 'alipay' - - def payment_form_render(self, request) -> str: - template = get_template('pretixplugins/stripe/checkout_payment_form_simple_noform.html') - ctx = { - 'request': request, - 'event': self.event, - 'settings': self.settings, - } - return template.render(ctx) - - def _create_source(self, request, payment): - source = stripe.Source.create( - type='alipay', - amount=self._get_amount(payment), - currency=self.event.currency.lower(), - metadata={ - 'order': str(payment.order.id), - 'event': self.event.id, - 'code': payment.order.code - }, - redirect={ - 'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={ - 'order': payment.order.code, - 'payment': payment.pk, - 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), - }) - }, - **self.api_kwargs - ) - return source - - def payment_is_valid_session(self, request): - return True - - def checkout_prepare(self, request, cart): - return True + confirmation_method = 'automatic' -class StripeBancontact(StripeMethod): +class StripeBancontact(StripeRedirectWithAccountNamePaymentIntentMethod): identifier = 'stripe_bancontact' verbose_name = _('Bancontact via Stripe') public_name = _('Bancontact') method = 'bancontact' - def payment_form_render(self, request) -> str: - template = get_template('pretixplugins/stripe/checkout_payment_form_simple.html') - ctx = { - 'request': request, - 'event': self.event, - 'settings': self.settings, - 'form': self.payment_form(request) + def _payment_intent_kwargs(self, request, payment): + return { + "payment_method_data": { + "type": "bancontact", + "giropay": {}, + "billing_details": { + "name": request.session.get(f"payment_stripe_{self.method}_account") or gettext("unknown name") + }, + } } - return template.render(ctx) - - @property - def payment_form_fields(self): - return OrderedDict([ - ('account', forms.CharField(label=_('Account holder'), min_length=3)), - ]) - - def _create_source(self, request, payment): - try: - source = stripe.Source.create( - type='bancontact', - amount=self._get_amount(payment), - currency=self.event.currency.lower(), - metadata={ - 'order': str(payment.order.id), - 'event': self.event.id, - 'code': payment.order.code - }, - owner={ - 'name': request.session.get('payment_stripe_bancontact_account') or gettext('unknown name') - }, - statement_descriptor=self.statement_descriptor(payment, 35), - redirect={ - 'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={ - 'order': payment.order.code, - 'payment': payment.pk, - 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), - }) - }, - **self.api_kwargs - ) - return source - finally: - if 'payment_stripe_bancontact_account' in request.session: - del request.session['payment_stripe_bancontact_account'] - - def payment_is_valid_session(self, request): - return ( - request.session.get('payment_stripe_bancontact_account', '') != '' - ) - - def checkout_prepare(self, request, cart): - form = self.payment_form(request) - if form.is_valid(): - request.session['payment_stripe_bancontact_account'] = form.cleaned_data['account'] - return True - return False def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: - return gettext('Bank account at {bank}').format(bank=pi["source"]["bancontact"]["bank_name"]) + return gettext('Bank account at {bank}').format( + bank=( + pi.get("latest_charge", {}).get("payment_method_details", {}).get("bancontact", {}).get("bank_name") or + pi.get("source", {}).get("bancontact", {}).get("bank_name", "?") + ) + ) except: logger.exception('Could not parse payment data') return super().payment_presale_render(payment) @@ -1732,40 +1661,32 @@ class StripeSofort(StripeMethod): ))), ]) - def _create_source(self, request, payment): - source = stripe.Source.create( - type='sofort', - amount=self._get_amount(payment), - currency=self.event.currency.lower(), - metadata={ - 'order': str(payment.order.id), - 'event': self.event.id, - 'code': payment.order.code - }, - statement_descriptor=self.statement_descriptor(payment, 35), - sofort={ - 'country': request.session.get('payment_stripe_sofort_bank_country'), - }, - redirect={ - 'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={ - 'order': payment.order.code, - 'payment': payment.pk, - 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), - }) - }, - **self.api_kwargs - ) - return source + def _payment_intent_kwargs(self, request, payment): + return { + "payment_method_data": { + "type": "sofort", + "sofort": { + "country": (request.session.get(f"payment_stripe_{self.method}_bank_country") or "DE").upper() + }, + } + } + + def execute_payment(self, request: HttpRequest, payment: OrderPayment): + try: + return super().execute_payment(request, payment) + finally: + if f'payment_stripe_{self.method}_bank_country' in request.session: + del request.session[f'payment_stripe_{self.method}_bank_country'] def payment_is_valid_session(self, request): return ( - request.session.get('payment_stripe_sofort_bank_country', '') != '' + request.session.get(f'payment_stripe_{self.method}_bank_country', '') != '' ) def checkout_prepare(self, request, cart): form = self.payment_form(request) if form.is_valid(): - request.session['payment_stripe_sofort_bank_country'] = form.cleaned_data['bank_country'] + request.session[f'payment_stripe_{self.method}_bank_country'] = form.cleaned_data['bank_country'] return True return False @@ -1781,79 +1702,38 @@ class StripeSofort(StripeMethod): return super().payment_presale_render(payment) -class StripeEPS(StripeMethod): +class StripeEPS(StripeRedirectWithAccountNamePaymentIntentMethod): identifier = 'stripe_eps' verbose_name = _('EPS via Stripe') public_name = _('EPS') method = 'eps' - def payment_form_render(self, request) -> str: - template = get_template('pretixplugins/stripe/checkout_payment_form_simple.html') - ctx = { - 'request': request, - 'event': self.event, - 'settings': self.settings, - 'form': self.payment_form(request) + def _payment_intent_kwargs(self, request, payment): + return { + "payment_method_data": { + "type": "eps", + "giropay": {}, + "billing_details": { + "name": request.session.get(f"payment_stripe_{self.method}_account") or gettext("unknown name") + }, + } } - return template.render(ctx) - - @property - def payment_form_fields(self): - return OrderedDict([ - ('account', forms.CharField(label=_('Account holder'))), - ]) - - def _create_source(self, request, payment): - try: - source = stripe.Source.create( - type='eps', - amount=self._get_amount(payment), - currency=self.event.currency.lower(), - metadata={ - 'order': str(payment.order.id), - 'event': self.event.id, - 'code': payment.order.code - }, - owner={ - 'name': request.session.get('payment_stripe_eps_account') or gettext('unknown name') - }, - statement_descriptor=self.statement_descriptor(payment), - redirect={ - 'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={ - 'order': payment.order.code, - 'payment': payment.pk, - 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), - }) - }, - **self.api_kwargs - ) - return source - finally: - if 'payment_stripe_eps_account' in request.session: - del request.session['payment_stripe_eps_account'] - - def payment_is_valid_session(self, request): - return ( - request.session.get('payment_stripe_eps_account', '') != '' - ) - - def checkout_prepare(self, request, cart): - form = self.payment_form(request) - if form.is_valid(): - request.session['payment_stripe_eps_account'] = form.cleaned_data['account'] - return True - return False def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: - return gettext('Bank account at {bank}').format(bank=pi["source"]["eps"]["bank"].replace('_', '').title()) + return gettext('Bank account at {bank}').format( + bank=( + pi.get("latest_charge", {}).get("payment_method_details", {}).get("eps", {}).get("bank") or + pi.get("source", {}).get("eps", {}).get("bank", "?") + ).replace("_", " ").title() + ) except: logger.exception('Could not parse payment data') return super().payment_presale_render(payment) -class StripeMultibanco(StripeMethod): +class StripeMultibanco(StripeSourceMethod): identifier = 'stripe_multibanco' verbose_name = _('Multibanco via Stripe') public_name = _('Multibanco') @@ -1900,138 +1780,59 @@ class StripeMultibanco(StripeMethod): return True -class StripePrzelewy24(StripeMethod): +class StripePrzelewy24(StripeRedirectMethod): identifier = 'stripe_przelewy24' verbose_name = _('Przelewy24 via Stripe') public_name = _('Przelewy24') - method = 'przelewy24' + method = 'p24' - def payment_form_render(self, request) -> str: - template = get_template('pretixplugins/stripe/checkout_payment_form_simple_noform.html') - ctx = { - 'request': request, - 'event': self.event, - 'settings': self.settings, - 'form': self.payment_form(request) + def _payment_intent_kwargs(self, request, payment): + return { + "payment_method_data": { + "type": "p24", + "billing_details": { + "email": payment.order.email + }, + } } - return template.render(ctx) - def _create_source(self, request, payment): - source = stripe.Source.create( - type='p24', - amount=self._get_amount(payment), - currency=self.event.currency.lower(), - metadata={ - 'order': str(payment.order.id), - 'event': self.event.id, - 'code': payment.order.code - }, - owner={ - 'email': payment.order.email - }, - statement_descriptor=self.statement_descriptor(payment, 35), - redirect={ - 'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={ - 'order': payment.order.code, - 'payment': payment.pk, - 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), - }) - }, - **self.api_kwargs - ) - return source - - def payment_is_valid_session(self, request): - return True - - def checkout_prepare(self, request, cart): - return True + @property + def is_enabled(self) -> bool: + return self.settings.get('_enabled', as_type=bool) and self.settings.get('method_przelewy24', as_type=bool) def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: - return gettext('Bank account at {bank}').format(bank=pi["source"]["p24"]["bank"].replace('_', '').title()) + return gettext('Bank account at {bank}').format( + bank=( + pi.get("latest_charge", {}).get("payment_method_details", {}).get("p24", {}).get("bank") or + pi.get("source", {}).get("p24", {}).get("bank", "?") + ).replace("_", " ").title() + ) except: logger.exception('Could not parse payment data') return super().payment_presale_render(payment) -class StripeWeChatPay(StripeMethod): +class StripeWeChatPay(StripeRedirectMethod): identifier = 'stripe_wechatpay' verbose_name = _('WeChat Pay via Stripe') public_name = _('WeChat Pay') - method = 'wechatpay' + method = 'wechat_pay' + confirmation_method = 'automatic' - def payment_form_render(self, request) -> str: - template = get_template('pretixplugins/stripe/checkout_payment_form_simple_noform.html') - ctx = { - 'request': request, - 'event': self.event, - 'settings': self.settings, - 'form': self.payment_form(request) + @property + def is_enabled(self) -> bool: + return self.settings.get('_enabled', as_type=bool) and self.settings.get('method_wechatpay', as_type=bool) + + def _payment_intent_kwargs(self, request, payment): + return { + "payment_method_data": { + "type": "wechat_pay", + }, + "payment_method_options": { + "wechat_pay": { + "client": "web" + }, + } } - return template.render(ctx) - - def _create_source(self, request, payment): - source = stripe.Source.create( - type='wechat', - amount=self._get_amount(payment), - currency=self.event.currency.lower(), - metadata={ - 'order': str(payment.order.id), - 'event': self.event.id, - 'code': payment.order.code - }, - statement_descriptor=self.statement_descriptor(payment, 32), - redirect={ - 'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={ - 'order': payment.order.code, - 'payment': payment.pk, - 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), - }) - }, - **self.api_kwargs - ) - return source - - def payment_is_valid_session(self, request): - return True - - def checkout_prepare(self, request, cart): - return True - - def execute_payment(self, request: HttpRequest, payment: OrderPayment): - self._init_api() - try: - source = self._create_source(request, payment) - - except stripe.error.StripeError as e: - if e.json_body and 'err' in e.json_body: - err = e.json_body['error'] - logger.exception('Stripe error: %s' % str(err)) - - if err.get('code') == 'idempotency_key_in_use': - # Same thing happening twice – we don't want to record a failure, as that might prevent the - # other thread from succeeding. - return - else: - err = {'message': str(e)} - logger.exception('Stripe error: %s' % str(e)) - payment.fail(info={ - 'error': True, - 'message': err['message'], - }) - 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( - reference=source.id, - defaults={'order': payment.order, 'payment': payment} - ) - payment.info = str(source) - payment.save() - - return eventreverse(request.event, 'presale:event.order', kwargs={ - 'order': payment.order.code, - 'secret': payment.order.secret - }) diff --git a/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js b/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js index ee44d01df1..85b6784594 100644 --- a/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js +++ b/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js @@ -208,7 +208,7 @@ var pretixstripe = { } ); }, - 'handleCardAction': function (payment_intent_client_secret) { + 'withStripe': function (callback) { $.ajax({ url: 'https://js.stripe.com/v3/', dataType: 'script', @@ -223,13 +223,68 @@ var pretixstripe = { locale: $.trim($("body").attr("data-locale")) }); } - pretixstripe.stripe.handleCardAction( - payment_intent_client_secret - ).then(function (result) { + callback(); + } + }); + }, + 'handleAlipayAction': function (payment_intent_client_secret) { + pretixstripe.withStripe(function () { + pretixstripe.stripe.confirmAlipayPayment( + payment_intent_client_secret, + { + return_url: window.location.href + } + ).then(function (result) { + if (result.error) { + waitingDialog.hide(); + $(".stripe-errors").stop().hide().removeClass("sr-only"); + $(".stripe-errors").html("
Technical error, please contact support: " + result.error.message + "
"); + $(".stripe-errors").slideDown(); + } else { + waitingDialog.show(gettext("Confirming your payment …")); + } + }); + }); + }, + 'handleWechatAction': function (payment_intent_client_secret) { + pretixstripe.withStripe(function () { + pretixstripe.stripe.confirmWechatPayPayment( + payment_intent_client_secret, + { + payment_method_options: { + wechat_pay: { + client: 'web', + }, + }, + } + ).then(function (result) { + if (result.error) { + waitingDialog.hide(); + $(".stripe-errors").stop().hide().removeClass("sr-only"); + $(".stripe-errors").html("
Technical error, please contact support: " + result.error.message + "
"); + $(".stripe-errors").slideDown(); + } else { waitingDialog.show(gettext("Confirming your payment …")); location.reload(); - }); - } + } + }); + }); + }, + 'handleCardAction': function (payment_intent_client_secret) { + pretixstripe.withStripe(function () { + pretixstripe.stripe.handleCardAction( + payment_intent_client_secret + ).then(function (result) { + if (result.error) { + waitingDialog.hide(); + $(".stripe-errors").stop().hide().removeClass("sr-only"); + $(".stripe-errors").html("
Technical error, please contact support: " + result.error.message + "
"); + $(".stripe-errors").slideDown(); + } else { + waitingDialog.show(gettext("Confirming your payment …")); + location.reload(); + } + }); }); }, 'handlePaymentRedirectAction': function (payment_intent_next_action_redirect_url) { @@ -270,6 +325,12 @@ $(function () { } else if ($("#stripe_payment_intent_next_action_redirect_url").length) { let payment_intent_next_action_redirect_url = $.trim($("#stripe_payment_intent_next_action_redirect_url").html()); pretixstripe.handlePaymentRedirectAction(payment_intent_next_action_redirect_url); + } else if ($.trim($("#stripe_payment_intent_action_type").html()) === "wechat_pay_display_qr_code") { + let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html()); + pretixstripe.handleWechatAction(payment_intent_client_secret); + } else if ($.trim($("#stripe_payment_intent_action_type").html()) === "alipay_handle_redirect") { + let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html()); + pretixstripe.handleAlipayAction(payment_intent_client_secret); } else if ($("#stripe_payment_intent_client_secret").length) { let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html()); pretixstripe.handleCardAction(payment_intent_client_secret); diff --git a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/control.html b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/control.html index b6162aded3..5f90f887b9 100644 --- a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/control.html +++ b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/control.html @@ -7,56 +7,65 @@
{% trans "Charge ID" %}
{{ payment_info.id }}
{% endif %} - {% if "source" in payment_info %} - {% if payment_info.source.card %} + {% if details %} + {% if details.card %}
{% trans "Card type" %}
-
{{ payment_info.source.card.brand }}
-
{% trans "Card number" %}
-
**** **** **** {{ payment_info.source.card.last4 }}
- {% if payment_info.source.owner.name %} -
{% trans "Payer name" %}
-
{{ payment_info.source.owner.name }}
- {% endif %} - {% endif %} - {% if payment_info.source.type == "sepa_debit" %} -
{% trans "Bank" %}
-
{{ payment_info.source.sepadirectdebit.bank_name }}
-
{% trans "Payer name" %}
-
{{ payment_info.source.owner.verified_name|default:payment_info.source.owner.name }}
- {% endif %} - {% if payment_info.source.type == "giropay" %} -
{% trans "Bank" %}
-
{{ payment_info.source.giropay.bank_name }} ({{ payment_info.source.giropay.bic }})
-
{% trans "Payer name" %}
-
{{ payment_info.source.owner.verified_name|default:payment_info.source.owner.name }}
- {% endif %} - {% if payment_info.source.type == "bancontact" %} -
{% trans "Bank" %}
-
{{ payment_info.source.bancontact.bank_name }} ({{ payment_info.source.bancontact.bic }})
- {% if owner in payment_info.source %} -
{% trans "Payer name" %}
-
{{ payment_info.source.owner.verified_name|default:payment_info.source.owner.name }}
- {% endif %} - {% endif %} - {% if payment_info.source.type == "ideal" %} -
{% trans "Bank" %}
-
{{ payment_info.source.ideal.bank }} ({{ payment_info.source.ideal.bic }})
-
{% trans "Payer name" %}
-
{{ payment_info.source.owner.verified_name|default:payment_info.source.owner.name }}
- {% endif %} - {% endif %} - {% if payment_info.charges.data.0 %} - {% if payment_info.charges.data.0.payment_method_details.card %} -
{% trans "Card type" %}
-
{{ payment_info.charges.data.0.payment_method_details.card.brand }}
+
{{ details.card.brand }}
{% trans "Card number" %}
- **** **** **** {{ payment_info.charges.data.0.payment_method_details.card.last4 }} - {% if payment_info.charges.data.0.payment_method_details.card.moto %} + **** **** **** {{ details.card.last4 }} + {% if details.card.moto %} {% trans "MOTO" %} {% endif %}
{% endif %} + {% if details.type == "sepa_debit" %} +
{% trans "Bank" %}
+
{{ details.sepadirectdebit.bank_name }}
+ {% if details.sepadirectdebit.verified_name %} +
{% trans "Payer name" %}
+
{{ details.sepadirectdebit.verified_name }}
+ {% endif %} + {% endif %} + {% if details.type == "giropay" %} +
{% trans "Bank" %}
+
{{ details.giropay.bank_name }} ({{ details.giropay.bic }})
+ {% if details.giropay.verified_name %} +
{% trans "Payer name" %}
+
{{ details.giropay.verified_name }}
+ {% endif %} + {% endif %} + {% if details.type == "eps" %} +
{% trans "Bank" %}
+
{{ details.eps.bank }}
+ {% if details.eps.verified_name %} +
{% trans "Payer name" %}
+
{{ details.eps.verified_name }}
+ {% endif %} + {% endif %} + {% if details.type == "bancontact" %} +
{% trans "Bank" %}
+
{{ details.bancontact.bank_name }} ({{ details.bancontact.bic }})
+ {% if details.bancontact.verified_name %} +
{% trans "Payer name" %}
+
{{ details.bancontact.verified_name }}
+ {% endif %} + {% endif %} + {% if details.type == "ideal" %} +
{% trans "Bank" %}
+
{{ details.ideal.bank }} ({{ details.ideal.bic }})
+ {% if details.ideal.verified_name %} +
{% trans "Payer name" %}
+
{{ details.ideal.verified_name }}
+ {% endif %} + {% endif %} + {% endif %} + {% if details.owner.verified_name %} +
{% trans "Payer name" %}
+
{{ details.owner.verified_name }}
+ {% elif details.owner.name %} +
{% trans "Payer name" %}
+
{{ details.owner.name }}
{% endif %} {% if "amount" in payment_info %}
{% trans "Total value" %}
diff --git a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/sca.html b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/sca.html index d8a47d5b41..f18a09c8f7 100644 --- a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/sca.html +++ b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/sca.html @@ -6,9 +6,14 @@ {% block custom_header %} {{ block.super }} {% include "pretixplugins/stripe/presale_head.html" with settings=stripe_settings %} + - - + {% if payment_intent_next_action_redirect_url %} + + {% endif %}} + {% if payment_intent_redirect_action_handling %} + + {% endif %} {% endblock %} {% block content %}
@@ -18,6 +23,9 @@ Confirm payment: {{ code }} {% endblocktrans %} +
+
+
diff --git a/src/pretix/plugins/stripe/views.py b/src/pretix/plugins/stripe/views.py index 3d479cdc0a..9b9fcf1d37 100644 --- a/src/pretix/plugins/stripe/views.py +++ b/src/pretix/plugins/stripe/views.py @@ -273,7 +273,11 @@ def charge_webhook(event, event_json, charge_id, rso): prov._init_api() try: - charge = stripe.Charge.retrieve(charge_id, expand=['dispute'], **prov.api_kwargs) + charge = stripe.Charge.retrieve( + charge_id, + expand=['dispute', 'refunds', 'payment_intent', 'payment_intent.latest_charge'], + **prov.api_kwargs + ) except stripe.error.StripeError: logger.exception('Stripe error on webhook. Event data: %s' % str(event_json)) return HttpResponse('Charge not found', status=500) @@ -321,7 +325,7 @@ def charge_webhook(event, event_json, charge_id, rso): order.log_action('pretix.plugins.stripe.event', data=event_json) - is_refund = charge['refunds']['total_count'] or charge['dispute'] + is_refund = charge['amount_refunded'] or 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')] @@ -354,6 +358,8 @@ def charge_webhook(event, event_json, charge_id, rso): OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED): try: + if getattr(charge, "payment_intent", None): + payment.info = str(charge.payment_intent) payment.confirm() except LockTimeoutException: return HttpResponse("Lock timeout, please try again.", status=503) @@ -439,14 +445,14 @@ def paymentintent_webhook(event, event_json, paymentintent_id, rso): prov._init_api() try: - paymentintent = stripe.PaymentIntent.retrieve(paymentintent_id, **prov.api_kwargs) + paymentintent = stripe.PaymentIntent.retrieve(paymentintent_id, expand=["latest_charge"], **prov.api_kwargs) except stripe.error.StripeError: logger.exception('Stripe error on webhook. Event data: %s' % str(event_json)) return HttpResponse('Charge not found', status=500) - for charge in paymentintent.charges.data: + if paymentintent.latest_charge: ReferencedStripeObject.objects.get_or_create( - reference=charge.id, + reference=paymentintent.latest_charge.id, defaults={'order': rso.payment.order, 'payment': rso.payment} ) @@ -581,6 +587,7 @@ class ScaView(StripeOrderView, View): try: intent = stripe.PaymentIntent.retrieve( payment_info['id'], + expand=["latest_charge"], **prov.api_kwargs ) except stripe.error.InvalidRequestError: @@ -591,12 +598,15 @@ class ScaView(StripeOrderView, View): messages.error(self.request, _('Sorry, there was an error in the payment process.')) return self._redirect_to_order() - if intent.status == 'requires_action' and intent.next_action.type in ['use_stripe_sdk', 'redirect_to_url']: + if intent.status == 'requires_action' and intent.next_action.type in [ + 'use_stripe_sdk', 'redirect_to_url', 'alipay_handle_redirect', 'wechat_pay_display_qr_code' + ]: ctx = { 'order': self.order, 'stripe_settings': StripeSettingsHolder(self.order.event).settings, } - if intent.next_action.type == 'use_stripe_sdk': + ctx['payment_intent_action_type'] = intent.next_action.type + if intent.next_action.type in ('use_stripe_sdk', 'alipay_handle_redirect', 'wechat_pay_display_qr_code'): ctx['payment_intent_client_secret'] = intent.client_secret elif intent.next_action.type == 'redirect_to_url': ctx['payment_intent_next_action_redirect_url'] = intent.next_action.redirect_to_url['url'] diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index e11a6c8705..c209e61e85 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -30,7 +30,8 @@ from django.core import mail as djmail from django.utils.timezone import now from django_countries.fields import Country from django_scopes import scopes_disabled -from stripe.error import APIConnectionError +from stripe import error +from tests.plugins.stripe.test_checkout import apple_domain_create from tests.plugins.stripe.test_provider import MockedCharge from pretix.base.models import InvoiceAddress, Order, OrderPosition @@ -744,13 +745,14 @@ def test_payment_refund_fail(token_client, organizer, event, order, monkeypatch) @pytest.mark.django_db def test_payment_refund_success(token_client, organizer, event, order, monkeypatch): - def charge_retr(*args, **kwargs): - def refund_create(amount): - r = MockedCharge() - r.id = 'foo' - r.status = 'succeeded' - return r + def refund_create(*args, **kwargs): + r = MockedCharge() + r.id = 'foo' + r.status = 'succeeded' + return r + + def charge_retr(*args, **kwargs): c = MockedCharge() c.refunds.create = refund_create return c @@ -765,7 +767,9 @@ def test_payment_refund_success(token_client, organizer, event, order, monkeypat 'id': 'ch_123345345' }) ) + monkeypatch.setattr("stripe.ApplePayDomain.create", apple_domain_create) monkeypatch.setattr("stripe.Charge.retrieve", charge_retr) + monkeypatch.setattr("stripe.Refund.create", refund_create) resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/payments/{}/refund/'.format( organizer.slug, event.slug, order.code, p1.local_id ), format='json', data={ @@ -784,7 +788,7 @@ def test_payment_refund_success(token_client, organizer, event, order, monkeypat def test_payment_refund_unavailable(token_client, organizer, event, order, monkeypatch): def charge_retr(*args, **kwargs): def refund_create(amount): - raise APIConnectionError(message='Foo') + raise error.APIConnectionError(message='Foo') c = MockedCharge() c.refunds.create = refund_create diff --git a/src/tests/control/test_orders.py b/src/tests/control/test_orders.py index 6f27e87c6b..1bae296511 100644 --- a/src/tests/control/test_orders.py +++ b/src/tests/control/test_orders.py @@ -43,6 +43,7 @@ from django.utils.timezone import now from django_countries.fields import Country from django_scopes import scopes_disabled from tests.base import SoupTest +from tests.plugins.stripe.test_checkout import apple_domain_create from tests.plugins.stripe.test_provider import MockedCharge from pretix.base.models import ( @@ -2081,15 +2082,17 @@ def test_refund_paid_order_automatically_failed(client, env, monkeypatch): p.confirm() client.login(email='dummy@dummy.dummy', password='dummy') - def charge_retr(*args, **kwargs): - def refund_create(amount): - raise PaymentException('This failed.') + def refund_create(*args, **kwargs): + raise PaymentException('This failed.') + def charge_retr(*args, **kwargs): c = MockedCharge() c.refunds.create = refund_create return c + monkeypatch.setattr("stripe.ApplePayDomain.create", apple_domain_create) monkeypatch.setattr("stripe.Charge.retrieve", charge_retr) + monkeypatch.setattr("stripe.Refund.create", refund_create) r = client.post('/control/event/dummy/dummy/orders/FOO/refund', { 'start-partial_amount': '7.00', @@ -2123,18 +2126,20 @@ def test_refund_paid_order_automatically(client, env, monkeypatch): p.confirm() client.login(email='dummy@dummy.dummy', password='dummy') - def charge_retr(*args, **kwargs): - def refund_create(amount): - r = MockedCharge() - r.id = 'foo' - r.status = 'succeeded' - return r + def refund_create(*args, **kwargs): + r = MockedCharge() + r.id = 'foo' + r.status = 'succeeded' + return r + def charge_retr(*args, **kwargs): c = MockedCharge() c.refunds.create = refund_create return c + monkeypatch.setattr("stripe.ApplePayDomain.create", apple_domain_create) monkeypatch.setattr("stripe.Charge.retrieve", charge_retr) + monkeypatch.setattr("stripe.Refund.create", refund_create) client.post('/control/event/dummy/dummy/orders/FOO/refund', { 'start-partial_amount': '7.00', diff --git a/src/tests/plugins/stripe/test_checkout.py b/src/tests/plugins/stripe/test_checkout.py index f86c79112e..fbeb0c4cd6 100644 --- a/src/tests/plugins/stripe/test_checkout.py +++ b/src/tests/plugins/stripe/test_checkout.py @@ -43,7 +43,7 @@ from pretix.base.models import ( from pretix.testutils.sessions import add_cart_session, get_cart_session_key -class MockedCharge(): +class MockedCharge: status = '' paid = False id = 'ch_123345345' @@ -52,18 +52,27 @@ class MockedCharge(): pass -class Object(): +class Object: pass -class MockedPaymentintent(): +class MockedPaymentintent: status = '' id = 'pi_1EUon12Tb35ankTnZyvC3SdE' + latest_charge = MockedCharge() charges = Object() - charges.data = [MockedCharge()] + charges.data = [latest_charge] last_payment_error = None +class MockedAppleDomain: + livemode = True + + +def apple_domain_create(**kwargs): + return MockedAppleDomain() + + @pytest.fixture def env(client): orga = Organizer.objects.create(name='CCC', slug='ccc') @@ -80,6 +89,7 @@ def env(client): quota_tickets.items.add(ticket) event.settings.set('attendee_names_asked', False) event.settings.set('payment_stripe__enabled', True) + event.settings.set('payment_stripe_publishable_key', 'nokey') add_cart_session(client, event, {'email': 'admin@localhost'}) return client, ticket @@ -92,10 +102,11 @@ def test_payment(env, monkeypatch): assert kwargs['payment_method'] == 'pm_189fTT2eZvKYlo2CvJKzEzeu' c = MockedPaymentintent() c.status = 'succeeded' - c.charges.data[0].paid = True + c.latest_charge.paid = True setattr(paymentintent_create, 'called', True) return c + monkeypatch.setattr("stripe.ApplePayDomain.create", apple_domain_create) monkeypatch.setattr("stripe.PaymentIntent.create", paymentintent_create) client, ticket = env diff --git a/src/tests/plugins/stripe/test_provider.py b/src/tests/plugins/stripe/test_provider.py index 777a22d97f..5fe0eea5ab 100644 --- a/src/tests/plugins/stripe/test_provider.py +++ b/src/tests/plugins/stripe/test_provider.py @@ -55,6 +55,7 @@ def env(): organizer=o, name='Dummy', slug='dummy', date_from=now(), live=True ) + event.settings.set('payment_stripe_publishable_key', 'nokey') o1 = Order.objects.create( code='FOOBAR', event=event, email='dummy@dummy.test', status=Order.STATUS_PENDING, @@ -75,11 +76,11 @@ def factory(): return RequestFactory() -class MockedRefunds(): +class MockedRefunds: pass -class MockedCharge(): +class MockedCharge: status = '' paid = False id = 'ch_123345345' @@ -96,18 +97,27 @@ class MockedCharge(): pass -class Object(): +class Object: pass -class MockedPaymentintent(): +class MockedPaymentintent: status = '' id = 'pi_1EUon12Tb35ankTnZyvC3SdE' + latest_charge = MockedCharge() charges = Object() - charges.data = [MockedCharge()] + charges.data = [latest_charge] last_payment_error = None +class MockedAppleDomain: + livemode = True + + +def apple_domain_create(**kwargs): + return MockedAppleDomain() + + @pytest.mark.django_db def test_perform_success(env, factory, monkeypatch): event, order = env @@ -118,9 +128,10 @@ def test_perform_success(env, factory, monkeypatch): assert kwargs['payment_method'] == 'pm_189fTT2eZvKYlo2CvJKzEzeu' c = MockedPaymentintent() c.status = 'succeeded' - c.charges.data[0].paid = True + c.latest_charge.paid = True return c + monkeypatch.setattr("stripe.ApplePayDomain.create", apple_domain_create) monkeypatch.setattr("stripe.PaymentIntent.create", paymentintent_create) prov = StripeCC(event) @@ -152,9 +163,10 @@ def test_perform_success_zero_decimal_currency(env, factory, monkeypatch): assert kwargs['payment_method'] == 'pm_189fTT2eZvKYlo2CvJKzEzeu' c = MockedPaymentintent() c.status = 'succeeded' - c.charges.data[0].paid = True + c.latest_charge.paid = True return c + monkeypatch.setattr("stripe.ApplePayDomain.create", apple_domain_create) monkeypatch.setattr("stripe.PaymentIntent.create", paymentintent_create) prov = StripeCC(event) req = factory.post('/', { @@ -180,6 +192,7 @@ def test_perform_card_error(env, factory, monkeypatch): def paymentintent_create(**kwargs): raise error.CardError(message='Foo', param='foo', code=100) + monkeypatch.setattr("stripe.ApplePayDomain.create", apple_domain_create) monkeypatch.setattr("stripe.PaymentIntent.create", paymentintent_create) prov = StripeCC(event) req = factory.post('/', { @@ -206,6 +219,7 @@ def test_perform_stripe_error(env, factory, monkeypatch): def paymentintent_create(**kwargs): raise error.CardError(message='Foo', param='foo', code=100) + monkeypatch.setattr("stripe.ApplePayDomain.create", apple_domain_create) monkeypatch.setattr("stripe.PaymentIntent.create", paymentintent_create) prov = StripeCC(event) req = factory.post('/', { @@ -236,11 +250,12 @@ def test_perform_failed(env, factory, monkeypatch): c = MockedPaymentintent() c.status = 'failed' c.failure_message = 'Foo' - c.charges.data[0].paid = True + c.latest_charge.paid = True c.last_payment_error = Object() c.last_payment_error.message = "Foo" return c + monkeypatch.setattr("stripe.ApplePayDomain.create", apple_domain_create) monkeypatch.setattr("stripe.PaymentIntent.create", paymentintent_create) prov = StripeCC(event) req = factory.post('/', { @@ -264,18 +279,20 @@ def test_perform_failed(env, factory, monkeypatch): def test_refund_success(env, factory, monkeypatch): event, order = env - def charge_retr(*args, **kwargs): - def refund_create(amount): - r = MockedCharge() - r.id = 'foo' - r.status = 'succeeded' - return r + def refund_create(*args, **kwargs): + r = MockedCharge() + r.id = 'foo' + r.status = 'succeeded' + return r + def charge_retr(*args, **kwargs): c = MockedCharge() c.refunds.create = refund_create return c + monkeypatch.setattr("stripe.ApplePayDomain.create", apple_domain_create) monkeypatch.setattr("stripe.Charge.retrieve", charge_retr) + monkeypatch.setattr("stripe.Refund.create", refund_create) order.status = Order.STATUS_PAID p = order.payments.create(provider='stripe_cc', amount=order.total, info=json.dumps({ 'id': 'ch_123345345' @@ -294,15 +311,17 @@ def test_refund_success(env, factory, monkeypatch): def test_refund_unavailable(env, factory, monkeypatch): event, order = env - def charge_retr(*args, **kwargs): - def refund_create(amount): - raise error.APIConnectionError(message='Foo') + def refund_create(*args, **kwargs): + raise error.APIConnectionError(message='Foo') + def charge_retr(*args, **kwargs): c = MockedCharge() c.refunds.create = refund_create return c + monkeypatch.setattr("stripe.ApplePayDomain.create", apple_domain_create) monkeypatch.setattr("stripe.Charge.retrieve", charge_retr) + monkeypatch.setattr("stripe.Refund.create", refund_create) order.status = Order.STATUS_PAID p = order.payments.create(provider='stripe_cc', amount=order.total, info=json.dumps({ 'id': 'ch_123345345' diff --git a/src/tests/plugins/stripe/test_webhook.py b/src/tests/plugins/stripe/test_webhook.py index 06f325af9b..3721d0fd8b 100644 --- a/src/tests/plugins/stripe/test_webhook.py +++ b/src/tests/plugins/stripe/test_webhook.py @@ -151,12 +151,22 @@ def test_webhook_all_good(env, client, monkeypatch): @pytest.mark.django_db -def test_webhook_mark_paid_without_reference_and_payment(env, client, monkeypatch): +def test_webhook_mark_paid(env, client, monkeypatch): order = env[1] order.status = Order.STATUS_PENDING order.save() - charge = get_test_charge(env[1]) + charge["amount_refunded"] = 0 + with scopes_disabled(): + payment = env[1].payments.create( + provider='stripe', amount=env[1].total, info='{}', state=OrderPayment.PAYMENT_STATE_CREATED, + ) + ReferencedStripeObject.objects.create( + order=order, + payment=payment, + reference="pi_123456", + ) + monkeypatch.setattr("stripe.Charge.retrieve", lambda *args, **kwargs: charge) client.post('/dummy/dummy/stripe/webhook/', json.dumps( @@ -169,6 +179,10 @@ def test_webhook_mark_paid_without_reference_and_payment(env, client, monkeypatc "object": { "id": "ch_18TY6GGGWE2Ias8TZHanef25", "object": "charge", + "payment_intent": "pi_123456", + "metadata": { + "event": order.event_id, + } # Rest of object is ignored anway } }, @@ -253,6 +267,7 @@ def test_webhook_global(env, client, monkeypatch): order.save() charge = get_test_charge(env[1]) + charge["amount_refunded"] = 0 monkeypatch.setattr("stripe.Charge.retrieve", lambda *args, **kwargs: charge) with scopes_disabled(): @@ -261,6 +276,8 @@ def test_webhook_global(env, client, monkeypatch): ) ReferencedStripeObject.objects.create(order=order, reference="ch_18TY6GGGWE2Ias8TZHanef25", payment=payment) + ReferencedStripeObject.objects.create(order=order, reference="pi_123456", + payment=payment) client.post('/_stripe/webhook/', json.dumps( { @@ -272,6 +289,10 @@ def test_webhook_global(env, client, monkeypatch): "object": { "id": "ch_18TY6GGGWE2Ias8TZHanef25", "object": "charge", + "payment_intent": "pi_123456", + "metadata": { + "event": order.event_id, + } # Rest of object is ignored anway } }, @@ -293,6 +314,7 @@ def test_webhook_global_legacy_reference(env, client, monkeypatch): order.save() charge = get_test_charge(env[1]) + charge["amount_refunded"] = 0 monkeypatch.setattr("stripe.Charge.retrieve", lambda *args, **kwargs: charge) with scopes_disabled(): @@ -300,6 +322,7 @@ def test_webhook_global_legacy_reference(env, client, monkeypatch): provider='stripe', amount=order.total, info=json.dumps(charge), state=OrderPayment.PAYMENT_STATE_CREATED ) ReferencedStripeObject.objects.create(order=order, reference="ch_18TY6GGGWE2Ias8TZHanef25") + ReferencedStripeObject.objects.create(order=order, reference="pi_123456") client.post('/_stripe/webhook/', json.dumps( { @@ -311,6 +334,10 @@ def test_webhook_global_legacy_reference(env, client, monkeypatch): "object": { "id": "ch_18TY6GGGWE2Ias8TZHanef25", "object": "charge", + "payment_intent": "pi_123456", + "metadata": { + "event": order.event_id, + } # Rest of object is ignored anway } },