# # This file is part of pretix (Community Edition). # # Copyright (C) 2014-2020 Raphael Michel and contributors # Copyright (C) 2020-2021 rami.io GmbH and contributors # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General # Public License as published by the Free Software Foundation in version 3 of the License. # # ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are # applicable granting you additional permissions and placing additional restrictions on your usage of this software. # Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive # this file, see . # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # # This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of # the Apache License 2.0 can be obtained at . # # This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A # full history of changes and contributors is available at . # # This file contains Apache-licensed contributions copyrighted by: FlaviaBastos, Jakob Schnell, Tobias Kunze, luto # # Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under the License. import hashlib import json import logging import re import urllib.parse from collections import OrderedDict from decimal import Decimal from json import JSONDecodeError import stripe from django import forms from django.conf import settings from django.contrib import messages from django.core import signing from django.db import transaction from django.http import HttpRequest from django.template.loader import get_template from django.urls import reverse from django.utils.crypto import get_random_string from django.utils.safestring import mark_safe from django.utils.timezone import now 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, ) from pretix.base.models import ( Event, InvoiceAddress, Order, OrderPayment, OrderRefund, Quota, ) from pretix.base.payment import ( BasePaymentProvider, PaymentException, WalletQueries, ) from pretix.base.plugins import get_all_plugins from pretix.base.services.mail import SendMailException from pretix.base.settings import SettingsSandbox from pretix.helpers import OF_SELF from pretix.helpers.countries import CachedCountries from pretix.helpers.http import get_client_ip from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.multidomain.urlreverse import build_absolute_uri from pretix.plugins.stripe.forms import StripeKeyValidator from pretix.plugins.stripe.models import ( ReferencedStripeObject, RegisteredApplePayDomain, ) from pretix.plugins.stripe.tasks import ( get_stripe_account_key, stripe_verify_domain, ) from pretix.presale.views.cart import cart_session logger = logging.getLogger('pretix.plugins.stripe') # State of the payment methods # # Source: https://stripe.com/docs/payments/payment-methods/overview # Last Update: 2023-12-20 # # Cards # - Credit and Debit Cards: ✓ # - Apple, Google Pay: ✓ # # Bank debits # - ACH Debit: ✗ # - Canadian PADs: ✗ # - BACS Direct Debit: ✗ # - SEPA Direct Debit: ✓ # - BECS Direct Debit: ✗ # # Bank redirects # - Bancontact: ✓ # - BLIK: ✗ # - EPS: ✓ # - giropay: ✓ # - iDEAL: ✓ # - P24: ✓ # - Sofort: ✓ # - FPX: ✗ # - PayNow: ✗ # - UPI: ✗ # - Netbanking: ✗ # # Bank transfers # - ACH Bank Transfer: ✗ # - SEPA Bank Transfer: ✗ # - UK Bank Transfer: ✗ # - Multibanco: ✗ # - Furikomi (Japan): ✗ # - Mexico Bank Transfer: ✗ # # Buy now, pay later # - Affirm: ✓ # - Afterpay/Clearpay: ✗ # - Klarna: ✓ # - Zip: ✗ # # Real-time payments # - Swish: ✗ # - PayNow: ✗ # - PromptPay: ✗ # - Pix: ✗ # # Vouchers # - Konbini: ✗ # - OXXO: ✗ # - Boleto: ✗ # # Wallets # - Apple Pay: ✓ (Cards) # - Google Pay: ✓ (Cards) # - Secure Remote Commerce: ✗ # - Link: ✓ (PaymentRequestButton) # - Cash App Pay: ✗ # - PayPal: ✓ (No settings UI yet) # - MobilePay: ✗ # - Alipay: ✓ # - WeChat Pay: ✓ # - GrabPay: ✓ class StripeSettingsHolder(BasePaymentProvider): 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) 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) 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')), ) def settings_content_render(self, request): if self.settings.connect_client_id and not self.settings.secret_key: # 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.'), self.get_connect_url(request), _('Connect with Stripe') ) else: 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') ) @property def settings_form_fields(self): 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, )) ] else: moto_settings = [] if self.settings.connect_client_id and not self.settings.secret_key: # 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.') )), ] else: return {} else: allcountries = list(countries) 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.'), )), ] 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, )), ] d = OrderedDict( fields + [ ('method_card', forms.BooleanField( label=_('Credit card payments'), required=False, )), ('method_giropay', forms.BooleanField( label=_('giropay'), 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_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_sofort', forms.BooleanField( label=_('SOFORT'), disabled=self.event.currency != 'EUR', help_text=( _('Stripe is in the process of removing this payment method. If you created your Stripe ' 'account after November 2023, you cannot use this payment method.') + '
%s
' % _( 'Despite the name, Sofort 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, )), ('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_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( # label=_('PayPal'), # disabled=self.event.currency not in [ # 'EUR', 'GBP', 'USD', 'CHF', 'CZK', 'DKK', 'NOK', 'PLN', 'SEK', 'AUD', 'CAD', 'HKD', 'NZD', 'SGD' # ] # )), ] + 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.move_to_end('_enabled', last=False) return d class StripeMethod(BasePaymentProvider): identifier = '' method = '' redirect_action_handling = 'iframe' # or redirect redirect_in_widget_allowed = True confirmation_method = 'manual' explanation = '' def __init__(self, event: Event): super().__init__(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 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( args='href="https://stripe.com/docs/testing#cards" target="_blank"' ) ) return None @property def settings_form_fields(self): return {} @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) def payment_refund_supported(self, payment: OrderPayment) -> bool: return True def payment_partial_refund_supported(self, payment: OrderPayment) -> bool: return True def payment_prepare(self, request, payment): return self.checkout_prepare(request, None) 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) def _decimal_to_int(self, amount): places = settings.CURRENCY_PLACES.get(self.event.currency, 2) 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_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)) if fee: d['application_fee_amount'] = self._decimal_to_int(fee) if 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( code=payment.order.code, 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( event=self.event.slug.upper(), code=payment.order.code, 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 } else: kwargs = { 'api_key': self.settings.secret_key, } return kwargs 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" ) def checkout_confirm_render(self, request, **kwargs) -> str: template = get_template('pretixplugins/stripe/checkout_payment_confirm.html') ctx = {'request': request, 'event': self.event, 'settings': self.settings, 'provider': self} return template.render(ctx) def payment_pending_render(self, request, payment) -> str: if payment.info: payment_info = json.loads(payment.info) else: payment_info = None template = get_template('pretixplugins/stripe/pending.html') ctx = { 'request': request, 'event': self.event, 'settings': self.settings, 'provider': self, 'order': payment.order, 'payment': payment, 'payment_info': payment_info, 'payment_hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest() } return template.render(ctx) def matching_id(self, payment: OrderPayment): return payment.info_data.get("id", None) def refund_matching_id(self, refund: OrderRefund): return refund.info_data.get('id', None) def api_payment_details(self, payment: OrderPayment): return { "id": payment.info_data.get("id", None), "payment_method": payment.info_data.get("payment_method", None) } def api_refund_details(self, refund: OrderRefund): try: return { "id": refund.info_data.get("id", None), } except JSONDecodeError: return {} def payment_control_render(self, request, payment) -> str: details = {} if payment.info: payment_info = json.loads(payment.info) if 'amount' in payment_info: payment_info['amount'] /= 10 ** settings.CURRENCY_PLACES.get(self.event.currency, 2) if isinstance(payment_info.get("latest_charge"), dict): details = payment_info["latest_charge"].get("payment_method_details", {}) elif payment_info.get("charges") and payment_info["charges"]["data"]: details = payment_info["charges"]["data"][0].get("payment_method_details", {}) elif payment_info.get("source"): details = payment_info["source"] else: payment_info = None details.setdefault('owner', {}) template = get_template('pretixplugins/stripe/control.html') ctx = { 'request': request, 'event': self.event, 'settings': self.settings, 'payment_info': payment_info, 'payment': payment, 'method': self.method, 'details': details, 'provider': self, } return template.render(ctx) def redirect(self, request, url): if request.session.get('iframe_session', False): return ( build_absolute_uri(request.event, 'plugins:stripe:redirect') + '?data=' + signing.dumps({ 'url': url, 'session': { 'payment_stripe_order_secret': request.session['payment_stripe_order_secret'], }, }, salt='safe-redirect') ) else: return str(url) @transaction.atomic() def execute_refund(self, refund: OrderRefund): self._init_api() payment_info = refund.payment.info_data OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=refund.payment.pk) if not payment_info: raise PaymentException(_('No payment information found.')) try: if payment_info['id'].startswith('pi_'): if 'latest_charge' in payment_info and isinstance(payment_info.get("latest_charge"), dict): chargeid = payment_info['latest_charge']['id'] else: chargeid = payment_info['charges']['data'][0]['id'] else: chargeid = payment_info['id'] kwargs = {} if self.settings.connect_destination: kwargs['reverse_transfer'] = True r = stripe.Refund.create( charge=chargeid, amount=self._get_amount(refund), **self.api_kwargs, **kwargs, ) except (stripe.error.InvalidRequestError, stripe.error.AuthenticationError, stripe.error.APIConnectionError) \ as e: if e.json_body and 'error' in e.json_body: err = e.json_body['error'] logger.exception('Stripe error: %s' % str(err)) else: err = {'message': str(e)} logger.exception('Stripe error: %s' % str(e)) refund.info_data = err refund.state = OrderRefund.REFUND_STATE_FAILED refund.execution_date = now() refund.save() refund.order.log_action('pretix.event.order.refund.failed', { 'local_id': refund.local_id, 'provider': refund.provider, 'error': str(e) }) raise PaymentException(_('We had trouble communicating with Stripe. Please try again and contact ' 'support if the problem persists.')) except stripe.error.StripeError as err: logger.error('Stripe error: %s' % str(err)) raise PaymentException(_('Stripe returned an error')) else: refund.info = str(r) if r.status in ('succeeded', 'pending'): refund.done() elif r.status in ('failed', 'canceled'): refund.state = OrderRefund.REFUND_STATE_FAILED refund.execution_date = now() refund.save() def shred_payment_info(self, obj: OrderPayment): if not obj.info: return d = json.loads(obj.info) keys = ( 'amount', 'currency', 'status', 'id', 'amount_capturable', 'amount_details', 'amount_received', 'application', 'application_fee_amount', 'canceled_at', 'confirmation_method', 'created', 'description', 'last_payment_error', 'payment_method', 'statement_descriptor', 'livemode' ) new = {k: v for k, v in d.items() if k in keys} if d.get("latest_charge") 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', ) new["latest_charge"] = {k: v for k, v in d["latest_charge"].items() if k in keys} if d.get('source'): new['source'] = { 'id': d['source'].get('id'), 'type': d['source'].get('type'), 'brand': d['source'].get('brand'), 'last4': d['source'].get('last4'), 'bank_name': d['source'].get('bank_name'), 'bank': d['source'].get('bank'), 'bic': d['source'].get('bic'), 'card': { 'brand': d['source'].get('card', {}).get('brand'), 'country': d['source'].get('card', {}).get('country'), 'last4': d['source'].get('card', {}).get('last4'), } } new['_shredded'] = True obj.info = json.dumps(new) obj.save(update_fields=['info']) for le in obj.order.all_logentries().filter( action_type="pretix.plugins.stripe.event" ).exclude(data="", shredded=True): d = le.parsed_data if 'data' in d: for k, v in list(d['data']['object'].items()): if v not in ('reason', 'status', 'failure_message', 'object', 'id'): d['data']['object'][k] = '█' le.data = json.dumps(d) le.shredded = True le.save(update_fields=['data', 'shredded']) def payment_is_valid_session(self, request): return request.session.get('payment_stripe_{}_payment_method_id'.format(self.method), '') != '' def checkout_prepare(self, request, cart): payment_method_id = request.POST.get('stripe_{}_payment_method_id'.format(self.method), '') request.session['payment_stripe_{}_payment_method_id'.format(self.method)] = payment_method_id if payment_method_id == '': messages.warning(request, _('You may need to enable JavaScript for Stripe payments.')) return False return True def execute_payment(self, request: HttpRequest, payment: OrderPayment): try: return self._handle_payment_intent(request, payment) finally: if 'payment_stripe_{}_payment_method_id'.format(self.method) in request.session: del request.session['payment_stripe_{}_payment_method_id'.format(self.method)] def is_moto(self, request, payment=None) -> bool: return False def _payment_intent_kwargs(self, request, payment): return {} def _handle_payment_intent(self, request, payment, intent=None): self._init_api() try: if self.payment_is_valid_session(request): payment_method_id = request.session.get('payment_stripe_{}_payment_method_id'.format(self.method), None) idempotency_key_seed = payment_method_id if payment_method_id is not None else payment.full_id params = {} params.update(self._connect_kwargs(payment)) params.update(self.api_kwargs) params.update(self._payment_intent_kwargs(request, payment)) if self.is_moto(request, payment): params.update({ 'payment_method_options': { 'card': { 'moto': True } } }) if self.method == "card": params['statement_descriptor_suffix'] = self.statement_descriptor(payment) else: 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 }, # TODO: Is this sufficient? idempotency_key=str(self.event.id) + payment.order.code + idempotency_key_seed, return_url=build_absolute_uri(self.event, 'plugins:stripe:sca.return', kwargs={ 'order': payment.order.code, 'payment': payment.pk, 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), }), expand=['latest_charge'], **params ) else: payment_info = json.loads(payment.info) if 'id' in payment_info: if not intent: intent = stripe.PaymentIntent.retrieve( payment_info['id'], expand=["latest_charge"], **self.api_kwargs ) else: return except stripe.error.CardError as e: if e.json_body: err = e.json_body['error'] logger.exception('Stripe error: %s' % str(err)) else: err = {'message': str(e)} logger.exception('Stripe error: %s' % str(e)) logger.info('Stripe card error: %s' % str(err)) payment.fail(info={ 'error': True, 'message': err['message'], }) raise PaymentException(_('Stripe reported an error with your card: %s') % err['message']) except stripe.error.StripeError as e: if e.json_body and 'error' in e.json_body: err = e.json_body['error'] logger.exception('Stripe error: %s' % str(err)) if err.get('code') == 'idempotency_key_in_use': # Same thing happening twice – we don't want to record a failure, as that might prevent the # other thread from succeeding. return else: err = {'message': str(e)} logger.exception('Stripe error: %s' % str(e)) payment.fail(info={ 'error': True, 'message': err['message'], }) raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch ' 'with us if this problem persists.')) else: ReferencedStripeObject.objects.get_or_create( reference=intent.id, defaults={'order': payment.order, 'payment': payment} ) if intent.status == 'requires_action': payment.info = str(intent) payment.state = OrderPayment.PAYMENT_STATE_CREATED payment.save() return self._redirect_to_sca(request, payment) if intent.status == 'requires_confirmation': payment.info = str(intent) payment.state = OrderPayment.PAYMENT_STATE_CREATED payment.save() self._confirm_payment_intent(request, payment) elif intent.status == 'succeeded' and intent.latest_charge.paid: try: payment.info = str(intent) payment.confirm() except Quota.QuotaExceededException as e: raise PaymentException(str(e)) except SendMailException: raise PaymentException(_('There was an error sending the confirmation mail.')) elif intent.status == 'processing': if request: messages.warning(request, _('Your payment is pending completion. We will inform you as soon as the ' 'payment completed.')) payment.info = str(intent) payment.state = OrderPayment.PAYMENT_STATE_PENDING payment.save() return elif intent.status == 'requires_payment_method': if request: messages.warning(request, _('Your payment failed. Please try again.')) payment.fail(info=str(intent)) return else: logger.info('Charge failed: %s' % str(intent)) payment.fail(info=str(intent)) raise PaymentException(_('Stripe reported an error: %s') % intent.last_payment_error.message) def _redirect_to_sca(self, request, payment): url = build_absolute_uri(self.event, 'plugins:stripe:sca', kwargs={ 'order': payment.order.code, 'payment': payment.pk, 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), }) if 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 def _confirm_payment_intent(self, request, payment): self._init_api() try: payment_info = json.loads(payment.info) intent = stripe.PaymentIntent.confirm( payment_info['id'], return_url=build_absolute_uri(self.event, 'plugins:stripe:sca.return', kwargs={ 'order': payment.order.code, 'payment': payment.pk, 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), }), expand=["latest_charge"], **self.api_kwargs ) payment.info = str(intent) payment.save() self._handle_payment_intent(request, payment) except stripe.error.CardError as e: if e.json_body: err = e.json_body['error'] logger.exception('Stripe error: %s' % str(err)) else: err = {'message': str(e)} logger.exception('Stripe error: %s' % str(e)) logger.info('Stripe card error: %s' % str(err)) payment.fail(info={ 'error': True, 'message': err['message'], }) raise PaymentException(_('Stripe reported an error with your card: %s') % err['message']) except stripe.error.InvalidRequestError as e: if e.json_body: err = e.json_body['error'] logger.exception('Stripe error: %s' % str(err)) else: err = {'message': str(e)} logger.exception('Stripe error: %s' % str(e)) payment.fail(info={ 'error': True, 'message': err['message'], }) raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch ' 'with us if this problem persists.')) class StripeSourceMethod(StripeMethod): def payment_is_valid_session(self, request): return True def _charge_source(self, request, source, payment): try: params = {} if not source.startswith('src_'): params['statement_descriptor'] = self.statement_descriptor(payment) params.update(self.api_kwargs) params.update(self._connect_kwargs(payment)) charge = stripe.Charge.create( amount=self._get_amount(payment), currency=self.event.currency.lower(), source=source, description='{event}-{code}'.format( event=self.event.slug.upper(), code=payment.order.code ), metadata={ 'order': str(payment.order.id), 'event': self.event.id, 'code': payment.order.code }, # TODO: Is this sufficient? idempotency_key=str(self.event.id) + payment.order.code + source, **params ) except stripe.error.CardError as e: if e.json_body: err = e.json_body['error'] logger.exception('Stripe error: %s' % str(err)) else: err = {'message': str(e)} logger.exception('Stripe error: %s' % str(e)) logger.info('Stripe card error: %s' % str(err)) payment.fail(info={ 'error': True, 'message': err['message'], }) raise PaymentException(_('Stripe reported an error with your card: %s') % err['message']) except stripe.error.StripeError as e: if e.json_body and 'error' in e.json_body: err = e.json_body['error'] logger.exception('Stripe error: %s' % str(err)) if err.get('code') == 'idempotency_key_in_use': # This is not an error we normally expect, however some payment methods like iDEAL will redirect # the user back to our confirmation page at the same time from two devices: the web browser the # purchase is executed from and the online banking app the payment is authorized from. # In this case we will just log the idempotency error but not expose it to the user and just # forward them back to their order page. There is a good chance that by the time the user hits # the order page, the other request has gone through and the payment is confirmed. # Usually however this should be prevented by SELECT FOR UPDATE calls! return else: err = {'message': str(e)} logger.exception('Stripe error: %s' % str(e)) payment.fail(info={ 'error': True, 'message': err['message'], }) raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch ' 'with us if this problem persists.')) else: ReferencedStripeObject.objects.get_or_create( reference=charge.id, defaults={'order': payment.order, 'payment': payment} ) if charge.status == 'succeeded' and charge.paid: try: payment.info = str(charge) payment.confirm() except Quota.QuotaExceededException as e: raise PaymentException(str(e)) except SendMailException: raise PaymentException(_('There was an error sending the confirmation mail.')) elif charge.status == 'pending': if request: messages.warning(request, _('Your payment is pending completion. We will inform you as soon as the ' 'payment completed.')) payment.info = str(charge) payment.state = OrderPayment.PAYMENT_STATE_PENDING payment.save() return else: logger.info('Charge failed: %s' % str(charge)) payment.fail(info=str(charge)) raise PaymentException(_('Stripe reported an error: %s') % charge.failure_message) def execute_payment(self, request: HttpRequest, payment: OrderPayment): self._init_api() try: source = self._create_source(request, payment) except stripe.error.StripeError as e: if e.json_body and 'err' in e.json_body: err = e.json_body['error'] logger.exception('Stripe error: %s' % str(err)) if err.get('code') == 'idempotency_key_in_use': # Same thing happening twice – we don't want to record a failure, as that might prevent the # other thread from succeeding. return else: err = {'message': str(e)} logger.exception('Stripe error: %s' % str(e)) payment.fail(info={ 'error': True, 'message': err['message'], }) raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch ' 'with us if this problem persists.')) ReferencedStripeObject.objects.get_or_create( reference=source.id, defaults={'order': payment.order, 'payment': payment} ) payment.info = str(source) payment.state = OrderPayment.PAYMENT_STATE_PENDING payment.save() request.session['payment_stripe_order_secret'] = payment.order.secret return self.redirect(request, source.redirect.url) class StripeRedirectMethod(StripeMethod): redirect_action_handling = "redirect" def payment_is_valid_session(self, request): # This does not have a payment_method_id, so we set it manually to None during checkout. # But we still need to check for its presence here. if "payment_stripe_{}_payment_method_id".format(self.method) in request.session: return True return False def checkout_prepare(self, request, cart): # This does not have a payment_method_id, so we set it manually to None during checkout, so that we can # verify later on if we are in or outside the checkout process. request.session["payment_stripe_{}_payment_method_id".format(self.method)] = None return True def _payment_intent_kwargs(self, request, payment): return { "payment_method_data": { "type": self.method, } } def payment_form_render(self, request) -> str: template = get_template('pretixplugins/stripe/checkout_payment_form_simple_noform.html') ctx = { 'request': request, 'event': self.event, 'settings': self.settings, 'explanation': self.explanation, } return template.render(ctx) class StripeCC(StripeMethod): identifier = 'stripe' verbose_name = _('Credit card via Stripe') public_name = _('Credit card') method = 'card' @property def walletqueries(self): # ToDo: Check against Stripe API, if ApplePay and GooglePay are even activated/available # This is probably only really feasable once the Payment Methods Configuration API is out of beta # https://stripe.com/docs/connect/payment-method-configurations if self.settings.get("walletdetection", True, as_type=bool): return [WalletQueries.APPLEPAY, WalletQueries.GOOGLEPAY] return [] 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(): stripe_verify_domain.apply_async(args=(self.event.pk, request.host)) 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) } 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', } for old, new in keymap.items(): if old in request.session: request.session[new] = request.session[old] del request.session[old] 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', '') return super().checkout_prepare(request, cart) def payment_is_valid_session(self, request): self._migrate_session(request) return super().payment_is_valid_session(request) def _handle_payment_intent(self, request, payment, intent=None): self._migrate_session(request) return super()._handle_payment_intent(request, payment, intent) def is_moto(self, request, payment=None) -> bool: # 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. 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 == 'resellers' return moto def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: if "latest_charge" in pi and isinstance(pi.get("latest_charge"), dict): card = pi["latest_charge"]["payment_method_details"]["card"] else: card = pi["source"]["card"] except: 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"))}' class StripeSEPADirectDebit(StripeMethod): 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 get_invoice_address(): if order and getattr(order, 'invoice_address', None): request._checkout_flow_invoice_address = order.invoice_address if not hasattr(request, '_checkout_flow_invoice_address'): cs = cart_session(request) 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) except InvoiceAddress.DoesNotExist: request._checkout_flow_invoice_address = InvoiceAddress() return request._checkout_flow_invoice_address cs = cart_session(request) self.ia = get_invoice_address() 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', '') } return template.render(ctx) @property 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), )), ]) 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'], } }, } } 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', '') return super().checkout_prepare(request, cart) def execute_payment(self, request: HttpRequest, payment: OrderPayment): try: return super().execute_payment(request, payment) finally: 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)] class StripeAffirm(StripeMethod): 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: 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 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 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', } } def payment_form_render(self, request, total, order=None) -> str: 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, } return template.render(ctx) class StripeKlarna(StripeRedirectMethod): identifier = "stripe_klarna" 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"} redirect_in_widget_allowed = False def _detect_country(self, request, order=None): def get_invoice_address(): if order and getattr(order, 'invoice_address', None): request._checkout_flow_invoice_address = order.invoice_address if not hasattr(request, '_checkout_flow_invoice_address'): cs = cart_session(request) 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) except InvoiceAddress.DoesNotExist: request._checkout_flow_invoice_address = InvoiceAddress() return request._checkout_flow_invoice_address ia = get_invoice_address() country = None if ia.country: country = str(ia.country) if country not in self.allowed_countries: country = guess_country_from_request(request, self.event) if country not in self.allowed_countries: country = self.settings.merchant_country if country not in self.allowed_countries: country = "DE" return country def _payment_intent_kwargs(self, request, payment): return { "payment_method_data": { "type": "klarna", "billing_details": { "email": payment.order.email, "address": { "country": self._detect_country(request, payment.order), }, }, } } def payment_form_render(self, request, total, order=None) -> str: template = get_template( "pretixplugins/stripe/checkout_payment_form_simple_messaging_noform.html" ) ctx = { "request": request, "event": self.event, "total": self._decimal_to_int(total), "method": self.method, 'explanation': self.explanation, "country": self._detect_country(request, order) } return template.render(ctx) 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 ) 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( args='href="https://docs.klarna.com/resources/test-environment/sample-customer-data/" target="_blank"' ) ) return None class StripeRedirectWithAccountNamePaymentIntentMethod(StripeRedirectMethod): def payment_form_render(self, request) -> str: template = get_template('pretixplugins/stripe/checkout_payment_form_simple.html') ctx = { 'request': request, 'event': self.event, 'settings': self.settings, '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'))), ]) 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'] 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'] return True return False class StripeGiropay(StripeRedirectWithAccountNamePaymentIntentMethod): 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.' ) redirect_in_widget_allowed = False def _payment_intent_kwargs(self, request, payment): return { "payment_method_data": { "type": "giropay", "giropay": {}, "billing_details": { "name": request.session.get(f"payment_stripe_{self.method}_account") or gettext("unknown name") }, } } def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: return gettext('Bank account at {bank}').format( bank=( pi.get("latest_charge", {}).get("payment_method_details", {}).get("giropay", {}).get("bank_name") or pi.get("source", {}).get("giropay", {}).get("bank_name", "?") ) ) except: logger.exception('Could not parse payment data') return super().payment_presale_render(payment) class StripeIdeal(StripeRedirectMethod): 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.' ) 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( bank=( pi.get("latest_charge", {}).get("payment_method_details", {}).get("ideal", {}).get("bank") or pi.get("source", {}).get("ideal", {}).get("bank", "?") ).replace("_", " ").title() ) except: logger.exception('Could not parse payment data') return super().payment_presale_render(payment) class StripeAlipay(StripeRedirectMethod): 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.' ) class StripeBancontact(StripeRedirectWithAccountNamePaymentIntentMethod): 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): return { "payment_method_data": { "type": "bancontact", "giropay": {}, "billing_details": { "name": request.session.get(f"payment_stripe_{self.method}_account") or gettext("unknown name") }, } } def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: return gettext('Bank account at {bank}').format( bank=( pi.get("latest_charge", {}).get("payment_method_details", {}).get("bancontact", {}).get("bank_name") or pi.get("source", {}).get("bancontact", {}).get("bank_name", "?") ) ) except: logger.exception('Could not parse payment data') return super().payment_presale_render(payment) class StripeSofort(StripeMethod): identifier = 'stripe_sofort' verbose_name = _('SOFORT via Stripe') public_name = _('SOFORT (instant bank transfer)') method = 'sofort' redirect_in_widget_allowed = False def payment_form_render(self, request) -> str: template = get_template('pretixplugins/stripe/checkout_payment_form_simple.html') ctx = { 'request': request, 'event': self.event, 'settings': self.settings, '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')) ))), ]) def _payment_intent_kwargs(self, request, payment): return { "payment_method_data": { "type": "sofort", "sofort": { "country": (request.session.get(f"payment_stripe_{self.method}_bank_country") or "DE").upper() }, } } def execute_payment(self, request: HttpRequest, payment: OrderPayment): try: return super().execute_payment(request, payment) finally: if f'payment_stripe_{self.method}_bank_country' in request.session: del request.session[f'payment_stripe_{self.method}_bank_country'] def payment_is_valid_session(self, request): return ( request.session.get(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'] 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( iban=f'{pi["source"]["sofort"]["country"]}****{pi["source"]["sofort"]["iban_last4"]}', bank=pi["source"]["sofort"]["bank_name"] ) except: 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' redirect_in_widget_allowed = False def _payment_intent_kwargs(self, request, payment): return { "payment_method_data": { "type": "eps", "giropay": {}, "billing_details": { "name": request.session.get(f"payment_stripe_{self.method}_account") or gettext("unknown name") }, } } def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: return gettext('Bank account at {bank}').format( bank=( pi.get("latest_charge", {}).get("payment_method_details", {}).get("eps", {}).get("bank") or pi.get("source", {}).get("eps", {}).get("bank", "?") ).replace("_", " ").title() ) except: logger.exception('Could not parse payment data') return super().payment_presale_render(payment) class StripeMultibanco(StripeSourceMethod): identifier = 'stripe_multibanco' verbose_name = _('Multibanco via Stripe') public_name = _('Multibanco') method = 'multibanco' redirect_in_widget_allowed = False def payment_form_render(self, request) -> str: template = get_template('pretixplugins/stripe/checkout_payment_form_simple_noform.html') ctx = { 'request': request, 'event': self.event, 'settings': self.settings, 'explanation': self.explanation, 'form': self.payment_form(request) } return template.render(ctx) def _create_source(self, request, payment): source = stripe.Source.create( type='multibanco', amount=self._get_amount(payment), currency=self.event.currency.lower(), metadata={ 'order': str(payment.order.id), 'event': self.event.id, 'code': payment.order.code }, owner={ 'email': payment.order.email }, redirect={ 'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={ 'order': payment.order.code, 'payment': payment.pk, 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(), }) }, **self.api_kwargs ) return source def payment_is_valid_session(self, request): return True def checkout_prepare(self, request, cart): return True class StripePrzelewy24(StripeRedirectMethod): 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.' ) redirect_in_widget_allowed = False def _payment_intent_kwargs(self, request, payment): return { "payment_method_data": { "type": "p24", "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) def payment_presale_render(self, payment: OrderPayment) -> str: pi = payment.info_data or {} try: return gettext('Bank account at {bank}').format( bank=( pi.get("latest_charge", {}).get("payment_method_details", {}).get("p24", {}).get("bank") or pi.get("source", {}).get("p24", {}).get("bank", "?") ).replace("_", " ").title() ) except: logger.exception('Could not parse payment data') return super().payment_presale_render(payment) class StripeWeChatPay(StripeRedirectMethod): 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.' ) @property def is_enabled(self) -> bool: return self.settings.get('_enabled', as_type=bool) and self.settings.get('method_wechatpay', as_type=bool) def _payment_intent_kwargs(self, request, payment): return { "payment_method_data": { "type": "wechat_pay", }, "payment_method_options": { "wechat_pay": { "client": "web" }, } } class StripePayPal(StripeRedirectMethod): identifier = 'stripe_paypal' verbose_name = _('PayPal via Stripe') public_name = _('PayPal') method = 'paypal'