diff --git a/pyproject.toml b/pyproject.toml index c2e185d71..689e5adca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,7 +93,7 @@ dependencies = [ "requests==2.31.*", "sentry-sdk==2.27.*", "sepaxml==2.6.*", - "stripe==7.9.*", + "stripe==12.1.*", "text-unidecode==1.*", "tlds>=2020041600", "tqdm==4.*", diff --git a/src/pretix/plugins/stripe/management/commands/stripe_connect_fill_countries.py b/src/pretix/plugins/stripe/management/commands/stripe_connect_fill_countries.py index 7062e9b1f..a7099ef98 100644 --- a/src/pretix/plugins/stripe/management/commands/stripe_connect_fill_countries.py +++ b/src/pretix/plugins/stripe/management/commands/stripe_connect_fill_countries.py @@ -19,12 +19,12 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # -import stripe from django.core.management.base import BaseCommand from django_scopes import scopes_disabled from pretix.base.models import Event from pretix.base.settings import GlobalSettingsObject +from pretix.plugins.stripe.utils import get_stripe_client class Command(BaseCommand): @@ -34,7 +34,10 @@ class Command(BaseCommand): def handle(self, *args, **options): cache = {} gs = GlobalSettingsObject() - api_key = gs.settings.payment_stripe_connect_secret_key or gs.settings.payment_stripe_connect_test_secret_key + api_key = ( + gs.settings.payment_stripe_connect_secret_key + or gs.settings.payment_stripe_connect_test_secret_key + ) if not api_key: self.stderr.write(self.style.ERROR("Stripe Connect is not set up!")) return @@ -46,11 +49,13 @@ class Command(BaseCommand): e.settings.payment_stripe_merchant_country = cache[uid] else: try: - account = stripe.Account.retrieve( + stripe_client = get_stripe_client(api_key) + account = stripe_client.accounts.retrieve( uid, - api_key=api_key ) except Exception as e: print(e) else: - e.settings.payment_stripe_merchant_country = cache[uid] = account.get('country') + e.settings.payment_stripe_merchant_country = cache[uid] = ( + account.get("country") + ) diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py index 2bf8ad4a2..979cac189 100644 --- a/src/pretix/plugins/stripe/payment.py +++ b/src/pretix/plugins/stripe/payment.py @@ -58,17 +58,24 @@ from django.utils.translation import gettext, gettext_lazy as _, pgettext from django_countries import countries from text_unidecode import unidecode -from pretix import __version__ from pretix.base.decimal import round_decimal from pretix.base.forms import SecretKeySettingsField from pretix.base.forms.questions import ( - guess_country, guess_country_from_request, + guess_country, + guess_country_from_request, ) from pretix.base.models import ( - Event, InvoiceAddress, Order, OrderPayment, OrderRefund, Quota, + Event, + InvoiceAddress, + Order, + OrderPayment, + OrderRefund, + Quota, ) from pretix.base.payment import ( - BasePaymentProvider, PaymentException, WalletQueries, + BasePaymentProvider, + PaymentException, + WalletQueries, ) from pretix.base.plugins import get_all_plugins from pretix.base.services.mail import SendMailException @@ -80,14 +87,17 @@ from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.multidomain.urlreverse import build_absolute_uri from pretix.plugins.stripe.forms import StripeKeyValidator from pretix.plugins.stripe.models import ( - ReferencedStripeObject, RegisteredApplePayDomain, + ReferencedStripeObject, + RegisteredApplePayDomain, ) from pretix.plugins.stripe.tasks import ( - get_stripe_account_key, stripe_verify_domain, + get_stripe_account_key, + stripe_verify_domain, ) +from pretix.plugins.stripe.utils import get_stripe_client from pretix.presale.views.cart import cart_session -logger = logging.getLogger('pretix.plugins.stripe') +logger = logging.getLogger("pretix.plugins.stripe") # State of the payment methods @@ -159,26 +169,26 @@ logger = logging.getLogger('pretix.plugins.stripe') class StripeSettingsHolder(BasePaymentProvider): - identifier = 'stripe_settings' - verbose_name = _('Stripe') + identifier = "stripe_settings" + verbose_name = _("Stripe") is_enabled = False is_meta = True def __init__(self, event: Event): super().__init__(event) - self.settings = SettingsSandbox('payment', 'stripe', event) + self.settings = SettingsSandbox("payment", "stripe", event) def get_connect_url(self, request): - request.session['payment_stripe_oauth_event'] = request.event.pk - if 'payment_stripe_oauth_token' not in request.session: - request.session['payment_stripe_oauth_token'] = get_random_string(32) + request.session["payment_stripe_oauth_event"] = request.event.pk + if "payment_stripe_oauth_token" not in request.session: + request.session["payment_stripe_oauth_token"] = get_random_string(32) return ( "https://connect.stripe.com/oauth/authorize?response_type=code&client_id={}&state={}" "&scope=read_write&redirect_uri={}" ).format( self.settings.connect_client_id, - request.session['payment_stripe_oauth_token'], - urllib.parse.quote(build_global_uri('plugins:stripe:oauth.return')), + request.session["payment_stripe_oauth_token"], + urllib.parse.quote(build_global_uri("plugins:stripe:oauth.return")), ) def settings_content_render(self, request): @@ -186,55 +196,64 @@ class StripeSettingsHolder(BasePaymentProvider): # Use Stripe connect if not self.settings.connect_user_id: return ( - "

{}

" - "{}" + "

{}

" "{}" ).format( - _('To accept payments via Stripe, you will need an account at Stripe. By clicking on the ' - 'following button, you can either create a new Stripe account connect pretix to an existing ' - 'one.'), + _( + "To accept payments via Stripe, you will need an account at Stripe. By clicking on the " + "following button, you can either create a new Stripe account connect pretix to an existing " + "one." + ), self.get_connect_url(request), - _('Connect with Stripe') + _("Connect with Stripe"), ) else: - return ( - "{}" - ).format( - reverse('plugins:stripe:oauth.disconnect', kwargs={ - 'organizer': self.event.organizer.slug, - 'event': self.event.slug, - }), - _('Disconnect from Stripe') + return ("{}").format( + reverse( + "plugins:stripe:oauth.disconnect", + kwargs={ + "organizer": self.event.organizer.slug, + "event": self.event.slug, + }, + ), + _("Disconnect from Stripe"), ) else: return "
%s
%s
" % ( - _('Please configure a Stripe Webhook to ' - 'the following endpoint in order to automatically cancel orders when charges are refunded externally ' - 'and to process asynchronous payment methods like SOFORT.'), - build_global_uri('plugins:stripe:webhook') + _( + 'Please configure a Stripe Webhook to ' + "the following endpoint in order to automatically cancel orders when charges are refunded externally " + "and to process asynchronous payment methods like SOFORT." + ), + build_global_uri("plugins:stripe:webhook"), ) @property def settings_form_fields(self): - if 'pretix_resellers' in [p.module for p in get_all_plugins()]: + if "pretix_resellers" in [p.module for p in get_all_plugins()]: moto_settings = [ - ('reseller_moto', - forms.BooleanField( - label=_('Enable MOTO payments for resellers'), - help_text=( - _('Gated feature (needs to be enabled for your account by Stripe support first)') + - '
%s
' % _( - 'We can flag the credit card transaction you make through the reseller interface as MOTO ' - '(Mail Order / Telephone Order), which will exempt them from Strong Customer ' - 'Authentication (SCA) requirements. However: By enabling this feature, you will need to ' - 'fill out yearly PCI-DSS self-assessment forms like the 40 page SAQ D. Please consult the ' - '%s for further information on this subject.' % - '{}'.format( - _('Stripe Integration security guide') - ) - ) - ), - required=False, - )) + ( + "reseller_moto", + forms.BooleanField( + label=_("Enable MOTO payments for resellers"), + help_text=( + _( + "Gated feature (needs to be enabled for your account by Stripe support first)" + ) + + '
%s
' + % _( + "We can flag the credit card transaction you make through the reseller interface as MOTO " + "(Mail Order / Telephone Order), which will exempt them from Strong Customer " + "Authentication (SCA) requirements. However: By enabling this feature, you will need to " + "fill out yearly PCI-DSS self-assessment forms like the 40 page SAQ D. Please consult the " + "%s for further information on this subject." + % '{}'.format( + _("Stripe Integration security guide") + ) + ) + ), + required=False, + ), + ) ] else: moto_settings = [] @@ -243,234 +262,355 @@ class StripeSettingsHolder(BasePaymentProvider): # Stripe connect if self.settings.connect_user_id: fields = [ - ('connect_user_name', - forms.CharField( - label=_('Stripe account'), - disabled=True - )), - ('connect_user_id', - forms.CharField( - label=_('Stripe account'), - disabled=True - )), - ('endpoint', - forms.ChoiceField( - label=_('Endpoint'), - initial='live', - choices=( - ('live', pgettext('stripe', 'Live')), - ('test', pgettext('stripe', 'Testing')), - ), - help_text=_('If your event is in test mode, we will always use Stripe\'s test API, ' - 'regardless of this setting.') - )), + ( + "connect_user_name", + forms.CharField(label=_("Stripe account"), disabled=True), + ), + ( + "connect_user_id", + forms.CharField(label=_("Stripe account"), disabled=True), + ), + ( + "endpoint", + forms.ChoiceField( + label=_("Endpoint"), + initial="live", + choices=( + ("live", pgettext("stripe", "Live")), + ("test", pgettext("stripe", "Testing")), + ), + help_text=_( + "If your event is in test mode, we will always use Stripe's test API, " + "regardless of this setting." + ), + ), + ), ] else: return {} else: allcountries = list(countries) - allcountries.insert(0, ('', _('Select country'))) + allcountries.insert(0, ("", _("Select country"))) fields = [ - ('publishable_key', - forms.CharField( - label=_('Publishable key'), - help_text='{text}
' - '

{help}

'.format( - text=_('Generate API keys'), - docs_url='https://marketplace.stripe.com/apps/install/link/eu.pretix.plugins.stripe.rak', - help=_('The button above will install our Stripe app to your account and will generate you ' - 'API keys with the recommended permission level for optimal usage with pretix.') - ), - validators=( - StripeKeyValidator('pk_'), - ), - )), - ('secret_key', - SecretKeySettingsField( - label=_('Secret key'), - validators=( - StripeKeyValidator(['sk_', 'rk_']), - ), - )), - ('merchant_country', - forms.ChoiceField( - choices=allcountries, - label=_('Merchant country'), - help_text=_('The country in which your Stripe-account is registered in. Usually, this is your ' - 'country of residence.'), - )), + ( + "publishable_key", + forms.CharField( + label=_("Publishable key"), + help_text='{text}
' + '

{help}

'.format( + text=_("Generate API keys"), + docs_url="https://marketplace.stripe.com/apps/install/link/eu.pretix.plugins.stripe.rak", + help=_( + "The button above will install our Stripe app to your account and will generate you " + "API keys with the recommended permission level for optimal usage with pretix." + ), + ), + validators=(StripeKeyValidator("pk_"),), + ), + ), + ( + "secret_key", + SecretKeySettingsField( + label=_("Secret key"), + validators=(StripeKeyValidator(["sk_", "rk_"]),), + ), + ), + ( + "merchant_country", + forms.ChoiceField( + choices=allcountries, + label=_("Merchant country"), + help_text=_( + "The country in which your Stripe-account is registered in. Usually, this is your " + "country of residence." + ), + ), + ), ] extra_fields = [ - ('walletdetection', - forms.BooleanField( - label=mark_safe( - _('Check for Apple Pay/Google Pay') + - ' ' + - '{}'.format(_('experimental')) - ), - help_text=_("pretix will attempt to check if the customer's web browser supports wallet-based payment " - "methods like Apple Pay or Google Pay and display them prominently with the credit card " - "payment method. This detection does not take into consideration if Google Pay/Apple Pay " - "has been disabled in the Stripe Dashboard."), - initial=True, - required=False, - )), - ('postfix', - forms.CharField( - label=_('Statement descriptor postfix'), - help_text=_('Any value entered here will be shown on the customer\'s credit card bill or bank account ' - 'transaction. We will automatically add the order code in front of it. Note that depending ' - 'on the payment method, only a very limited number of characters is allowed. We do not ' - 'recommend entering more than {cnt} characters into this field.').format( - cnt=22 - 1 - settings.ENTROPY['order_code'] - ), - required=False, - )), + ( + "walletdetection", + forms.BooleanField( + label=mark_safe( + _("Check for Apple Pay/Google Pay") + + " " + + '{}'.format( + _("experimental") + ) + ), + help_text=_( + "pretix will attempt to check if the customer's web browser supports wallet-based payment " + "methods like Apple Pay or Google Pay and display them prominently with the credit card " + "payment method. This detection does not take into consideration if Google Pay/Apple Pay " + "has been disabled in the Stripe Dashboard." + ), + initial=True, + required=False, + ), + ), + ( + "postfix", + forms.CharField( + label=_("Statement descriptor postfix"), + help_text=_( + "Any value entered here will be shown on the customer's credit card bill or bank account " + "transaction. We will automatically add the order code in front of it. Note that depending " + "on the payment method, only a very limited number of characters is allowed. We do not " + "recommend entering more than {cnt} characters into this field." + ).format(cnt=22 - 1 - settings.ENTROPY["order_code"]), + required=False, + ), + ), ] d = OrderedDict( - fields + [ - ('method_card', - forms.BooleanField( - label=_('Credit card payments'), - required=False, - )), - ('method_ideal', - forms.BooleanField( - label=_('iDEAL'), - disabled=self.event.currency != 'EUR', - help_text=_('Some payment methods might need to be enabled in the settings of your Stripe account ' - 'before they work properly.'), - required=False, - )), - ('method_alipay', - forms.BooleanField( - label=_('Alipay'), - disabled=self.event.currency not in ('EUR', 'AUD', 'CAD', 'GBP', 'HKD', 'JPY', 'NZD', 'SGD', 'USD'), - help_text=_('Some payment methods might need to be enabled in the settings of your Stripe account ' - 'before they work properly.'), - required=False, - )), - ('method_bancontact', - forms.BooleanField( - label=_('Bancontact'), - disabled=self.event.currency != 'EUR', - help_text=_('Some payment methods might need to be enabled in the settings of your Stripe account ' - 'before they work properly.'), - required=False, - )), - ('method_sepa_debit', - forms.BooleanField( - label=_('SEPA Direct Debit'), - disabled=self.event.currency != 'EUR', - help_text=( - _('Some payment methods might need to be enabled in the settings of your Stripe account ' - 'before work properly.') + - '
%s
' % _( - 'SEPA Direct Debit payments via Stripe are not processed ' - 'instantly but might take up to 14 days to be confirmed in some cases. ' - 'Please only activate this payment method if your payment term allows for this lag.' - )), - required=False, - )), - ('sepa_creditor_name', - forms.CharField( - label=_('SEPA Creditor Mandate Name'), - disabled=self.event.currency != 'EUR', - help_text=_('Please provide your SEPA Creditor Mandate Name, that will be displayed to the user.'), - required=False, - widget=forms.TextInput( - attrs={ - 'data-display-dependency': '#id_payment_stripe_method_sepa_debit', - 'data-required-if': '#id_payment_stripe_method_sepa_debit' - } - ), - )), - ('method_eps', - forms.BooleanField( - label=_('EPS'), - disabled=self.event.currency != 'EUR', - help_text=_('Some payment methods might need to be enabled in the settings of your Stripe account ' - 'before they work properly.'), - required=False, - )), - ('method_multibanco', - forms.BooleanField( - label=_('Multibanco'), - disabled=self.event.currency != 'EUR', - help_text=_('Some payment methods might need to be enabled in the settings of your Stripe account ' - 'before they work properly.'), - required=False, - )), - ('method_przelewy24', - forms.BooleanField( - label=_('Przelewy24'), - disabled=self.event.currency not in ['EUR', 'PLN'], - help_text=_('Some payment methods might need to be enabled in the settings of your Stripe account ' - 'before they work properly.'), - required=False, - )), - ('method_wechatpay', - forms.BooleanField( - label=_('WeChat Pay'), - disabled=self.event.currency not in ['AUD', 'CAD', 'EUR', 'GBP', 'HKD', 'JPY', 'SGD', 'USD'], - help_text=_('Some payment methods might need to be enabled in the settings of your Stripe account ' - 'before they work properly.'), - required=False, - )), - ('method_revolut_pay', - forms.BooleanField( - label='Revolut Pay', - disabled=self.event.currency not in ['EUR', 'GBP'], - help_text=_('Some payment methods might need to be enabled in the settings of your Stripe account ' - 'before they work properly.'), - required=False, - )), - ('method_swish', - forms.BooleanField( - label=_('Swish'), - disabled=self.event.currency != 'SEK', - help_text=_('Some payment methods might need to be enabled in the settings of your Stripe account ' - 'before they work properly.'), - required=False, - )), - ('method_twint', - forms.BooleanField( - label='TWINT', - disabled=self.event.currency != 'CHF', - help_text=_('Some payment methods might need to be enabled in the settings of your Stripe account ' - 'before they work properly.'), - required=False, - )), - ('method_affirm', - forms.BooleanField( - label=_('Affirm'), - disabled=self.event.currency not in ['USD', 'CAD'], - help_text=' '.join([ - str(_('Some payment methods might need to be enabled in the settings of your Stripe account ' - 'before they work properly.')), - str(_('Only available for payments between $50 and $30,000.')) - ]), - required=False, - )), - ('method_klarna', - forms.BooleanField( - label=_('Klarna'), - disabled=self.event.currency not in [ - 'AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'NOK', 'NZD', 'PLN', 'SEK', 'USD' - ], - help_text=' '.join([ - str(_('Some payment methods might need to be enabled in the settings of your Stripe account ' - 'before they work properly.')), - str(_('Klarna and Stripe will decide which of the payment methods offered by Klarna are ' - 'available to the user.')), - str(_('Klarna\'s terms of services do not allow it to be used by charities or political ' - 'organizations.')), - ]), - required=False, - )), + fields + + [ + ( + "method_card", + forms.BooleanField( + label=_("Credit card payments"), + required=False, + ), + ), + ( + "method_ideal", + forms.BooleanField( + label=_("iDEAL"), + disabled=self.event.currency != "EUR", + help_text=_( + "Some payment methods might need to be enabled in the settings of your Stripe account " + "before they work properly." + ), + required=False, + ), + ), + ( + "method_alipay", + forms.BooleanField( + label=_("Alipay"), + disabled=self.event.currency + not in ( + "EUR", + "AUD", + "CAD", + "GBP", + "HKD", + "JPY", + "NZD", + "SGD", + "USD", + ), + help_text=_( + "Some payment methods might need to be enabled in the settings of your Stripe account " + "before they work properly." + ), + required=False, + ), + ), + ( + "method_bancontact", + forms.BooleanField( + label=_("Bancontact"), + disabled=self.event.currency != "EUR", + help_text=_( + "Some payment methods might need to be enabled in the settings of your Stripe account " + "before they work properly." + ), + required=False, + ), + ), + ( + "method_sepa_debit", + forms.BooleanField( + label=_("SEPA Direct Debit"), + disabled=self.event.currency != "EUR", + help_text=( + _( + "Some payment methods might need to be enabled in the settings of your Stripe account " + "before work properly." + ) + + '
%s
' + % _( + "SEPA Direct Debit payments via Stripe are not processed " + "instantly but might take up to 14 days to be confirmed in some cases. " + "Please only activate this payment method if your payment term allows for this lag." + ) + ), + required=False, + ), + ), + ( + "sepa_creditor_name", + forms.CharField( + label=_("SEPA Creditor Mandate Name"), + disabled=self.event.currency != "EUR", + help_text=_( + "Please provide your SEPA Creditor Mandate Name, that will be displayed to the user." + ), + required=False, + widget=forms.TextInput( + attrs={ + "data-display-dependency": "#id_payment_stripe_method_sepa_debit", + "data-required-if": "#id_payment_stripe_method_sepa_debit", + } + ), + ), + ), + ( + "method_eps", + forms.BooleanField( + label=_("EPS"), + disabled=self.event.currency != "EUR", + help_text=_( + "Some payment methods might need to be enabled in the settings of your Stripe account " + "before they work properly." + ), + required=False, + ), + ), + ( + "method_multibanco", + forms.BooleanField( + label=_("Multibanco"), + disabled=self.event.currency != "EUR", + help_text=_( + "Some payment methods might need to be enabled in the settings of your Stripe account " + "before they work properly." + ), + required=False, + ), + ), + ( + "method_przelewy24", + forms.BooleanField( + label=_("Przelewy24"), + disabled=self.event.currency not in ["EUR", "PLN"], + help_text=_( + "Some payment methods might need to be enabled in the settings of your Stripe account " + "before they work properly." + ), + required=False, + ), + ), + ( + "method_wechatpay", + forms.BooleanField( + label=_("WeChat Pay"), + disabled=self.event.currency + not in ["AUD", "CAD", "EUR", "GBP", "HKD", "JPY", "SGD", "USD"], + help_text=_( + "Some payment methods might need to be enabled in the settings of your Stripe account " + "before they work properly." + ), + required=False, + ), + ), + ( + "method_revolut_pay", + forms.BooleanField( + label="Revolut Pay", + disabled=self.event.currency not in ["EUR", "GBP"], + help_text=_( + "Some payment methods might need to be enabled in the settings of your Stripe account " + "before they work properly." + ), + required=False, + ), + ), + ( + "method_swish", + forms.BooleanField( + label=_("Swish"), + disabled=self.event.currency != "SEK", + help_text=_( + "Some payment methods might need to be enabled in the settings of your Stripe account " + "before they work properly." + ), + required=False, + ), + ), + ( + "method_twint", + forms.BooleanField( + label="TWINT", + disabled=self.event.currency != "CHF", + help_text=_( + "Some payment methods might need to be enabled in the settings of your Stripe account " + "before they work properly." + ), + required=False, + ), + ), + ( + "method_affirm", + forms.BooleanField( + label=_("Affirm"), + disabled=self.event.currency not in ["USD", "CAD"], + help_text=" ".join( + [ + str( + _( + "Some payment methods might need to be enabled in the settings of your Stripe account " + "before they work properly." + ) + ), + str( + _( + "Only available for payments between $50 and $30,000." + ) + ), + ] + ), + required=False, + ), + ), + ( + "method_klarna", + forms.BooleanField( + label=_("Klarna"), + disabled=self.event.currency + not in [ + "AUD", + "CAD", + "CHF", + "CZK", + "DKK", + "EUR", + "GBP", + "NOK", + "NZD", + "PLN", + "SEK", + "USD", + ], + help_text=" ".join( + [ + str( + _( + "Some payment methods might need to be enabled in the settings of your Stripe account " + "before they work properly." + ) + ), + str( + _( + "Klarna and Stripe will decide which of the payment methods offered by Klarna are " + "available to the user." + ) + ), + str( + _( + "Klarna's terms of services do not allow it to be used by charities or political " + "organizations." + ) + ), + ] + ), + required=False, + ), + ), # Disabled for now, since we still need to figure out how to make this work on our connect platform # ('method_paypal', # forms.BooleanField( @@ -482,50 +622,60 @@ class StripeSettingsHolder(BasePaymentProvider): # 'before they work properly.'), # required=False, # )), - ('method_mobilepay', - forms.BooleanField( - label=_('MobilePay'), - disabled=self.event.currency not in ['DKK', 'EUR', 'NOK', 'SEK'], - help_text=_('Some payment methods might need to be enabled in the settings of your Stripe account ' - 'before they work properly.'), - required=False, - )), - ] + extra_fields + list(super().settings_form_fields.items()) + moto_settings + ( + "method_mobilepay", + forms.BooleanField( + label=_("MobilePay"), + disabled=self.event.currency + not in ["DKK", "EUR", "NOK", "SEK"], + help_text=_( + "Some payment methods might need to be enabled in the settings of your Stripe account " + "before they work properly." + ), + required=False, + ), + ), + ] + + extra_fields + + list(super().settings_form_fields.items()) + + moto_settings ) if not self.settings.connect_client_id or self.settings.secret_key: - d['connect_destination'] = forms.CharField( - label=_('Destination'), - validators=( - StripeKeyValidator(['acct_']), - ), - required=False + d["connect_destination"] = forms.CharField( + label=_("Destination"), + validators=(StripeKeyValidator(["acct_"]),), + required=False, ) - d.move_to_end('_enabled', last=False) + d.move_to_end("_enabled", last=False) return d class StripeMethod(BasePaymentProvider): - identifier = '' - method = '' - redirect_action_handling = 'iframe' # or redirect + identifier = "" + method = "" + redirect_action_handling = "iframe" # or redirect redirect_in_widget_allowed = True - confirmation_method = 'manual' - explanation = '' + confirmation_method = "manual" + explanation = "" def __init__(self, event: Event): super().__init__(event) - self.settings = SettingsSandbox('payment', 'stripe', event) + self.settings = SettingsSandbox("payment", "stripe", event) @property def test_mode_message(self): if self.settings.connect_client_id and not self.settings.secret_key: is_testmode = True else: - is_testmode = self.settings.secret_key and '_test_' in self.settings.secret_key + is_testmode = ( + self.settings.secret_key and "_test_" in self.settings.secret_key + ) if is_testmode: return mark_safe( - _('The Stripe plugin is operating in test mode. You can use one of many test ' - 'cards to perform a transaction. No money will actually be transferred.').format( + _( + "The Stripe plugin is operating in test mode. You can use one of many test " + "cards to perform a transaction. No money will actually be transferred." + ).format( args='href="https://stripe.com/docs/testing#cards" target="_blank"' ) ) @@ -537,8 +687,9 @@ class StripeMethod(BasePaymentProvider): @property def is_enabled(self) -> bool: - return self.settings.get('_enabled', as_type=bool) and self.settings.get('method_{}'.format(self.method), - as_type=bool) + return self.settings.get("_enabled", as_type=bool) and self.settings.get( + "method_{}".format(self.method), as_type=bool + ) def payment_refund_supported(self, payment: OrderPayment) -> bool: return True @@ -551,81 +702,103 @@ class StripeMethod(BasePaymentProvider): def _amount_to_decimal(self, cents): places = settings.CURRENCY_PLACES.get(self.event.currency, 2) - return round_decimal(float(cents) / (10 ** places), self.event.currency) + return round_decimal(float(cents) / (10**places), self.event.currency) def _decimal_to_int(self, amount): places = settings.CURRENCY_PLACES.get(self.event.currency, 2) - return int(amount * 10 ** places) + return int(amount * 10**places) def _get_amount(self, payment): return self._decimal_to_int(payment.amount) def _connect_kwargs(self, payment): d = {} - if self.settings.connect_client_id and self.settings.connect_user_id and not self.settings.secret_key: - fee = Decimal('0.00') - if self.settings.get('connect_app_fee_percent', as_type=Decimal): - fee = round_decimal(self.settings.get('connect_app_fee_percent', as_type=Decimal) * payment.amount / Decimal('100.00'), self.event.currency) + if ( + self.settings.connect_client_id + and self.settings.connect_user_id + and not self.settings.secret_key + ): + fee = Decimal("0.00") + if self.settings.get("connect_app_fee_percent", as_type=Decimal): + fee = round_decimal( + self.settings.get("connect_app_fee_percent", as_type=Decimal) + * payment.amount + / Decimal("100.00"), + self.event.currency, + ) if self.settings.connect_app_fee_max: - fee = min(fee, self.settings.get('connect_app_fee_max', as_type=Decimal)) - if self.settings.get('connect_app_fee_min', as_type=Decimal): - fee = max(fee, self.settings.get('connect_app_fee_min', as_type=Decimal)) + fee = min( + fee, self.settings.get("connect_app_fee_max", as_type=Decimal) + ) + if self.settings.get("connect_app_fee_min", as_type=Decimal): + fee = max( + fee, self.settings.get("connect_app_fee_min", as_type=Decimal) + ) if fee: - d['application_fee_amount'] = self._decimal_to_int(fee) + d["application_fee_amount"] = self._decimal_to_int(fee) if self.settings.connect_destination: - d['transfer_data'] = { - 'destination': self.settings.connect_destination - } + d["transfer_data"] = {"destination": self.settings.connect_destination} return d def statement_descriptor(self, payment, length=22): if self.settings.postfix: # If a custom postfix is set, we only transmit the order code, so we have as much room as possible for # the postfix. - return '{code} {postfix}'.format( + return "{code} {postfix}".format( code=payment.order.code, - postfix=re.sub("[^a-zA-Z0-9-_. ]", "", unidecode(str(self.settings.postfix))), + postfix=re.sub( + "[^a-zA-Z0-9-_. ]", "", unidecode(str(self.settings.postfix)) + ), )[:length] else: # If no custom postfix is set, we transmit the event slug and event name for backwards compatibility # with older pretix versions. - return '{event}-{code} {eventname}'.format( + return "{event}-{code} {eventname}".format( event=self.event.slug.upper(), code=payment.order.code, - eventname=re.sub("[^a-zA-Z0-9-_. ]", "", unidecode(str(self.event.name))), + eventname=re.sub( + "[^a-zA-Z0-9-_. ]", "", unidecode(str(self.event.name)) + ), )[:length] @property - def api_kwargs(self): - if self.settings.connect_client_id and self.settings.connect_user_id and not self.settings.secret_key: - if self.settings.get('endpoint', 'live') == 'live' and not self.event.testmode: - kwargs = { - 'api_key': self.settings.connect_secret_key, - 'stripe_account': self.settings.connect_user_id - } - else: - kwargs = { - 'api_key': self.settings.connect_test_secret_key, - 'stripe_account': self.settings.connect_user_id - } + def api_options(self): + if ( + self.settings.connect_client_id + and self.settings.connect_user_id + and not self.settings.secret_key + ): + options = {"stripe_account": self.settings.connect_user_id} else: - kwargs = { - 'api_key': self.settings.secret_key, - } - return kwargs + options = {} + return options def _init_api(self): - stripe.api_version = '2023-10-16' - stripe.set_app_info( - "pretix", - partner_id="pp_partner_FSaz4PpKIur7Ox", - version=__version__, - url="https://pretix.eu" - ) + if ( + self.settings.connect_client_id + and self.settings.connect_user_id + and not self.settings.secret_key + ): + if ( + self.settings.get("endpoint", "live") == "live" + and not self.event.testmode + ): + self.stripe_client = get_stripe_client(self.settings.connect_secret_key) + else: + self.stripe_client = get_stripe_client( + self.settings.connect_test_secret_key + ) + else: + self.stripe_client = get_stripe_client(self.settings.secret_key) 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} + 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: @@ -633,16 +806,16 @@ class StripeMethod(BasePaymentProvider): payment_info = json.loads(payment.info) else: payment_info = None - template = get_template('pretixplugins/stripe/pending.html') + 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': payment.order.tagged_secret('plugins:stripe') + "request": request, + "event": self.event, + "settings": self.settings, + "provider": self, + "order": payment.order, + "payment": payment, + "payment_info": payment_info, + "payment_hash": payment.order.tagged_secret("plugins:stripe"), } return template.render(ctx) @@ -650,12 +823,12 @@ class StripeMethod(BasePaymentProvider): return payment.info_data.get("id", None) def refund_matching_id(self, refund: OrderRefund): - return refund.info_data.get('id', None) + 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) + "payment_method": payment.info_data.get("payment_method", None), } def api_refund_details(self, refund: OrderRefund): @@ -670,41 +843,53 @@ class StripeMethod(BasePaymentProvider): 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 "amount" in payment_info: + payment_info["amount"] /= 10 ** settings.CURRENCY_PLACES.get( + self.event.currency, 2 + ) if isinstance(payment_info.get("latest_charge"), dict): - details = payment_info["latest_charge"].get("payment_method_details", {}) + 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", {}) + 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', {}) + details.setdefault("owner", {}) - template = get_template('pretixplugins/stripe/control.html') + 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, + "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): + 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'], + 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') + salt="safe-redirect", + ) ) else: return str(url) @@ -717,54 +902,68 @@ class StripeMethod(BasePaymentProvider): OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=refund.payment.pk) if not payment_info: - raise PaymentException(_('No payment information found.')) + raise PaymentException(_("No payment information found.")) try: - if payment_info['id'].startswith('pi_'): - if 'latest_charge' in payment_info and isinstance(payment_info.get("latest_charge"), dict): - chargeid = payment_info['latest_charge']['id'] + if payment_info["id"].startswith("pi_"): + if "latest_charge" in payment_info and isinstance( + payment_info.get("latest_charge"), dict + ): + chargeid = payment_info["latest_charge"]["id"] else: - chargeid = payment_info['charges']['data'][0]['id'] + chargeid = payment_info["charges"]["data"][0]["id"] else: - chargeid = payment_info['id'] + 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, + kwargs["reverse_transfer"] = True + r = self.stripe_client.refunds.create( + params={ + "charge": chargeid, + "amount": self._get_amount(refund), + **kwargs, + }, + options=self.api_options, ) - 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)) + 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)) + 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.')) + 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')) + logger.error("Stripe error: %s" % str(err)) + raise PaymentException(_("Stripe returned an error")) else: refund.info = str(r) - if r.status in ('succeeded', 'pending'): + if r.status in ("succeeded", "pending"): refund.done() - elif r.status in ('failed', 'canceled'): + elif r.status in ("failed", "canceled"): refund.state = OrderRefund.REFUND_STATE_FAILED refund.execution_date = now() refund.save() @@ -775,63 +974,116 @@ class StripeMethod(BasePaymentProvider): 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' + "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") and not isinstance(d["latest_charge"], str): 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', + "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["latest_charge"] = { + k: v for k, v in d["latest_charge"].items() if k in keys } - new['_shredded'] = True - obj.info = json.dumps(new) - obj.save(update_fields=['info']) + 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"), + }, + } - for le in obj.order.all_logentries().filter( - action_type="pretix.plugins.stripe.event" - ).exclude(data="", shredded=True): + 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] = '█' + 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']) + 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), '') != '' + 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 + 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.')) + if payment_method_id == "": + messages.warning( + request, _("You may need to enable JavaScript for Stripe payments.") + ) return False return True @@ -839,8 +1091,13 @@ class StripeMethod(BasePaymentProvider): 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)] + 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 @@ -853,107 +1110,120 @@ class StripeMethod(BasePaymentProvider): 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 + 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 # Fixme? 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 - } - } - }) + params.update({"payment_method_options": {"card": {"moto": True}}}) if self.method == "card": - params['statement_descriptor_suffix'] = self.statement_descriptor(payment) + params["statement_descriptor_suffix"] = self.statement_descriptor( + payment + ) else: - params['statement_descriptor'] = self.statement_descriptor(payment) + params["statement_descriptor"] = self.statement_descriptor(payment) - 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 - ), - metadata={ - 'order': str(payment.order.id), - 'event': self.event.id, - 'code': payment.order.code + intent = self.stripe_client.payment_intents.create( + params={ + "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 + ), + "metadata": { + "order": str(payment.order.id), + "event": self.event.id, + "code": payment.order.code, + }, + "return_url": build_absolute_uri( + self.event, + "plugins:stripe:sca.return", + kwargs={ + "order": payment.order.code, + "payment": payment.pk, + "hash": payment.order.tagged_secret("plugins:stripe"), + }, + ), + "expand": ["latest_charge"], + **params, }, - # 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': payment.order.tagged_secret('plugins:stripe'), - }), - expand=['latest_charge'], - **params + options=self.api_options, ) else: payment_info = json.loads(payment.info) - if 'id' in payment_info: + if "id" in payment_info: if not intent: - intent = stripe.PaymentIntent.retrieve( - payment_info['id'], - expand=["latest_charge"], - **self.api_kwargs + intent = self.stripe_client.payment_intents.retrieve( + payment_info["id"], + params={ + "expand": ["latest_charge"], + }, + options=self.api_options, ) else: return except stripe.error.CardError as e: if e.json_body: - err = e.json_body['error'] - logger.exception('Stripe error: %s' % str(err)) + 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']) + 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 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': + 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.')) + 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} + defaults={"order": payment.order, "payment": payment}, ) - if intent.status == 'requires_action': + if intent.status == "requires_action": payment.info = str(intent) - if intent.next_action.type == 'multibanco_display_details': + if intent.next_action.type == "multibanco_display_details": payment.state = OrderPayment.PAYMENT_STATE_PENDING payment.save() return @@ -962,19 +1232,19 @@ class StripeMethod(BasePaymentProvider): payment.save() return self._redirect_to_sca(request, payment) - if intent.status == 'requires_action': + if intent.status == "requires_action": payment.info = str(intent) payment.state = OrderPayment.PAYMENT_STATE_CREATED payment.save() return self._redirect_to_sca(request, payment) - if intent.status == 'requires_confirmation': + 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: + elif intent.status == "succeeded" and intent.latest_charge.paid: try: payment.info = str(intent) payment.confirm() @@ -982,36 +1252,61 @@ class StripeMethod(BasePaymentProvider): raise PaymentException(str(e)) except SendMailException: - raise PaymentException(_('There was an error sending the confirmation mail.')) - elif intent.status == 'processing': + 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.')) + 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': + elif intent.status == "requires_payment_method": if request: - messages.warning(request, _('Your payment failed. Please try again.')) + messages.warning( + request, _("Your payment failed. Please try again.") + ) payment.fail(info=str(intent)) return else: - logger.info('Charge failed: %s' % str(intent)) + logger.info("Charge failed: %s" % str(intent)) payment.fail(info=str(intent)) - raise PaymentException(_('Stripe reported an error: %s') % intent.last_payment_error.message) + raise PaymentException( + _("Stripe reported an error: %s") + % intent.last_payment_error.message + ) def _redirect_to_sca(self, request, payment): - url = build_absolute_uri(self.event, 'plugins:stripe:sca', kwargs={ - 'order': payment.order.code, - 'payment': payment.pk, - 'hash': payment.order.tagged_secret('plugins:stripe'), - }) - if not self.redirect_in_widget_allowed and request.session.get('iframe_session', False): - return build_absolute_uri(self.event, 'plugins:stripe:redirect') + '?data=' + signing.dumps({ - 'url': url, - 'session': {}, - }, salt='safe-redirect') + url = build_absolute_uri( + self.event, + "plugins:stripe:sca", + kwargs={ + "order": payment.order.code, + "payment": payment.pk, + "hash": payment.order.tagged_secret("plugins:stripe"), + }, + ) + if not self.redirect_in_widget_allowed and request.session.get( + "iframe_session", False + ): + return ( + build_absolute_uri(self.event, "plugins:stripe:redirect") + + "?data=" + + signing.dumps( + { + "url": url, + "session": {}, + }, + salt="safe-redirect", + ) + ) return url @@ -1021,15 +1316,21 @@ class StripeMethod(BasePaymentProvider): 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': payment.order.tagged_secret('plugins:stripe'), - }), - expand=["latest_charge"], - **self.api_kwargs + intent = self.stripe_client.payment_intents.confirm( + payment_info["id"], + params={ + "return_url": build_absolute_uri( + self.event, + "plugins:stripe:sca.return", + kwargs={ + "order": payment.order.code, + "payment": payment.pk, + "hash": payment.order.tagged_secret("plugins:stripe"), + }, + ), + "expand": ["latest_charge"], + }, + options=self.api_options, ) payment.info = str(intent) @@ -1038,30 +1339,40 @@ class StripeMethod(BasePaymentProvider): 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)) + 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']) + 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)) + 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.')) + 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 StripeRedirectMethod(StripeMethod): @@ -1077,7 +1388,9 @@ class StripeRedirectMethod(StripeMethod): 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 + request.session["payment_stripe_{}_payment_method_id".format(self.method)] = ( + None + ) return True def _payment_intent_kwargs(self, request, payment): @@ -1088,21 +1401,23 @@ class StripeRedirectMethod(StripeMethod): } def payment_form_render(self, request) -> str: - template = get_template('pretixplugins/stripe/checkout_payment_form_simple_noform.html') + template = get_template( + "pretixplugins/stripe/checkout_payment_form_simple_noform.html" + ) ctx = { - 'request': request, - 'event': self.event, - 'settings': self.settings, - 'explanation': self.explanation, + "request": request, + "event": self.event, + "settings": self.settings, + "explanation": self.explanation, } return template.render(ctx) class StripeCC(StripeMethod): - identifier = 'stripe' - verbose_name = _('Credit card via Stripe') - public_name = _('Credit card') - method = 'card' + identifier = "stripe" + verbose_name = _("Credit card via Stripe") + public_name = _("Credit card") + method = "card" @property def walletqueries(self): @@ -1115,26 +1430,28 @@ class StripeCC(StripeMethod): def payment_form_render(self, request, total, order=None) -> str: account = get_stripe_account_key(self) - if not RegisteredApplePayDomain.objects.filter(account=account, domain=request.host).exists(): + if not RegisteredApplePayDomain.objects.filter( + account=account, domain=request.host + ).exists(): stripe_verify_domain.apply_async(args=(self.event.pk, request.host)) - template = get_template('pretixplugins/stripe/checkout_payment_form_card.html') + template = get_template("pretixplugins/stripe/checkout_payment_form_card.html") ctx = { - 'request': request, - 'event': self.event, - 'total': self._decimal_to_int(total), - 'settings': self.settings, - 'explanation': self.explanation, - 'is_moto': self.is_moto(request) + "request": request, + "event": self.event, + "total": self._decimal_to_int(total), + "settings": self.settings, + "explanation": self.explanation, + "is_moto": self.is_moto(request), } return template.render(ctx) def _migrate_session(self, request): # todo: remove after pretix 2023.8 was released keymap = { - 'payment_stripe_payment_method_id': 'payment_stripe_card_payment_method_id', - 'payment_stripe_brand': 'payment_stripe_card_brand', - 'payment_stripe_last4': 'payment_stripe_card_last4', + "payment_stripe_payment_method_id": "payment_stripe_card_payment_method_id", + "payment_stripe_brand": "payment_stripe_card_brand", + "payment_stripe_last4": "payment_stripe_card_last4", } for old, new in keymap.items(): if old in request.session: @@ -1143,8 +1460,12 @@ class StripeCC(StripeMethod): def checkout_prepare(self, request, cart): self._migrate_session(request) - request.session['payment_stripe_card_brand'] = request.POST.get('stripe_card_brand', '') - request.session['payment_stripe_card_last4'] = request.POST.get('stripe_card_last4', '') + request.session["payment_stripe_card_brand"] = request.POST.get( + "stripe_card_brand", "" + ) + request.session["payment_stripe_card_last4"] = request.POST.get( + "stripe_card_last4", "" + ) return super().checkout_prepare(request, cart) @@ -1160,13 +1481,15 @@ class StripeCC(StripeMethod): # We don't have a payment yet when checking if we should display the MOTO-flag # However, before we execute the payment, we absolutely have to check if the request-SalesChannel as well as the # order are tagged as a reseller-transaction. Else, a user with a valid reseller-session might be able to place - # a MOTO transaction trough the WebShop. + # a MOTO transaction through the WebShop. - moto = self.settings.get('reseller_moto', False, as_type=bool) and \ - request.sales_channel.identifier == 'resellers' + moto = ( + self.settings.get("reseller_moto", False, as_type=bool) + and request.sales_channel.identifier == "resellers" + ) if payment: - return moto and payment.order.sales_channel.identifier == 'resellers' + return moto and payment.order.sales_channel.identifier == "resellers" return moto @@ -1178,33 +1501,39 @@ class StripeCC(StripeMethod): else: card = pi["source"]["card"] except: - logger.exception('Could not parse payment data') + logger.exception("Could not parse payment data") return super().payment_presale_render(payment) - return f'{self.public_name}: ' \ - f'{card.get("brand", "").title()} ' \ - f'************{card.get("last4", "****")}, ' \ - f'{_("expires {month}/{year}").format(month=card.get("exp_month"), year=card.get("exp_year"))}' + return ( + f"{self.public_name}: " + f'{card.get("brand", "").title()} ' + f'************{card.get("last4", "****")}, ' + f'{_("expires {month}/{year}").format(month=card.get("exp_month"), year=card.get("exp_year"))}' + ) class StripeSEPADirectDebit(StripeMethod): - identifier = 'stripe_sepa_debit' - verbose_name = _('SEPA Debit via Stripe') - public_name = _('SEPA Debit') - method = 'sepa_debit' + identifier = "stripe_sepa_debit" + verbose_name = _("SEPA Debit via Stripe") + public_name = _("SEPA Debit") + method = "sepa_debit" ia = InvoiceAddress() - def payment_form_render(self, request: HttpRequest, total: Decimal, order: Order=None) -> str: + def payment_form_render( + self, request: HttpRequest, total: Decimal, order: Order = None + ) -> str: def get_invoice_address(): - if order and getattr(order, 'invoice_address', None): + if order and getattr(order, "invoice_address", None): request._checkout_flow_invoice_address = order.invoice_address - if not hasattr(request, '_checkout_flow_invoice_address'): + if not hasattr(request, "_checkout_flow_invoice_address"): cs = cart_session(request) - iapk = cs.get('invoice_address') + iapk = cs.get("invoice_address") if not iapk: request._checkout_flow_invoice_address = InvoiceAddress() else: try: - request._checkout_flow_invoice_address = InvoiceAddress.objects.get(pk=iapk, order__isnull=True) + request._checkout_flow_invoice_address = ( + InvoiceAddress.objects.get(pk=iapk, order__isnull=True) + ) except InvoiceAddress.DoesNotExist: request._checkout_flow_invoice_address = InvoiceAddress() return request._checkout_flow_invoice_address @@ -1212,14 +1541,16 @@ class StripeSEPADirectDebit(StripeMethod): cs = cart_session(request) self.ia = get_invoice_address() - template = get_template('pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html') + template = get_template( + "pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html" + ) ctx = { - 'request': request, - 'event': self.event, - 'settings': self.settings, - 'form': self.payment_form(request), - 'explanation': self.explanation, - 'email': order.email if order else cs.get('email', '') + "request": request, + "event": self.event, + "settings": self.settings, + "form": self.payment_form(request), + "explanation": self.explanation, + "email": order.email if order else cs.get("email", ""), } return template.render(ctx) @@ -1227,78 +1558,93 @@ class StripeSEPADirectDebit(StripeMethod): def payment_form_fields(self): return OrderedDict( [ - ('accountname', - forms.CharField( - label=_('Account Holder Name'), - initial=self.ia.name, - )), - ('line1', - forms.CharField( - label=_('Account Holder Street'), - required=False, - widget=forms.TextInput( - attrs={ - 'data-display-dependency': '#stripe_sepa_debit_country', - 'data-required-if': '#stripe_sepa_debit_country' - } - ), - initial=self.ia.street, - )), - ('postal_code', - forms.CharField( - label=_('Account Holder Postal Code'), - required=False, - widget=forms.TextInput( - attrs={ - 'data-display-dependency': '#stripe_sepa_debit_country', - 'data-required-if': '#stripe_sepa_debit_country' - } - ), - initial=self.ia.zipcode, - )), - ('city', - forms.CharField( - label=_('Account Holder City'), - required=False, - widget=forms.TextInput( - attrs={ - 'data-display-dependency': '#stripe_sepa_debit_country', - 'data-required-if': '#stripe_sepa_debit_country' - } - ), - initial=self.ia.city, - )), - ('country', - forms.ChoiceField( - label=_('Account Holder Country'), - required=False, - choices=CachedCountries(), - widget=forms.Select( - attrs={ - 'data-display-dependency': '#stripe_sepa_debit_country', - 'data-required-if': '#stripe_sepa_debit_country' - } - ), - initial=self.ia.country or guess_country(self.event), - )), - ]) + ( + "accountname", + forms.CharField( + label=_("Account Holder Name"), + initial=self.ia.name, + ), + ), + ( + "line1", + forms.CharField( + label=_("Account Holder Street"), + required=False, + widget=forms.TextInput( + attrs={ + "data-display-dependency": "#stripe_sepa_debit_country", + "data-required-if": "#stripe_sepa_debit_country", + } + ), + initial=self.ia.street, + ), + ), + ( + "postal_code", + forms.CharField( + label=_("Account Holder Postal Code"), + required=False, + widget=forms.TextInput( + attrs={ + "data-display-dependency": "#stripe_sepa_debit_country", + "data-required-if": "#stripe_sepa_debit_country", + } + ), + initial=self.ia.zipcode, + ), + ), + ( + "city", + forms.CharField( + label=_("Account Holder City"), + required=False, + widget=forms.TextInput( + attrs={ + "data-display-dependency": "#stripe_sepa_debit_country", + "data-required-if": "#stripe_sepa_debit_country", + } + ), + initial=self.ia.city, + ), + ), + ( + "country", + forms.ChoiceField( + label=_("Account Holder Country"), + required=False, + choices=CachedCountries(), + widget=forms.Select( + attrs={ + "data-display-dependency": "#stripe_sepa_debit_country", + "data-required-if": "#stripe_sepa_debit_country", + } + ), + initial=self.ia.country or guess_country(self.event), + ), + ), + ] + ) def _payment_intent_kwargs(self, request, payment): return { - 'mandate_data': { - 'customer_acceptance': { - 'type': 'online', - 'online': { - 'ip_address': get_client_ip(request), - 'user_agent': request.META['HTTP_USER_AGENT'], - } + "mandate_data": { + "customer_acceptance": { + "type": "online", + "online": { + "ip_address": get_client_ip(request), + "user_agent": request.META["HTTP_USER_AGENT"], + }, }, } } def checkout_prepare(self, request, cart): - request.session['payment_stripe_sepa_debit_last4'] = request.POST.get('stripe_sepa_debit_last4', '') - request.session['payment_stripe_sepa_debit_bank'] = request.POST.get('stripe_sepa_debit_bank', '') + request.session["payment_stripe_sepa_debit_last4"] = request.POST.get( + "stripe_sepa_debit_last4", "" + ) + request.session["payment_stripe_sepa_debit_bank"] = request.POST.get( + "stripe_sepa_debit_bank", "" + ) return super().checkout_prepare(request, cart) @@ -1306,53 +1652,61 @@ class StripeSEPADirectDebit(StripeMethod): try: return super().execute_payment(request, payment) finally: - fields = ['accountname', 'line1', 'postal_code', 'city', 'country'] + fields = ["accountname", "line1", "postal_code", "city", "country"] for field in fields: - if 'payment_stripe_sepa_debit_{}'.format(field) in request.session: - del request.session['payment_stripe_sepa_debit_{}'.format(field)] + if "payment_stripe_sepa_debit_{}".format(field) in request.session: + del request.session["payment_stripe_sepa_debit_{}".format(field)] class StripeAffirm(StripeMethod): - identifier = 'stripe_affirm' - verbose_name = _('Affirm via Stripe') - public_name = _('Affirm') - method = 'affirm' - redirect_action_handling = 'redirect' + identifier = "stripe_affirm" + verbose_name = _("Affirm via Stripe") + public_name = _("Affirm") + method = "affirm" + redirect_action_handling = "redirect" def payment_is_valid_session(self, request): # Affirm 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: + if "payment_stripe_{}_payment_method_id".format(self.method) in request.session: return True return False def checkout_prepare(self, request, cart): # Affirm 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 + request.session["payment_stripe_{}_payment_method_id".format(self.method)] = ( + None + ) return True - def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool: - return Decimal(50.00) <= total <= Decimal(30000.00) and super().is_allowed(request, total) + def is_allowed(self, request: HttpRequest, total: Decimal = None) -> bool: + return Decimal(50.00) <= total <= Decimal(30000.00) and super().is_allowed( + request, total + ) - def order_change_allowed(self, order: Order, request: HttpRequest=None) -> bool: - return Decimal(50.00) <= order.pending_sum <= Decimal(30000.00) and super().order_change_allowed(order, request) + def order_change_allowed(self, order: Order, request: HttpRequest = None) -> bool: + return Decimal(50.00) <= order.pending_sum <= Decimal( + 30000.00 + ) and super().order_change_allowed(order, request) def _payment_intent_kwargs(self, request, payment): return { - 'payment_method_data': { - 'type': 'affirm', + "payment_method_data": { + "type": "affirm", } } def payment_form_render(self, request, total, order=None) -> str: - template = get_template('pretixplugins/stripe/checkout_payment_form_simple_messaging_noform.html') + template = get_template( + "pretixplugins/stripe/checkout_payment_form_simple_messaging_noform.html" + ) ctx = { - 'request': request, - 'event': self.event, - 'total': self._decimal_to_int(total), - 'explanation': self.explanation, - 'method': self.method, + "request": request, + "event": self.event, + "total": self._decimal_to_int(total), + "explanation": self.explanation, + "method": self.method, } return template.render(ctx) @@ -1362,21 +1716,41 @@ class StripeKlarna(StripeRedirectMethod): verbose_name = _("Klarna via Stripe") public_name = _("Klarna") method = "klarna" - allowed_countries = {"US", "CA", "AU", "NZ", "GB", "IE", "FR", "ES", "DE", "AT", "BE", "DK", "FI", "IT", "NL", "NO", "SE"} + allowed_countries = { + "US", + "CA", + "AU", + "NZ", + "GB", + "IE", + "FR", + "ES", + "DE", + "AT", + "BE", + "DK", + "FI", + "IT", + "NL", + "NO", + "SE", + } redirect_in_widget_allowed = False def _detect_country(self, request, order=None): def get_invoice_address(): - if order and getattr(order, 'invoice_address', None): + if order and getattr(order, "invoice_address", None): request._checkout_flow_invoice_address = order.invoice_address - if not hasattr(request, '_checkout_flow_invoice_address'): + if not hasattr(request, "_checkout_flow_invoice_address"): cs = cart_session(request) - iapk = cs.get('invoice_address') + iapk = cs.get("invoice_address") if not iapk: request._checkout_flow_invoice_address = InvoiceAddress() else: try: - request._checkout_flow_invoice_address = InvoiceAddress.objects.get(pk=iapk, order__isnull=True) + request._checkout_flow_invoice_address = ( + InvoiceAddress.objects.get(pk=iapk, order__isnull=True) + ) except InvoiceAddress.DoesNotExist: request._checkout_flow_invoice_address = InvoiceAddress() return request._checkout_flow_invoice_address @@ -1415,8 +1789,8 @@ class StripeKlarna(StripeRedirectMethod): "event": self.event, "total": self._decimal_to_int(total), "method": self.method, - 'explanation': self.explanation, - "country": self._detect_country(request, order) + "explanation": self.explanation, + "country": self._detect_country(request, order), } return template.render(ctx) @@ -1442,56 +1816,62 @@ class StripeKlarna(StripeRedirectMethod): class StripeRedirectWithAccountNamePaymentIntentMethod(StripeRedirectMethod): def payment_form_render(self, request) -> str: - template = get_template('pretixplugins/stripe/checkout_payment_form_simple.html') + template = get_template( + "pretixplugins/stripe/checkout_payment_form_simple.html" + ) ctx = { - 'request': request, - 'event': self.event, - 'settings': self.settings, - 'explanation': self.explanation, - 'form': self.payment_form(request) + "request": request, + "event": self.event, + "settings": self.settings, + "explanation": self.explanation, + "form": self.payment_form(request), } return template.render(ctx) @property def payment_form_fields(self): - return OrderedDict([ - ('account', forms.CharField(label=_('Account holder'))), - ]) + return OrderedDict( + [ + ("account", forms.CharField(label=_("Account holder"))), + ] + ) def execute_payment(self, request: HttpRequest, payment: OrderPayment): try: return super().execute_payment(request, payment) finally: - if f'payment_stripe_{self.method}_account' in request.session: - del request.session[f'payment_stripe_{self.method}_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[f"payment_stripe_{self.method}_payment_method_id"] = None - request.session[f'payment_stripe_{self.method}_account'] = form.cleaned_data['account'] + 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' + identifier = "stripe_giropay" + verbose_name = _("giropay via Stripe") + public_name = _("giropay") + method = "giropay" explanation = _( - 'giropay is an online payment method available to all customers of most German banks, usually after one-time ' - 'activation. Please keep your online banking account and login information available.' + "giropay is an online payment method available to all customers of most German banks, usually after one-time " + "activation. Please keep your online banking account and login information available." ) redirect_in_widget_allowed = False - def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool: + def is_allowed(self, request: HttpRequest, total: Decimal = None) -> bool: # Stripe<>giropay is shut down July 1st return super().is_allowed(request, total) and now() < datetime( 2024, 7, 1, 0, 0, 0, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin") ) - def order_change_allowed(self, order: Order, request: HttpRequest=None) -> bool: + def order_change_allowed(self, order: Order, request: HttpRequest = None) -> bool: return super().order_change_allowed(order, request) and now() < datetime( 2024, 7, 1, 0, 0, 0, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin") ) @@ -1502,7 +1882,8 @@ class StripeGiropay(StripeRedirectWithAccountNamePaymentIntentMethod): "type": "giropay", "giropay": {}, "billing_details": { - "name": request.session.get(f"payment_stripe_{self.method}_account") or gettext("unknown name") + "name": request.session.get(f"payment_stripe_{self.method}_account") + or gettext("unknown name") }, } } @@ -1510,59 +1891,67 @@ class StripeGiropay(StripeRedirectWithAccountNamePaymentIntentMethod): def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: - return gettext('Bank account at {bank}').format( + 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", "?") + 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') + logger.exception("Could not parse payment data") return super().payment_presale_render(payment) class StripeIdeal(StripeRedirectMethod): - identifier = 'stripe_ideal' - verbose_name = _('iDEAL via Stripe') - public_name = _('iDEAL') - method = 'ideal' + identifier = "stripe_ideal" + verbose_name = _("iDEAL via Stripe") + public_name = _("iDEAL") + method = "ideal" explanation = _( - 'iDEAL is an online payment method available to customers of Dutch banks. Please keep your online ' - 'banking account and login information available.' + "iDEAL is an online payment method available to customers of Dutch banks. Please keep your online " + "banking account and login information available." ) redirect_in_widget_allowed = False def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: - return gettext('Bank account at {bank}').format( + 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() + 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') + logger.exception("Could not parse payment data") return super().payment_presale_render(payment) class StripeAlipay(StripeRedirectMethod): - identifier = 'stripe_alipay' - verbose_name = _('Alipay via Stripe') - public_name = _('Alipay') - method = 'alipay' - confirmation_method = 'automatic' + identifier = "stripe_alipay" + verbose_name = _("Alipay via Stripe") + public_name = _("Alipay") + method = "alipay" + confirmation_method = "automatic" explanation = _( - 'This payment method is available to customers of the Chinese payment system Alipay. Please keep ' - 'your login information available.' + "This payment method is available to customers of the Chinese payment system Alipay. Please keep " + "your login information available." ) class StripeBancontact(StripeRedirectWithAccountNamePaymentIntentMethod): - identifier = 'stripe_bancontact' - verbose_name = _('Bancontact via Stripe') - public_name = _('Bancontact') - method = 'bancontact' + identifier = "stripe_bancontact" + verbose_name = _("Bancontact via Stripe") + public_name = _("Bancontact") + method = "bancontact" redirect_in_widget_allowed = False def _payment_intent_kwargs(self, request, payment): @@ -1570,7 +1959,8 @@ class StripeBancontact(StripeRedirectWithAccountNamePaymentIntentMethod): "payment_method_data": { "type": "bancontact", "billing_details": { - "name": request.session.get(f"payment_stripe_{self.method}_account") or gettext("unknown name") + "name": request.session.get(f"payment_stripe_{self.method}_account") + or gettext("unknown name") }, } } @@ -1578,64 +1968,82 @@ class StripeBancontact(StripeRedirectWithAccountNamePaymentIntentMethod): def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: - return gettext('Bank account at {bank}').format( + 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", "?") + 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') + logger.exception("Could not parse payment data") return super().payment_presale_render(payment) class StripeSofort(StripeRedirectMethod): - identifier = 'stripe_sofort' - verbose_name = _('SOFORT via Stripe') - public_name = _('SOFORT (instant bank transfer)') - method = 'sofort' + identifier = "stripe_sofort" + verbose_name = _("SOFORT via Stripe") + public_name = _("SOFORT (instant bank transfer)") + method = "sofort" redirect_in_widget_allowed = False - def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool: + def is_allowed(self, request: HttpRequest, total: Decimal = None) -> bool: # Stripe<>Sofort is shut down November 29th return super().is_allowed(request, total) and now() < datetime( 2024, 11, 29, 0, 0, 0, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin") ) - def order_change_allowed(self, order: Order, request: HttpRequest=None) -> bool: + def order_change_allowed(self, order: Order, request: HttpRequest = None) -> bool: return super().order_change_allowed(order, request) and now() < datetime( 2024, 11, 29, 0, 0, 0, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin") ) def payment_form_render(self, request) -> str: - template = get_template('pretixplugins/stripe/checkout_payment_form_simple.html') + template = get_template( + "pretixplugins/stripe/checkout_payment_form_simple.html" + ) ctx = { - 'request': request, - 'event': self.event, - 'settings': self.settings, - 'explanation': self.explanation, - 'form': self.payment_form(request) + "request": request, + "event": self.event, + "settings": self.settings, + "explanation": self.explanation, + "form": self.payment_form(request), } return template.render(ctx) @property def payment_form_fields(self): - return OrderedDict([ - ('bank_country', forms.ChoiceField(label=_('Country of your bank'), choices=( - ('de', _('Germany')), - ('at', _('Austria')), - ('be', _('Belgium')), - ('nl', _('Netherlands')), - ('es', _('Spain')) - ))), - ]) + return OrderedDict( + [ + ( + "bank_country", + forms.ChoiceField( + label=_("Country of your bank"), + choices=( + ("de", _("Germany")), + ("at", _("Austria")), + ("be", _("Belgium")), + ("nl", _("Netherlands")), + ("es", _("Spain")), + ), + ), + ), + ] + ) 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() + "country": ( + request.session.get( + f"payment_stripe_{self.method}_bank_country" + ) + or "DE" + ).upper() }, } } @@ -1644,38 +2052,40 @@ class StripeSofort(StripeRedirectMethod): 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'] + 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(f'payment_stripe_{self.method}_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[f'payment_stripe_{self.method}_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 def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: - return gettext('Bank account {iban} at {bank}').format( + return gettext("Bank account {iban} at {bank}").format( iban=f'{pi["source"]["sofort"]["country"]}****{pi["source"]["sofort"]["iban_last4"]}', - bank=pi["source"]["sofort"]["bank_name"] + bank=pi["source"]["sofort"]["bank_name"], ) except: - logger.exception('Could not parse payment data') + logger.exception("Could not parse payment data") return super().payment_presale_render(payment) class StripeEPS(StripeRedirectWithAccountNamePaymentIntentMethod): - identifier = 'stripe_eps' - verbose_name = _('EPS via Stripe') - public_name = _('EPS') - method = 'eps' + identifier = "stripe_eps" + verbose_name = _("EPS via Stripe") + public_name = _("EPS") + method = "eps" redirect_in_widget_allowed = False def _payment_intent_kwargs(self, request, payment): @@ -1683,7 +2093,8 @@ class StripeEPS(StripeRedirectWithAccountNamePaymentIntentMethod): "payment_method_data": { "type": "eps", "billing_details": { - "name": request.session.get(f"payment_stripe_{self.method}_account") or gettext("unknown name") + "name": request.session.get(f"payment_stripe_{self.method}_account") + or gettext("unknown name") }, } } @@ -1691,24 +2102,29 @@ class StripeEPS(StripeRedirectWithAccountNamePaymentIntentMethod): def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: - return gettext('Bank account at {bank}').format( + 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() + 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') + logger.exception("Could not parse payment data") return super().payment_presale_render(payment) class StripeMultibanco(StripeRedirectMethod): - identifier = 'stripe_multibanco' - verbose_name = _('Multibanco via Stripe') - public_name = _('Multibanco') - method = 'multibanco' + 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.' + "Multibanco is a payment method available to Portuguese bank account holders." ) redirect_in_widget_allowed = False abort_pending_allowed = True @@ -1719,19 +2135,19 @@ class StripeMultibanco(StripeRedirectMethod): "type": "multibanco", "billing_details": { "email": payment.order.email, - } + }, } } class StripePrzelewy24(StripeRedirectMethod): - identifier = 'stripe_przelewy24' - verbose_name = _('Przelewy24 via Stripe') - public_name = _('Przelewy24') - method = 'p24' + identifier = "stripe_przelewy24" + verbose_name = _("Przelewy24 via Stripe") + public_name = _("Przelewy24") + method = "p24" explanation = _( - 'Przelewy24 is an online payment method available to customers of Polish banks. Please keep your online ' - 'banking account and login information available.' + "Przelewy24 is an online payment method available to customers of Polish banks. Please keep your online " + "banking account and login information available." ) redirect_in_widget_allowed = False @@ -1739,44 +2155,51 @@ class StripePrzelewy24(StripeRedirectMethod): return { "payment_method_data": { "type": "p24", - "billing_details": { - "email": payment.order.email - }, + "billing_details": {"email": payment.order.email}, } } @property def is_enabled(self) -> bool: - return self.settings.get('_enabled', as_type=bool) and self.settings.get('method_przelewy24', as_type=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( + 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() + 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') + logger.exception("Could not parse payment data") return super().payment_presale_render(payment) class StripeWeChatPay(StripeRedirectMethod): - identifier = 'stripe_wechatpay' - verbose_name = _('WeChat Pay via Stripe') - public_name = _('WeChat Pay') - method = 'wechat_pay' - confirmation_method = 'automatic' + identifier = "stripe_wechatpay" + verbose_name = _("WeChat Pay via Stripe") + public_name = _("WeChat Pay") + method = "wechat_pay" + confirmation_method = "automatic" explanation = _( - 'This payment method is available to users of the Chinese app WeChat. Please keep your login information ' - 'available.' + "This payment method is available to users of the Chinese app WeChat. Please keep your login information " + "available." ) @property def is_enabled(self) -> bool: - return self.settings.get('_enabled', as_type=bool) and self.settings.get('method_wechatpay', as_type=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 { @@ -1784,22 +2207,20 @@ class StripeWeChatPay(StripeRedirectMethod): "type": "wechat_pay", }, "payment_method_options": { - "wechat_pay": { - "client": "web" - }, - } + "wechat_pay": {"client": "web"}, + }, } class StripeRevolutPay(StripeRedirectMethod): - identifier = 'stripe_revolut_pay' - verbose_name = _('Revolut Pay via Stripe') - public_name = _('Revolut Pay') - method = 'revolut_pay' - confirmation_method = 'automatic' + identifier = "stripe_revolut_pay" + verbose_name = _("Revolut Pay via Stripe") + public_name = _("Revolut Pay") + method = "revolut_pay" + confirmation_method = "automatic" explanation = _( - 'This payment method is available to users of the Revolut app. Please keep your login information ' - 'available.' + "This payment method is available to users of the Revolut app. Please keep your login information " + "available." ) def _payment_intent_kwargs(self, request, payment): @@ -1811,22 +2232,22 @@ class StripeRevolutPay(StripeRedirectMethod): class StripePayPal(StripeRedirectMethod): - identifier = 'stripe_paypal' - verbose_name = _('PayPal via Stripe') - public_name = _('PayPal') - method = 'paypal' + identifier = "stripe_paypal" + verbose_name = _("PayPal via Stripe") + public_name = _("PayPal") + method = "paypal" redirect_in_widget_allowed = False class StripeSwish(StripeRedirectMethod): - identifier = 'stripe_swish' - verbose_name = _('Swish via Stripe') - public_name = _('Swish') - method = 'swish' - confirmation_method = 'automatic' + identifier = "stripe_swish" + verbose_name = _("Swish via Stripe") + public_name = _("Swish") + method = "swish" + confirmation_method = "automatic" explanation = _( - 'This payment method is available to users of the Swedish apps Swish and BankID. Please have your app ' - 'ready.' + "This payment method is available to users of the Swedish apps Swish and BankID. Please have your app " + "ready." ) def _payment_intent_kwargs(self, request, payment): @@ -1838,23 +2259,27 @@ class StripeSwish(StripeRedirectMethod): "swish": { "reference": payment.order.full_code, }, - } + }, } class StripeTwint(StripeRedirectMethod): - identifier = 'stripe_twint' - verbose_name = _('TWINT via Stripe') - public_name = 'TWINT' - method = 'twint' - confirmation_method = 'automatic' + identifier = "stripe_twint" + verbose_name = _("TWINT via Stripe") + public_name = "TWINT" + method = "twint" + confirmation_method = "automatic" explanation = _( - 'This payment method is available to users of the Swiss app TWINT. Please have your app ' - 'ready.' + "This payment method is available to users of the Swiss app TWINT. Please have your app " + "ready." ) - def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool: - return super().is_allowed(request, total) and request.event.currency == "CHF" and total <= Decimal("5000.00") + def is_allowed(self, request: HttpRequest, total: Decimal = None) -> bool: + return ( + super().is_allowed(request, total) + and request.event.currency == "CHF" + and total <= Decimal("5000.00") + ) def _payment_intent_kwargs(self, request, payment): return { @@ -1865,13 +2290,13 @@ class StripeTwint(StripeRedirectMethod): class StripeMobilePay(StripeRedirectMethod): - identifier = 'stripe_mobilepay' - verbose_name = 'MobilePay via Stripe' - public_name = 'MobilePay' - method = 'mobilepay' - confirmation_method = 'automatic' + identifier = "stripe_mobilepay" + verbose_name = "MobilePay via Stripe" + public_name = "MobilePay" + method = "mobilepay" + confirmation_method = "automatic" explanation = _( - 'This payment method is available to MobilePay app users in Denmark and Finland. Please have your app ready.' + "This payment method is available to MobilePay app users in Denmark and Finland. Please have your app ready." ) def _payment_intent_kwargs(self, request, payment): diff --git a/src/pretix/plugins/stripe/tasks.py b/src/pretix/plugins/stripe/tasks.py index cbf04fd35..497c297bf 100644 --- a/src/pretix/plugins/stripe/tasks.py +++ b/src/pretix/plugins/stripe/tasks.py @@ -29,6 +29,7 @@ from pretix.base.services.tasks import EventTask from pretix.celery_app import app from pretix.multidomain.urlreverse import get_event_domain from pretix.plugins.stripe.models import RegisteredApplePayDomain +from pretix.plugins.stripe.utils import get_stripe_client logger = logging.getLogger(__name__) @@ -51,6 +52,7 @@ def get_stripe_account_key(prov): @app.task(base=EventTask, max_retries=5, default_retry_delay=1) def stripe_verify_domain(event, domain): from pretix.plugins.stripe.payment import StripeCC + prov = StripeCC(event) account = get_stripe_account_key(prov) @@ -59,29 +61,22 @@ def stripe_verify_domain(event, domain): # we're building our api_kwargs here by hand. # Only if no live connect secret key is set, we'll fall back to the testmode keys. # But this should never happen except in scenarios where pretix runs in devmode. - if prov.settings.connect_client_id and prov.settings.connect_user_id: - api_kwargs = { - 'api_key': prov.settings.connect_secret_key or prov.settings.connect_test_secret_key, - 'stripe_account': prov.settings.connect_user_id - } - else: - api_kwargs = { - 'api_key': prov.settings.secret_key, - } + stripe_client = get_stripe_client( + prov.settings.connect_secret_key or prov.settings.connect_test_secret_key + ) if RegisteredApplePayDomain.objects.filter(account=account, domain=domain).exists(): return try: - resp = stripe.ApplePayDomain.create( - domain_name=domain, - **api_kwargs + resp = stripe_client.apple_pay_domains.create( + params={ + "domain_name": domain, + }, + options=prov.api_options, ) except stripe.error.StripeError: - logger.exception('Could not verify domain with Stripe') + logger.exception("Could not verify domain with Stripe") else: if resp.livemode: - RegisteredApplePayDomain.objects.create( - domain=domain, - account=account - ) + RegisteredApplePayDomain.objects.create(domain=domain, account=account) diff --git a/src/pretix/plugins/stripe/utils.py b/src/pretix/plugins/stripe/utils.py index 9fd5bdc50..f0a3d7acd 100644 --- a/src/pretix/plugins/stripe/utils.py +++ b/src/pretix/plugins/stripe/utils.py @@ -19,3 +19,18 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # +import stripe +from stripe import StripeClient + +from pretix import __version__ + + +def get_stripe_client(api_key): + stripe.set_app_info( + "pretix", + partner_id="pp_partner_FSaz4PpKIur7Ox", + version=__version__, + url="https://pretix.eu", + ) + stripe.enable_telemetry = False + return StripeClient(api_key) diff --git a/src/pretix/plugins/stripe/views.py b/src/pretix/plugins/stripe/views.py index 628323dac..3151024b0 100644 --- a/src/pretix/plugins/stripe/views.py +++ b/src/pretix/plugins/stripe/views.py @@ -59,7 +59,8 @@ from pretix.base.payment import PaymentException from pretix.base.services.locking import LockTimeoutException from pretix.base.settings import GlobalSettingsObject from pretix.control.permissions import ( - AdministratorPermissionRequiredMixin, event_permission_required, + AdministratorPermissionRequiredMixin, + event_permission_required, ) from pretix.control.views.event import DecoupleMixin from pretix.control.views.organizer import OrganizerDetailViewMixin @@ -70,181 +71,253 @@ from pretix.plugins.stripe.forms import OrganizerStripeSettingsForm from pretix.plugins.stripe.models import ReferencedStripeObject from pretix.plugins.stripe.payment import StripeCC, StripeSettingsHolder from pretix.plugins.stripe.tasks import ( - get_domain_for_event, stripe_verify_domain, + get_domain_for_event, + stripe_verify_domain, ) +from pretix.plugins.stripe.utils import get_stripe_client -logger = logging.getLogger('pretix.plugins.stripe') +logger = logging.getLogger("pretix.plugins.stripe") @xframe_options_exempt def redirect_view(request, *args, **kwargs): try: - data = signing.loads(request.GET.get('data', ''), salt='safe-redirect') + data = signing.loads(request.GET.get("data", ""), salt="safe-redirect") except signing.BadSignature: - return HttpResponseBadRequest('Invalid parameter') + return HttpResponseBadRequest("Invalid parameter") - if 'go' in request.GET: - if 'session' in data: - for k, v in data['session'].items(): + if "go" in request.GET: + if "session" in data: + for k, v in data["session"].items(): request.session[k] = v - return redirect(data['url']) + return redirect(data["url"]) else: params = request.GET.copy() - params['go'] = '1' - r = render(request, 'pretixplugins/stripe/redirect.html', { - 'url': build_absolute_uri(request.event, 'plugins:stripe:redirect') + '?' + urllib.parse.urlencode(params), - }) + params["go"] = "1" + r = render( + request, + "pretixplugins/stripe/redirect.html", + { + "url": build_absolute_uri(request.event, "plugins:stripe:redirect") + + "?" + + urllib.parse.urlencode(params), + }, + ) r._csp_ignore = True return r @scopes_disabled() def oauth_return(request, *args, **kwargs): - if 'payment_stripe_oauth_event' not in request.session: - messages.error(request, _('An error occurred during connecting with Stripe, please try again.')) - return redirect('control:index') + if "payment_stripe_oauth_event" not in request.session: + messages.error( + request, + _("An error occurred during connecting with Stripe, please try again."), + ) + return redirect("control:index") - event = get_object_or_404(Event, pk=request.session['payment_stripe_oauth_event']) + event = get_object_or_404(Event, pk=request.session["payment_stripe_oauth_event"]) - if request.GET.get('state') != request.session['payment_stripe_oauth_token']: - messages.error(request, _('An error occurred during connecting with Stripe, please try again.')) - return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={ - 'organizer': event.organizer.slug, - 'event': event.slug, - 'provider': 'stripe_settings' - })) + if request.GET.get("state") != request.session["payment_stripe_oauth_token"]: + messages.error( + request, + _("An error occurred during connecting with Stripe, please try again."), + ) + return redirect_to_url( + reverse( + "control:event.settings.payment.provider", + kwargs={ + "organizer": event.organizer.slug, + "event": event.slug, + "provider": "stripe_settings", + }, + ) + ) gs = GlobalSettingsObject() testdata = {} + stripe_client = get_stripe_client( + gs.settings.payment_stripe_connect_secret_key + or gs.settings.payment_stripe_connect_test_secret_key + ) try: - resp = requests.post('https://connect.stripe.com/oauth/token', data={ - 'grant_type': 'authorization_code', - 'client_secret': ( - gs.settings.payment_stripe_connect_secret_key or gs.settings.payment_stripe_connect_test_secret_key - ), - 'code': request.GET.get('code') - }) + resp = requests.post( + "https://connect.stripe.com/oauth/token", + data={ + "grant_type": "authorization_code", + "client_secret": ( + gs.settings.payment_stripe_connect_secret_key + or gs.settings.payment_stripe_connect_test_secret_key + ), + "code": request.GET.get("code"), + }, + ) data = resp.json() - if 'error' not in data: - account = stripe.Account.retrieve( - data['stripe_user_id'], - api_key=gs.settings.payment_stripe_connect_secret_key or gs.settings.payment_stripe_connect_test_secret_key + if "error" not in data: + account = stripe_client.accounts.retrieve( + data["stripe_user_id"], ) except: - logger.exception('Failed to obtain OAuth token') - messages.error(request, _('An error occurred during connecting with Stripe, please try again.')) + logger.exception("Failed to obtain OAuth token") + messages.error( + request, + _("An error occurred during connecting with Stripe, please try again."), + ) else: - if 'error' not in data and data['livemode']: + if "error" not in data and data["livemode"]: try: - testresp = requests.post('https://connect.stripe.com/oauth/token', data={ - 'grant_type': 'refresh_token', - 'client_secret': gs.settings.payment_stripe_connect_test_secret_key, - 'refresh_token': data['refresh_token'] - }) + testresp = requests.post( + "https://connect.stripe.com/oauth/token", + data={ + "grant_type": "refresh_token", + "client_secret": gs.settings.payment_stripe_connect_test_secret_key, + "refresh_token": data["refresh_token"], + }, + ) testdata = testresp.json() except: - logger.exception('Failed to obtain OAuth token') - messages.error(request, _('An error occurred during connecting with Stripe, please try again.')) - return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={ - 'organizer': event.organizer.slug, - 'event': event.slug, - 'provider': 'stripe_settings' - })) - - if 'error' in data: - messages.error(request, _('Stripe returned an error: {}').format(data['error_description'])) - elif data['livemode'] and 'error' in testdata: - messages.error(request, _('Stripe returned an error: {}').format(testdata['error_description'])) - else: - messages.success(request, - _('Your Stripe account is now connected to pretix. You can change the settings in ' - 'detail below.')) - event.settings.payment_stripe_publishable_key = data['stripe_publishable_key'] - # event.settings.payment_stripe_connect_access_token = data['access_token'] we don't need it, right? - event.settings.payment_stripe_connect_refresh_token = data['refresh_token'] - event.settings.payment_stripe_connect_user_id = data['stripe_user_id'] - event.settings.payment_stripe_merchant_country = account.get('country') - if ( - account.get('business_profile', {}).get('name') - or account.get('settings', {}).get('dashboard', {}).get('display_name') - or account.get('email') - ): - event.settings.payment_stripe_connect_user_name = ( - account.get('business_profile', {}).get('name') - or account.get('settings', {}).get('dashboard', {}).get('display_name') - or account.get('email') + logger.exception("Failed to obtain OAuth token") + messages.error( + request, + _( + "An error occurred during connecting with Stripe, please try again." + ), + ) + return redirect_to_url( + reverse( + "control:event.settings.payment.provider", + kwargs={ + "organizer": event.organizer.slug, + "event": event.slug, + "provider": "stripe_settings", + }, + ) ) - if data['livemode']: - event.settings.payment_stripe_publishable_test_key = testdata['stripe_publishable_key'] + if "error" in data: + messages.error( + request, + _("Stripe returned an error: {}").format(data["error_description"]), + ) + elif data["livemode"] and "error" in testdata: + messages.error( + request, + _("Stripe returned an error: {}").format(testdata["error_description"]), + ) + else: + messages.success( + request, + _( + "Your Stripe account is now connected to pretix. You can change the settings in " + "detail below." + ), + ) + event.settings.payment_stripe_publishable_key = data[ + "stripe_publishable_key" + ] + # event.settings.payment_stripe_connect_access_token = data['access_token'] we don't need it, right? + event.settings.payment_stripe_connect_refresh_token = data["refresh_token"] + event.settings.payment_stripe_connect_user_id = data["stripe_user_id"] + event.settings.payment_stripe_merchant_country = account.get("country") + if ( + account.get("business_profile", {}).get("name") + or account.get("settings", {}).get("dashboard", {}).get("display_name") + or account.get("email") + ): + event.settings.payment_stripe_connect_user_name = ( + account.get("business_profile", {}).get("name") + or account.get("settings", {}) + .get("dashboard", {}) + .get("display_name") + or account.get("email") + ) + + if data["livemode"]: + event.settings.payment_stripe_publishable_test_key = testdata[ + "stripe_publishable_key" + ] else: - event.settings.payment_stripe_publishable_test_key = event.settings.payment_stripe_publishable_key + event.settings.payment_stripe_publishable_test_key = ( + event.settings.payment_stripe_publishable_key + ) - if request.session.get('payment_stripe_oauth_enable', False): + if request.session.get("payment_stripe_oauth_enable", False): event.settings.payment_stripe__enabled = True - del request.session['payment_stripe_oauth_enable'] + del request.session["payment_stripe_oauth_enable"] - stripe_verify_domain.apply_async(args=(event.pk, get_domain_for_event(event))) + stripe_verify_domain.apply_async( + args=(event.pk, get_domain_for_event(event)) + ) - return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={ - 'organizer': event.organizer.slug, - 'event': event.slug, - 'provider': 'stripe_settings' - })) + return redirect_to_url( + reverse( + "control:event.settings.payment.provider", + kwargs={ + "organizer": event.organizer.slug, + "event": event.slug, + "provider": "stripe_settings", + }, + ) + ) @csrf_exempt @require_POST @scopes_disabled() def webhook(request, *args, **kwargs): - event_json = json.loads(request.body.decode('utf-8')) + event_json = json.loads(request.body.decode("utf-8")) # We do not check for the event type as we are not interested in the event it self, # we just use it as a trigger to look the charge up to be absolutely sure. # Another reason for this is that stripe events are not authenticated, so they could # come from anywhere. - if event_json['data']['object']['object'] == "charge": + if event_json["data"]["object"]["object"] == "charge": func = charge_webhook - objid = event_json['data']['object']['id'] + objid = event_json["data"]["object"]["id"] lookup_ids = [ objid, - (event_json['data']['object'].get('source') or {}).get('id') + (event_json["data"]["object"].get("source") or {}).get("id"), ] - elif event_json['data']['object']['object'] == "dispute": + elif event_json["data"]["object"]["object"] == "dispute": func = charge_webhook - objid = event_json['data']['object']['charge'] + objid = event_json["data"]["object"]["charge"] lookup_ids = [objid] - elif event_json['data']['object']['object'] == "source": + elif event_json["data"]["object"]["object"] == "source": func = source_webhook - objid = event_json['data']['object']['id'] + objid = event_json["data"]["object"]["id"] lookup_ids = [objid] - elif event_json['data']['object']['object'] == "payment_intent": + elif event_json["data"]["object"]["object"] == "payment_intent": func = paymentintent_webhook - objid = event_json['data']['object']['id'] + objid = event_json["data"]["object"]["id"] lookup_ids = [objid] else: return HttpResponse("Not interested in this data type", status=200) - rso = ReferencedStripeObject.objects.select_related('order', 'order__event').filter( - reference__in=[lid for lid in lookup_ids if lid] - ).first() + rso = ( + ReferencedStripeObject.objects.select_related("order", "order__event") + .filter(reference__in=[lid for lid in lookup_ids if lid]) + .first() + ) if rso: return func(rso.order.event, event_json, objid, rso) else: - if event_json['data']['object']['object'] == "charge" and 'payment_intent' in event_json['data']['object']: + if ( + event_json["data"]["object"]["object"] == "charge" + and "payment_intent" in event_json["data"]["object"] + ): # If we receive a charge webhook *before* the payment intent webhook, we don't know the charge ID yet # and can't match it -- but we know the payment intent ID! try: - rso = ReferencedStripeObject.objects.select_related('order', 'order__event').get( - reference=event_json['data']['object']['payment_intent'] - ) + rso = ReferencedStripeObject.objects.select_related( + "order", "order__event" + ).get(reference=event_json["data"]["object"]["payment_intent"]) return func(rso.order.event, event_json, objid, rso) except ReferencedStripeObject.DoesNotExist: return HttpResponse("Unable to detect event", status=200) - elif hasattr(request, 'event') and func != paymentintent_webhook: + elif hasattr(request, "event") and func != paymentintent_webhook: # This is a legacy integration from back when didn't have ReferencedStripeObject. This can't happen for # payment intents or charges connected with payment intents since they didn't exist back then. Our best # hope is to go for request.event and see if we can find the order ID. @@ -256,14 +329,14 @@ def webhook(request, *args, **kwargs): SOURCE_TYPES = { - 'sofort': 'stripe_sofort', - 'three_d_secure': 'stripe', - 'card': 'stripe', - 'sepa_debit': 'stripe_sepa_debit', - 'giropay': 'stripe_giropay', - 'ideal': 'stripe_ideal', - 'alipay': 'stripe_alipay', - 'bancontact': 'stripe_bancontact', + "sofort": "stripe_sofort", + "three_d_secure": "stripe", + "card": "stripe", + "sepa_debit": "stripe_sepa_debit", + "giropay": "stripe_giropay", + "ideal": "stripe_ideal", + "alipay": "stripe_alipay", + "bancontact": "stripe_bancontact", } @@ -272,21 +345,28 @@ def charge_webhook(event, event_json, charge_id, rso): prov._init_api() try: - charge = stripe.Charge.retrieve( + charge = prov.stripe_client.charges.retrieve( charge_id, - expand=['dispute', 'refunds', 'payment_intent', 'payment_intent.latest_charge'], - **prov.api_kwargs + params={ + "expand": [ + "dispute", + "refunds", + "payment_intent", + "payment_intent.latest_charge", + ], + }, + options=prov.api_options, ) except stripe.error.StripeError: - logger.exception('Stripe error on webhook. Event data: %s' % str(event_json)) - return HttpResponse('Charge not found', status=500) + logger.exception("Stripe error on webhook. Event data: %s" % str(event_json)) + return HttpResponse("Charge not found", status=500) - metadata = charge['metadata'] - if 'event' not in metadata: - return HttpResponse('Event not given in charge metadata', status=200) + metadata = charge["metadata"] + if "event" not in metadata: + return HttpResponse("Event not given in charge metadata", status=200) - if int(metadata['event']) != event.pk: - return HttpResponse('Not interested in this event', status=200) + if int(metadata["event"]) != event.pk: + return HttpResponse("Not interested in this event", status=200) if rso and rso.payment: order = rso.payment.order @@ -296,25 +376,36 @@ def charge_webhook(event, event_json, charge_id, rso): payment = None else: try: - order = event.orders.get(id=metadata['order']) + order = event.orders.get(id=metadata["order"]) except Order.DoesNotExist: - return HttpResponse('Order not found', status=200) + return HttpResponse("Order not found", status=200) payment = None with transaction.atomic(): if payment: - payment = OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=payment.pk) + payment = OrderPayment.objects.select_for_update(of=OF_SELF).get( + pk=payment.pk + ) else: - payment = order.payments.filter( - info__icontains=charge['id'], - provider__startswith='stripe', - amount=prov._amount_to_decimal(charge['amount']), - ).select_for_update(of=OF_SELF).last() + payment = ( + order.payments.filter( + info__icontains=charge["id"], + provider__startswith="stripe", + amount=prov._amount_to_decimal(charge["amount"]), + ) + .select_for_update(of=OF_SELF) + .last() + ) if not payment: payment = order.payments.create( state=OrderPayment.PAYMENT_STATE_CREATED, - provider=SOURCE_TYPES.get(charge['source'].get('type', charge['source'].get('object', 'card')), 'stripe'), - amount=prov._amount_to_decimal(charge['amount']), + provider=SOURCE_TYPES.get( + charge["source"].get( + "type", charge["source"].get("object", "card") + ), + "stripe", + ), + amount=prov._amount_to_decimal(charge["amount"]), info=str(charge), ) @@ -322,40 +413,47 @@ def charge_webhook(event, event_json, charge_id, rso): prov = payment.payment_provider prov._init_api() - order.log_action('pretix.plugins.stripe.event', data=event_json) + order.log_action("pretix.plugins.stripe.event", data=event_json) - is_refund = charge['amount_refunded'] or 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')] - for r in charge['refunds']['data']: - a = prov._amount_to_decimal(r['amount']) - if r['status'] in ('failed', 'canceled'): + known_refunds = [r.info_data.get("id") for r in payment.refunds.all()] + migrated_refund_amounts = [ + r.amount for r in payment.refunds.all() if not r.info_data.get("id") + ] + for r in charge["refunds"]["data"]: + a = prov._amount_to_decimal(r["amount"]) + if r["status"] in ("failed", "canceled"): continue if a in migrated_refund_amounts: migrated_refund_amounts.remove(a) continue - if r['id'] not in known_refunds: - payment.create_external_refund( - amount=a, - info=str(r) - ) - if charge['dispute']: - if charge['dispute']['status'] != 'won' and charge['dispute']['id'] not in known_refunds: - a = prov._amount_to_decimal(charge['dispute']['amount']) + if r["id"] not in known_refunds: + payment.create_external_refund(amount=a, info=str(r)) + if charge["dispute"]: + if ( + charge["dispute"]["status"] != "won" + and charge["dispute"]["id"] not in known_refunds + ): + a = prov._amount_to_decimal(charge["dispute"]["amount"]) if a in migrated_refund_amounts: migrated_refund_amounts.remove(a) else: payment.create_external_refund( - amount=a, - info=str(charge['dispute']) + amount=a, info=str(charge["dispute"]) ) - elif charge['status'] == 'succeeded' and payment.state in (OrderPayment.PAYMENT_STATE_PENDING, - OrderPayment.PAYMENT_STATE_CREATED, - OrderPayment.PAYMENT_STATE_CANCELED, - OrderPayment.PAYMENT_STATE_FAILED): + elif charge["status"] == "succeeded" and payment.state in ( + OrderPayment.PAYMENT_STATE_PENDING, + OrderPayment.PAYMENT_STATE_CREATED, + OrderPayment.PAYMENT_STATE_CANCELED, + OrderPayment.PAYMENT_STATE_FAILED, + ): try: if getattr(charge, "payment_intent", None): payment.info = str(charge.payment_intent) @@ -364,7 +462,10 @@ def charge_webhook(event, event_json, charge_id, rso): 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): + 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) @@ -374,17 +475,17 @@ def source_webhook(event, event_json, source_id, rso): prov = StripeCC(event) prov._init_api() try: - src = stripe.Source.retrieve(source_id, **prov.api_kwargs) + src = prov.stripe_client.sources.retrieve(source_id, options=prov.api_options) except stripe.error.StripeError: - logger.exception('Stripe error on webhook. Event data: %s' % str(event_json)) - return HttpResponse('Charge not found', status=500) + logger.exception("Stripe error on webhook. Event data: %s" % str(event_json)) + return HttpResponse("Charge not found", status=500) - metadata = src['metadata'] - if 'event' not in metadata: - return HttpResponse('Event not given in charge metadata', status=200) + metadata = src["metadata"] + if "event" not in metadata: + return HttpResponse("Event not given in charge metadata", status=200) - if int(metadata['event']) != event.pk: - return HttpResponse('Not interested in this event', status=200) + if int(metadata["event"]) != event.pk: + return HttpResponse("Not interested in this event", status=200) with transaction.atomic(): if rso and rso.payment: @@ -395,24 +496,38 @@ def source_webhook(event, event_json, source_id, rso): payment = None else: try: - order = event.orders.get(id=metadata['order']) + order = event.orders.get(id=metadata["order"]) except Order.DoesNotExist: - return HttpResponse('Order not found', status=200) + return HttpResponse("Order not found", status=200) payment = None if payment: - payment = OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=payment.pk) + payment = OrderPayment.objects.select_for_update(of=OF_SELF).get( + pk=payment.pk + ) else: - payment = order.payments.filter( - info__icontains=src['id'], - provider__startswith='stripe', - amount=prov._amount_to_decimal(src['amount']) if src['amount'] is not None else order.total, - ).select_for_update(of=OF_SELF).last() + payment = ( + order.payments.filter( + info__icontains=src["id"], + provider__startswith="stripe", + amount=( + prov._amount_to_decimal(src["amount"]) + if src["amount"] is not None + else order.total + ), + ) + .select_for_update(of=OF_SELF) + .last() + ) if not payment: payment = order.payments.create( state=OrderPayment.PAYMENT_STATE_CREATED, - provider=SOURCE_TYPES.get(src['type'], 'stripe'), - amount=prov._amount_to_decimal(src['amount']) if src['amount'] is not None else order.total, + provider=SOURCE_TYPES.get(src["type"], "stripe"), + amount=( + prov._amount_to_decimal(src["amount"]) + if src["amount"] is not None + else order.total + ), info=str(src), ) @@ -420,18 +535,24 @@ def source_webhook(event, event_json, source_id, rso): prov = payment.payment_provider prov._init_api() - order.log_action('pretix.plugins.stripe.event', data=event_json) - go = (event_json['type'] == 'source.chargeable' and - payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED) and - src.status == 'chargeable') + order.log_action("pretix.plugins.stripe.event", data=event_json) + go = ( + event_json["type"] == "source.chargeable" + and payment.state + in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED) + and src.status == "chargeable" + ) if go: try: prov._charge_source(None, source_id, payment) except PaymentException: - logger.exception('Webhook error') - elif src.status == 'failed': + logger.exception("Webhook error") + elif src.status == "failed": payment.fail(info=str(src)) - elif src.status == 'canceled' and payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED): + elif src.status == "canceled" and payment.state in ( + OrderPayment.PAYMENT_STATE_PENDING, + OrderPayment.PAYMENT_STATE_CREATED, + ): payment.info = str(src) payment.state = OrderPayment.PAYMENT_STATE_CANCELED payment.save() @@ -444,15 +565,21 @@ def paymentintent_webhook(event, event_json, paymentintent_id, rso): prov._init_api() try: - paymentintent = stripe.PaymentIntent.retrieve(paymentintent_id, expand=["latest_charge"], **prov.api_kwargs) + paymentintent = prov.stripe_client.payment_intents.retrieve( + paymentintent_id, + params={ + "expand": ["latest_charge"], + }, + options=prov.api_options, + ) except stripe.error.StripeError: - logger.exception('Stripe error on webhook. Event data: %s' % str(event_json)) - return HttpResponse('Charge not found', status=500) + logger.exception("Stripe error on webhook. Event data: %s" % str(event_json)) + return HttpResponse("Charge not found", status=500) if paymentintent.latest_charge: ReferencedStripeObject.objects.get_or_create( reference=paymentintent.latest_charge.id, - defaults={'order': rso.payment.order, 'payment': rso.payment} + defaults={"order": rso.payment.order, "payment": rso.payment}, ) if event_json["type"] == "payment_intent.payment_failed": @@ -461,10 +588,10 @@ def paymentintent_webhook(event, event_json, paymentintent_id, rso): return HttpResponse(status=200) -@event_permission_required('can_change_event_settings') +@event_permission_required("can_change_event_settings") def oauth_disconnect(request, **kwargs): if request.method != "POST": - return render(request, 'pretixplugins/stripe/oauth_disconnect.html', {}) + return render(request, "pretixplugins/stripe/oauth_disconnect.html", {}) del request.event.settings.payment_stripe_publishable_key del request.event.settings.payment_stripe_publishable_test_key @@ -473,27 +600,34 @@ def oauth_disconnect(request, **kwargs): del request.event.settings.payment_stripe_connect_user_id del request.event.settings.payment_stripe_connect_user_name request.event.settings.payment_stripe__enabled = False - messages.success(request, _('Your Stripe account has been disconnected.')) + messages.success(request, _("Your Stripe account has been disconnected.")) - return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={ - 'organizer': request.event.organizer.slug, - 'event': request.event.slug, - 'provider': 'stripe_settings' - })) + return redirect_to_url( + reverse( + "control:event.settings.payment.provider", + kwargs={ + "organizer": request.event.organizer.slug, + "event": request.event.slug, + "provider": "stripe_settings", + }, + ) + ) class StripeOrderView: def dispatch(self, request, *args, **kwargs): try: self.order = request.event.orders.get_with_secret_check( - code=kwargs['order'], received_secret=kwargs['hash'].lower(), tag='plugins:stripe' + code=kwargs["order"], + received_secret=kwargs["hash"].lower(), + tag="plugins:stripe", ) except Order.DoesNotExist: - raise Http404('Unknown order') + raise Http404("Unknown order") self.payment = get_object_or_404( self.order.payments, - pk=self.kwargs['payment'], - provider__startswith='stripe' + pk=self.kwargs["payment"], + provider__startswith="stripe", ) return super().dispatch(request, *args, **kwargs) @@ -502,119 +636,181 @@ class StripeOrderView: return self.request.event.get_payment_providers()[self.payment.provider] def _redirect_to_order(self): - if self.request.session.get('payment_stripe_order_secret') != self.order.secret and not self.payment.provider.startswith('stripe'): - messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link ' - 'in your emails to continue.')) - return redirect_to_url(eventreverse(self.request.event, 'presale:event.index')) + if self.request.session.get( + "payment_stripe_order_secret" + ) != self.order.secret and not self.payment.provider.startswith("stripe"): + messages.error( + self.request, + _( + "Sorry, there was an error in the payment process. Please check the link " + "in your emails to continue." + ), + ) + return redirect_to_url( + eventreverse(self.request.event, "presale:event.index") + ) - return redirect_to_url(eventreverse(self.request.event, 'presale:event.order', kwargs={ - 'order': self.order.code, - 'secret': self.order.secret - }) + ('?paid=yes' if self.order.status == Order.STATUS_PAID else '')) + return redirect_to_url( + eventreverse( + self.request.event, + "presale:event.order", + kwargs={"order": self.order.code, "secret": self.order.secret}, + ) + + ("?paid=yes" if self.order.status == Order.STATUS_PAID else "") + ) -@method_decorator(xframe_options_exempt, 'dispatch') +@method_decorator(xframe_options_exempt, "dispatch") class ReturnView(StripeOrderView, View): def get(self, request, *args, **kwargs): prov = self.pprov prov._init_api() try: - src = stripe.Source.retrieve(request.GET.get('source'), **prov.api_kwargs) + src = prov.stripe_client.sources.retrieve( + request.GET.get("source"), options=prov.api_options + ) except stripe.error.InvalidRequestError: - logger.exception('Could not retrieve source') - messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link ' - 'in your emails to continue.')) - return redirect_to_url(eventreverse(self.request.event, 'presale:event.index')) + logger.exception("Could not retrieve source") + messages.error( + self.request, + _( + "Sorry, there was an error in the payment process. Please check the link " + "in your emails to continue." + ), + ) + return redirect_to_url( + eventreverse(self.request.event, "presale:event.index") + ) - if src.client_secret != request.GET.get('client_secret'): - messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link ' - 'in your emails to continue.')) - return redirect_to_url(eventreverse(self.request.event, 'presale:event.index')) + if src.client_secret != request.GET.get("client_secret"): + messages.error( + self.request, + _( + "Sorry, there was an error in the payment process. Please check the link " + "in your emails to continue." + ), + ) + return redirect_to_url( + eventreverse(self.request.event, "presale:event.index") + ) with transaction.atomic(): self.order.refresh_from_db() - self.payment = OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=self.payment.pk) + self.payment = OrderPayment.objects.select_for_update(of=OF_SELF).get( + pk=self.payment.pk + ) if self.payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED: - if 'payment_stripe_token' in request.session: - del request.session['payment_stripe_token'] + if "payment_stripe_token" in request.session: + del request.session["payment_stripe_token"] return self._redirect_to_order() - if src.status == 'chargeable': + if src.status == "chargeable": try: prov._charge_source(request, src.id, self.payment) except PaymentException as e: messages.error(request, str(e)) return self._redirect_to_order() finally: - if 'payment_stripe_token' in request.session: - del request.session['payment_stripe_token'] - elif src.status == 'consumed': + if "payment_stripe_token" in request.session: + del request.session["payment_stripe_token"] + elif src.status == "consumed": # Webhook was faster, wow! ;) - if 'payment_stripe_token' in request.session: - del request.session['payment_stripe_token'] + if "payment_stripe_token" in request.session: + del request.session["payment_stripe_token"] return self._redirect_to_order() - elif src.status == 'pending': + elif src.status == "pending": self.payment.state = OrderPayment.PAYMENT_STATE_PENDING self.payment.info = str(src) self.payment.save() else: # failed or canceled self.payment.fail(info=str(src)) - messages.error(self.request, _('We had trouble authorizing your card payment. Please try again and ' - 'get in touch with us if this problem persists.')) + messages.error( + self.request, + _( + "We had trouble authorizing your card payment. Please try again and " + "get in touch with us if this problem persists." + ), + ) return self._redirect_to_order() -@method_decorator(xframe_options_exempt, 'dispatch') +@method_decorator(xframe_options_exempt, "dispatch") class ScaView(StripeOrderView, View): def get(self, request, *args, **kwargs): prov = self.pprov prov._init_api() - if self.payment.state in (OrderPayment.PAYMENT_STATE_CONFIRMED, - OrderPayment.PAYMENT_STATE_CANCELED, - OrderPayment.PAYMENT_STATE_FAILED): + if self.payment.state in ( + OrderPayment.PAYMENT_STATE_CONFIRMED, + OrderPayment.PAYMENT_STATE_CANCELED, + OrderPayment.PAYMENT_STATE_FAILED, + ): return self._redirect_to_order() payment_info = json.loads(self.payment.info) - if 'id' in payment_info: + if "id" in payment_info: try: - intent = stripe.PaymentIntent.retrieve( - payment_info['id'], - expand=["latest_charge"], - **prov.api_kwargs + intent = prov.stripe_client.payment_intents.retrieve( + payment_info["id"], + params={ + "expand": ["latest_charge"], + }, + options=prov.api_options, ) except stripe.error.InvalidRequestError: - logger.exception('Could not retrieve payment intent') - messages.error(self.request, _('Sorry, there was an error in the payment process.')) + logger.exception("Could not retrieve payment intent") + messages.error( + self.request, _("Sorry, there was an error in the payment process.") + ) return self._redirect_to_order() else: - messages.error(self.request, _('Sorry, there was an error in the payment process.')) + 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', 'alipay_handle_redirect', 'wechat_pay_display_qr_code', - 'swish_handle_redirect_or_display_qr_code', 'multibanco_display_details', + 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", + "multibanco_display_details", ]: ctx = { - 'order': self.order, - 'stripe_settings': StripeSettingsHolder(self.order.event).settings, + "order": self.order, + "stripe_settings": StripeSettingsHolder(self.order.event).settings, } - 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'] - ctx['payment_intent_redirect_action_handling'] = prov.redirect_action_handling - 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' + 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"] + ) + ctx["payment_intent_redirect_action_handling"] = ( + prov.redirect_action_handling + ) + 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 = render(request, "pretixplugins/stripe/sca.html", ctx) r._csp_ignore = True return r else: @@ -626,7 +822,7 @@ class ScaView(StripeOrderView, View): return self._redirect_to_order() -@method_decorator(xframe_options_exempt, 'dispatch') +@method_decorator(xframe_options_exempt, "dispatch") class ScaReturnView(StripeOrderView, View): def get(self, request, *args, **kwargs): prov = self.pprov @@ -638,31 +834,40 @@ class ScaReturnView(StripeOrderView, View): self.order.refresh_from_db() ctx = { - 'order': self.order, - 'payment_intent_redirect_action_handling': prov.redirect_action_handling, - 'order_url': eventreverse(self.request.event, 'presale:event.order', kwargs={ - 'order': self.order.code, - 'secret': self.order.secret - }), + "order": self.order, + "payment_intent_redirect_action_handling": prov.redirect_action_handling, + "order_url": eventreverse( + self.request.event, + "presale:event.order", + kwargs={"order": self.order.code, "secret": self.order.secret}, + ), } - return render(request, 'pretixplugins/stripe/sca_return.html', ctx) + return render(request, "pretixplugins/stripe/sca_return.html", ctx) -class OrganizerSettingsFormView(DecoupleMixin, OrganizerDetailViewMixin, AdministratorPermissionRequiredMixin, FormView): +class OrganizerSettingsFormView( + DecoupleMixin, + OrganizerDetailViewMixin, + AdministratorPermissionRequiredMixin, + FormView, +): model = Organizer - permission = 'can_change_organizer_settings' + permission = "can_change_organizer_settings" form_class = OrganizerStripeSettingsForm - template_name = 'pretixplugins/stripe/organizer_stripe.html' + template_name = "pretixplugins/stripe/organizer_stripe.html" def get_success_url(self): - return reverse('plugins:stripe:settings.connect', kwargs={ - 'organizer': self.request.organizer.slug, - }) + return reverse( + "plugins:stripe:settings.connect", + kwargs={ + "organizer": self.request.organizer.slug, + }, + ) def get_form_kwargs(self): kwargs = super().get_form_kwargs() - kwargs['obj'] = self.request.organizer + kwargs["obj"] = self.request.organizer return kwargs @transaction.atomic @@ -672,12 +877,15 @@ class OrganizerSettingsFormView(DecoupleMixin, OrganizerDetailViewMixin, Adminis form.save() if form.has_changed(): self.request.organizer.log_action( - 'pretix.organizer.settings', user=self.request.user, data={ - k: form.cleaned_data.get(k) for k in form.changed_data - } + "pretix.organizer.settings", + user=self.request.user, + data={k: form.cleaned_data.get(k) for k in form.changed_data}, ) - messages.success(self.request, _('Your changes have been saved.')) + messages.success(self.request, _("Your changes have been saved.")) return redirect_to_url(self.get_success_url()) else: - messages.error(self.request, _('We could not save your changes. See below for details.')) + messages.error( + self.request, + _("We could not save your changes. See below for details."), + ) return self.get(request)