From a6b8cd8a540a0df933583b72a8bd61a670432402 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 19 Jun 2024 18:00:23 +0200 Subject: [PATCH] Stripe: Move Multibanco to payment intents (#4243) --- src/pretix/plugins/stripe/payment.py | 193 +++--------------- .../pretixplugins/stripe/pending.html | 41 +++- src/pretix/plugins/stripe/views.py | 5 +- 3 files changed, 65 insertions(+), 174 deletions(-) diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py index cc20efe5aa..0db7b68ea4 100644 --- a/src/pretix/plugins/stripe/payment.py +++ b/src/pretix/plugins/stripe/payment.py @@ -944,6 +944,17 @@ class StripeMethod(BasePaymentProvider): reference=intent.id, defaults={'order': payment.order, 'payment': payment} ) + if intent.status == 'requires_action': + payment.info = str(intent) + if intent.next_action.type == 'multibanco_display_details': + payment.state = OrderPayment.PAYMENT_STATE_PENDING + payment.save() + return + + payment.state = OrderPayment.PAYMENT_STATE_CREATED + payment.save() + return self._redirect_to_sca(request, payment) + if intent.status == 'requires_action': payment.info = str(intent) payment.state = OrderPayment.PAYMENT_STATE_CREATED @@ -1046,135 +1057,6 @@ class StripeMethod(BasePaymentProvider): '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 = {} - if not source.startswith('src_'): - params['statement_descriptor'] = self.statement_descriptor(payment) - params.update(self.api_kwargs) - params.update(self._connect_kwargs(payment)) - charge = stripe.Charge.create( - amount=self._get_amount(payment), - currency=self.event.currency.lower(), - source=source, - description='{event}-{code}'.format( - event=self.event.slug.upper(), - code=payment.order.code - ), - 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 + source, - **params - ) - 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': - # This is not an error we normally expect, however some payment methods like iDEAL will redirect - # the user back to our confirmation page at the same time from two devices: the web browser the - # purchase is executed from and the online banking app the payment is authorized from. - # In this case we will just log the idempotency error but not expose it to the user and just - # forward them back to their order page. There is a good chance that by the time the user hits - # the order page, the other request has gone through and the payment is confirmed. - # Usually however this should be prevented by SELECT FOR UPDATE calls! - 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=charge.id, - defaults={'order': payment.order, 'payment': payment} - ) - if charge.status == 'succeeded' and charge.paid: - try: - payment.info = str(charge) - payment.confirm() - except Quota.QuotaExceededException as e: - raise PaymentException(str(e)) - - except SendMailException: - raise PaymentException(_('There was an error sending the confirmation mail.')) - elif charge.status == 'pending': - if request: - messages.warning(request, _('Your payment is pending completion. We will inform you as soon as the ' - 'payment completed.')) - payment.info = str(charge) - payment.state = OrderPayment.PAYMENT_STATE_PENDING - payment.save() - return - else: - logger.info('Charge failed: %s' % str(charge)) - payment.fail(info=str(charge)) - raise PaymentException(_('Stripe reported an error: %s') % charge.failure_message) - - 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.state = OrderPayment.PAYMENT_STATE_PENDING - payment.save() - request.session['payment_stripe_order_secret'] = payment.order.secret - return self.redirect(request, source.redirect.url) - - class StripeRedirectMethod(StripeMethod): redirect_action_handling = "redirect" @@ -1793,53 +1675,26 @@ class StripeEPS(StripeRedirectWithAccountNamePaymentIntentMethod): return super().payment_presale_render(payment) -class StripeMultibanco(StripeSourceMethod): +class StripeMultibanco(StripeRedirectMethod): identifier = 'stripe_multibanco' verbose_name = _('Multibanco via Stripe') public_name = _('Multibanco') method = 'multibanco' + explanation = _( + 'Multibanco is a payment method available to Portuguese bank account holders.' + ) redirect_in_widget_allowed = False + abort_pending_allowed = True - 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, - 'explanation': self.explanation, - 'form': self.payment_form(request) + def _payment_intent_kwargs(self, request, payment): + return { + "payment_method_data": { + "type": "multibanco", + "billing_details": { + "email": payment.order.email, + } + } } - return template.render(ctx) - - def _create_source(self, request, payment): - source = stripe.Source.create( - type='multibanco', - 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 - }, - redirect={ - 'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={ - 'order': payment.order.code, - 'payment': payment.pk, - 'hash': payment.order.tagged_secret('plugins:stripe'), - }) - }, - **self.api_kwargs - ) - return source - - def payment_is_valid_session(self, request): - return True - - def checkout_prepare(self, request, cart): - return True class StripePrzelewy24(StripeRedirectMethod): diff --git a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html index 67d7a941e7..1628170405 100644 --- a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html +++ b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html @@ -1,10 +1,43 @@ {% load i18n %} {% load eventurl %} +{% load money %} {% if payment.state == "pending" %} -

{% blocktrans trimmed %} - We're waiting for an answer from the payment provider regarding your payment. Please contact us if this - takes more than a few days. - {% endblocktrans %}

+ {% if payment_info.next_action.type == "multibanco_display_details" %} +

{% trans "Payment instructions" %}:

+
    +
  1. + {% blocktrans trimmed %} + In your online bank account or from an ATM, choose "Payment and other services". + {% endblocktrans %} +
  2. +
  3. + {% blocktrans trimmed %} + Click "Payments of services/shopping". + {% endblocktrans %} +
  4. +
  5. + {% blocktrans trimmed %} + Enter the entity number, reference number, and amount. + {% endblocktrans %} +
  6. +
+
+
{% trans "Entity number:" %}
{{ payment_info.next_action.multibanco_display_details.entity }}

+
{% trans "Reference number:" %}
{{ payment_info.next_action.multibanco_display_details.reference }}

+
{% trans "Amount:" %}
{{ payment.amount|money:event.currency }}
+
+

+ {% trans "There is no further action required on this website." %} + {% trans "We will send you an email as soon as we received your payment." %} +

+ {% else %} +

+ {% blocktrans trimmed %} + We're waiting for an answer from the payment provider regarding your payment. Please contact us if this + takes more than a few days. + {% endblocktrans %} +

+ {% endif %} {% elif payment.state == "created" and payment_info.status == "requires_action" %}

{% blocktrans trimmed %} You need to confirm your payment. Please click the link below to do so or start a new payment. diff --git a/src/pretix/plugins/stripe/views.py b/src/pretix/plugins/stripe/views.py index fbd3656d42..628323dac1 100644 --- a/src/pretix/plugins/stripe/views.py +++ b/src/pretix/plugins/stripe/views.py @@ -595,7 +595,7 @@ class ScaView(StripeOrderView, View): 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', - 'swish_handle_redirect_or_display_qr_code', + 'swish_handle_redirect_or_display_qr_code', 'multibanco_display_details', ]: ctx = { 'order': self.order, @@ -610,6 +610,9 @@ class ScaView(StripeOrderView, View): elif intent.next_action.type == 'swish_handle_redirect_or_display_qr_code': ctx['payment_intent_next_action_redirect_url'] = intent.next_action.swish_handle_redirect_or_display_qr_code['hosted_instructions_url'] ctx['payment_intent_redirect_action_handling'] = 'iframe' + elif intent.next_action.type == 'multibanco_display_details': + ctx['payment_intent_next_action_redirect_url'] = intent.next_action.multibanco_display_details['hosted_voucher_url'] + ctx['payment_intent_redirect_action_handling'] = 'iframe' r = render(request, 'pretixplugins/stripe/sca.html', ctx) r._csp_ignore = True