diff --git a/MANIFEST.in b/MANIFEST.in
index c497bff7d..300ad2fa1 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -16,8 +16,6 @@ recursive-include src/pretix/plugins/banktransfer/templates *
recursive-include src/pretix/plugins/banktransfer/static *
recursive-include src/pretix/plugins/manualpayment/templates *
recursive-include src/pretix/plugins/manualpayment/static *
-recursive-include src/pretix/plugins/paypal/templates *
-recursive-include src/pretix/plugins/paypal/static *
recursive-include src/pretix/plugins/paypal2/templates *
recursive-include src/pretix/plugins/paypal2/static *
recursive-include src/pretix/plugins/src/pretixdroid/templates *
diff --git a/setup.cfg b/setup.cfg
index e7cdae97c..0f00aaee4 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -26,7 +26,6 @@ ignore =
src/tests/plugins/*
src/tests/plugins/badges/*
src/tests/plugins/banktransfer/*
- src/tests/plugins/paypal/*
src/tests/plugins/paypal2/*
src/tests/plugins/pretixdroid/*
src/tests/plugins/stripe/*
diff --git a/src/pretix/plugins/paypal/__init__.py b/src/pretix/plugins/paypal/__init__.py
index fe627f44f..9616a7bf6 100644
--- a/src/pretix/plugins/paypal/__init__.py
+++ b/src/pretix/plugins/paypal/__init__.py
@@ -19,15 +19,3 @@
# 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: Tobias Kunze
-#
-# 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.
diff --git a/src/pretix/plugins/paypal/api.py b/src/pretix/plugins/paypal/api.py
deleted file mode 100644
index 3a09c9422..000000000
--- a/src/pretix/plugins/paypal/api.py
+++ /dev/null
@@ -1,53 +0,0 @@
-#
-# This file is part of pretix (Community Edition).
-#
-# Copyright (C) 2014-2020 Raphael Michel and contributors
-# Copyright (C) 2020-today pretix 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
-# .
-#
-import hashlib
-import json
-
-from django.core.cache import cache
-from paypalrestsdk.api import Api as VendorApi
-
-
-class Api(VendorApi):
- def get_token_hash(self, authorization_code=None, refresh_token=None, headers=None):
- if not authorization_code and not refresh_token:
- checksum = hashlib.sha256(self.basic_auth().encode()).hexdigest()
- cache_key_hash = f'pretix_paypal_token_hash_{checksum}'
- cache_key_request_at = f'pretix_paypal_token_request_at_{checksum}'
- token_hash = cache.get(cache_key_hash)
- if token_hash:
- token_request_at = cache.get(cache_key_request_at)
- if token_request_at:
- self.token_hash = json.loads(token_hash)
- self.token_request_at = token_request_at
- self.validate_token_hash()
- if self.token_hash is not None:
- return self.token_hash
-
- t = super().get_token_hash(authorization_code, refresh_token, headers)
-
- if self.token_hash:
- cache.set(cache_key_hash, json.dumps(self.token_hash), 3600 * 4)
- cache.set(cache_key_request_at, self.token_request_at, 3600 * 4)
-
- return t
-
- return super().get_token_hash(authorization_code, refresh_token, headers)
diff --git a/src/pretix/plugins/paypal/apps.py b/src/pretix/plugins/paypal/apps.py
deleted file mode 100644
index 0b14e95e7..000000000
--- a/src/pretix/plugins/paypal/apps.py
+++ /dev/null
@@ -1,69 +0,0 @@
-#
-# This file is part of pretix (Community Edition).
-#
-# Copyright (C) 2014-2020 Raphael Michel and contributors
-# Copyright (C) 2020-today pretix 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: Tobias Kunze
-#
-# 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.
-
-from django.apps import AppConfig
-from django.utils.functional import cached_property
-from django.utils.translation import gettext_lazy as _
-
-from pretix import __version__ as version
-
-
-class PaypalApp(AppConfig):
- name = 'pretix.plugins.paypal'
- verbose_name = _("PayPal")
-
- class PretixPluginMeta:
- name = _("PayPal")
- author = _("the pretix team")
- version = version
- category = 'PAYMENT'
- featured = True
- picture = 'pretixplugins/paypal/paypal_logo.svg'
- description = _("Accept payments with your PayPal account. PayPal is one of the most popular payment methods "
- "world-wide.")
-
- def ready(self):
- from . import signals # NOQA
-
- def is_available(self, event):
- return 'pretix.plugins.paypal' in event.plugins.split(',')
-
- @cached_property
- def compatibility_errors(self):
- errs = []
- try:
- import paypalrestsdk # NOQA
- except ImportError:
- errs.append("Python package 'paypalrestsdk' is not installed.")
- return errs
diff --git a/src/pretix/plugins/paypal/migrations/0005_delete_referencedpaypalobject.py b/src/pretix/plugins/paypal/migrations/0005_delete_referencedpaypalobject.py
new file mode 100644
index 000000000..793f72249
--- /dev/null
+++ b/src/pretix/plugins/paypal/migrations/0005_delete_referencedpaypalobject.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.2.17 on 2026-09-08 08:05
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("paypal", "0004_bigint"),
+ ]
+
+ operations = [
+ migrations.SeparateDatabaseAndState(
+ state_operations=[
+ migrations.DeleteModel(
+ name="ReferencedPayPalObject",
+ ),
+ ],
+ database_operations=[]
+ )
+ ]
diff --git a/src/pretix/plugins/paypal/payment.py b/src/pretix/plugins/paypal/payment.py
deleted file mode 100644
index 2c8eb8265..000000000
--- a/src/pretix/plugins/paypal/payment.py
+++ /dev/null
@@ -1,715 +0,0 @@
-#
-# This file is part of pretix (Community Edition).
-#
-# Copyright (C) 2014-2020 Raphael Michel and contributors
-# Copyright (C) 2020-today pretix 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: Jakob Schnell, Tobias Kunze
-#
-# 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 json
-import logging
-from collections import OrderedDict
-from decimal import Decimal
-
-import paypalrestsdk
-import paypalrestsdk.exceptions
-from django import forms
-from django.contrib import messages
-from django.http import HttpRequest
-from django.template.loader import get_template
-from django.urls import reverse
-from django.utils.html import format_html
-from django.utils.timezone import now
-from django.utils.translation import gettext as __, gettext_lazy as _
-from i18nfield.strings import LazyI18nString
-from paypalrestsdk.exceptions import BadRequest, UnauthorizedAccess
-from paypalrestsdk.openid_connect import Tokeninfo
-from requests import RequestException
-
-from pretix.base.decimal import round_decimal
-from pretix.base.forms import SecretKeySettingsField
-from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
-from pretix.base.payment import BasePaymentProvider, PaymentException
-from pretix.base.settings import SettingsSandbox
-from pretix.base.views.redirect import safelink
-from pretix.multidomain.urlreverse import eventreverse_absolute
-from pretix.plugins.paypal.api import Api
-from pretix.plugins.paypal.models import ReferencedPayPalObject
-
-logger = logging.getLogger('pretix.plugins.paypal')
-
-SUPPORTED_CURRENCIES = ['AUD', 'BRL', 'CAD', 'CZK', 'DKK', 'EUR', 'HKD', 'HUF', 'INR', 'ILS', 'JPY', 'MYR', 'MXN',
- 'TWD', 'NZD', 'NOK', 'PHP', 'PLN', 'GBP', 'RUB', 'SGD', 'SEK', 'CHF', 'THB', 'USD']
-
-LOCAL_ONLY_CURRENCIES = ['INR']
-
-
-class Paypal(BasePaymentProvider):
- identifier = 'paypal'
- verbose_name = _('PayPal')
- payment_form_fields = OrderedDict([
- ])
-
- def __init__(self, event: Event):
- super().__init__(event)
- self.settings = SettingsSandbox('payment', 'paypal', event)
-
- @property
- def test_mode_message(self):
- if self.settings.connect_client_id and not self.settings.secret:
- # in OAuth mode, sandbox mode needs to be set global
- is_sandbox = self.settings.connect_endpoint == 'sandbox'
- else:
- is_sandbox = self.settings.get('endpoint') == 'sandbox'
- if is_sandbox:
- return _('The PayPal sandbox is being used, you can test without actually sending money but you will need a '
- 'PayPal sandbox user to log in.')
- return None
-
- @property
- def settings_form_fields(self):
- if self.settings.connect_client_id and not self.settings.secret:
- # PayPal connect
- if self.settings.connect_user_id:
- fields = [
- ('connect_user_id',
- forms.CharField(
- label=_('PayPal account'),
- disabled=True
- )),
- ]
- else:
- return {}
- else:
- fields = [
- ('client_id',
- forms.CharField(
- label=_('Client ID'),
- min_length=80,
- help_text=format_html(
- '{text}',
- text=_('Click here for a tutorial on how to obtain the required keys'),
- docs_url='https://docs.pretix.eu/en/latest/user/payments/paypal.html',
- )
- )),
- ('secret',
- SecretKeySettingsField(
- label=_('Secret'),
- max_length=80,
- min_length=80,
- )),
- ('endpoint',
- forms.ChoiceField(
- label=_('Endpoint'),
- initial='live',
- choices=(
- ('live', 'Live'),
- ('sandbox', 'Sandbox'),
- ),
- )),
- ]
-
- extra_fields = [
- ('prefix',
- forms.CharField(
- label=_('Reference prefix'),
- help_text=_('Any value entered here will be added in front of the regular booking reference '
- 'containing the order number.'),
- required=False,
- )),
- ('postfix',
- forms.CharField(
- label=_('Reference postfix'),
- help_text=_('Any value entered here will be added behind the regular booking reference '
- 'containing the order number.'),
- required=False,
- )),
- ]
-
- d = OrderedDict(
- fields + extra_fields + list(super().settings_form_fields.items())
- )
-
- d.move_to_end('prefix')
- d.move_to_end('postfix')
- d.move_to_end('_enabled', False)
- return d
-
- def get_connect_url(self, request):
- request.session['payment_paypal_oauth_event'] = request.event.pk
-
- self.init_api()
- return Tokeninfo.authorize_url({'scope': 'openid profile email'})
-
- def settings_content_render(self, request):
- settings_content = ""
- if self.settings.connect_client_id and not self.settings.secret:
- # Use PayPal connect
- if not self.settings.connect_user_id:
- # Migrate User to PayPal v2
- self.event.disable_plugin("pretix.plugins.paypal")
- self.event.enable_plugin("pretix.plugins.paypal2")
- self.event.save()
- else:
- settings_content = (
- ""
- ).format(
- reverse('plugins:paypal:oauth.disconnect', kwargs={
- 'organizer': self.event.organizer.slug,
- 'event': self.event.slug,
- }),
- _('Disconnect from PayPal')
- )
- else:
- # Migrate User to PayPal v2
- self.event.disable_plugin("pretix.plugins.paypal")
- self.event.enable_plugin("pretix.plugins.paypal2")
- self.event.save()
-
- return settings_content
-
- def is_allowed(self, request: HttpRequest, total: Decimal = None) -> bool:
- return super().is_allowed(request, total) and self.event.currency in SUPPORTED_CURRENCIES
-
- def init_api(self):
- if self.settings.connect_client_id and not self.settings.secret:
- paypalrestsdk.api.__api__ = Api(
- mode="sandbox" if "sandbox" in self.settings.connect_endpoint else 'live',
- client_id=self.settings.connect_client_id,
- client_secret=self.settings.connect_secret_key,
- openid_client_id=self.settings.connect_client_id,
- openid_client_secret=self.settings.connect_secret_key
- )
- else:
- paypalrestsdk.api.__api__ = Api(
- mode="sandbox" if "sandbox" in self.settings.get('endpoint') else 'live',
- client_id=self.settings.get('client_id'),
- client_secret=self.settings.get('secret')
- )
-
- def payment_is_valid_session(self, request):
- return (request.session.get('payment_paypal_id', '') != ''
- and request.session.get('payment_paypal_payer', '') != '')
-
- def payment_form_render(self, request) -> str:
- template = get_template('pretixplugins/paypal/checkout_payment_form.html')
- ctx = {'request': request, 'event': self.event, 'settings': self.settings}
- return template.render(ctx)
-
- def checkout_prepare(self, request, cart):
- self.init_api()
- kwargs = {}
- if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
- kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
-
- try:
- if self.settings.connect_client_id and not self.settings.secret:
- if not request.event.settings.payment_paypal_connect_user_id:
- raise PaymentException('Payment method misconfigured')
-
- try:
- tokeninfo = Tokeninfo.create_with_refresh_token(request.event.settings.payment_paypal_connect_refresh_token)
- except BadRequest as ex:
- ex = json.loads(ex.content)
- messages.error(request, '{}: {} ({})'.format(
- _('We had trouble communicating with PayPal'),
- ex['error_description'],
- ex['correlation_id'])
- )
- return
-
- # Even if the token has been refreshed, calling userinfo() can fail. In this case we just don't
- # get the userinfo again and use the payment_paypal_connect_user_id that we already have on file
- try:
- userinfo = tokeninfo.userinfo()
- request.event.settings.payment_paypal_connect_user_id = userinfo.email
- except UnauthorizedAccess:
- pass
-
- payee = {
- "email": request.event.settings.payment_paypal_connect_user_id,
- # If PayPal ever offers a good way to get the MerchantID via the Identifity API,
- # we should use it instead of the merchant's eMail-address
- # "merchant_id": request.event.settings.payment_paypal_connect_user_id,
- }
- else:
- payee = {}
-
- payment = paypalrestsdk.Payment({
- 'header': {'PayPal-Partner-Attribution-Id': 'ramiioSoftwareentwicklung_SP'},
- 'intent': 'sale',
- 'payer': {
- "payment_method": "paypal",
- },
- "redirect_urls": {
- "return_url": eventreverse_absolute(request.event, 'plugins:paypal:return', kwargs=kwargs),
- "cancel_url": eventreverse_absolute(request.event, 'plugins:paypal:abort', kwargs=kwargs),
- },
- "transactions": [
- {
- "item_list": {
- "items": [
- {
- "name": '{prefix}{orderstring}{postfix}'.format(
- prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
- orderstring=__('Order for %s') % str(request.event),
- postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
- ),
- "quantity": 1,
- "price": self.format_price(cart['total']),
- "currency": request.event.currency
- }
- ]
- },
- "amount": {
- "currency": request.event.currency,
- "total": self.format_price(cart['total'])
- },
- "description": __('Event tickets for {event}').format(event=request.event.name),
- "payee": payee,
- "custom": '{prefix}{slug}{postfix}'.format(
- prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
- slug=request.event.slug.upper(),
- postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
- )
- }
- ]
- })
- request.session['payment_paypal_payment'] = None
- return self._create_payment(request, payment)
- except paypalrestsdk.exceptions.ConnectionError as e:
- messages.error(request, _('We had trouble communicating with PayPal'))
- logger.exception('Error on creating payment: ' + str(e))
-
- def format_price(self, value):
- return str(round_decimal(value, self.event.currency, {
- # PayPal behaves differently than Stripe in deciding what currencies have decimal places
- # Source https://developer.paypal.com/docs/classic/api/currency_codes/
- 'HUF': 0,
- 'JPY': 0,
- 'MYR': 0,
- 'TWD': 0,
- # However, CLPs are not listed there while PayPal requires us not to send decimal places there. WTF.
- 'CLP': 0,
- # Let's just guess that the ones listed here are 0-based as well
- # https://developers.braintreepayments.com/reference/general/currencies
- 'BIF': 0,
- 'DJF': 0,
- 'GNF': 0,
- 'KMF': 0,
- 'KRW': 0,
- 'LAK': 0,
- 'PYG': 0,
- 'RWF': 0,
- 'UGX': 0,
- 'VND': 0,
- 'VUV': 0,
- 'XAF': 0,
- 'XOF': 0,
- 'XPF': 0,
- }))
-
- @property
- def abort_pending_allowed(self):
- return False
-
- def _create_payment(self, request, payment):
- if payment.create():
- if payment.state not in ('created', 'approved', 'pending'):
- messages.error(request, _('We had trouble communicating with PayPal'))
- logger.error('Invalid payment state: ' + str(payment))
- return
- request.session['payment_paypal_id'] = payment.id
- for link in payment.links:
- if link.method == "REDIRECT" and link.rel == "approval_url":
- if request.session.get('iframe_session', False):
- return safelink(link.href, framebreak=True)
- else:
- return str(link.href)
- else:
- messages.error(request, _('We had trouble communicating with PayPal'))
- logger.error('Error on creating payment: ' + str(payment.error))
-
- def checkout_confirm_render(self, request) -> str:
- """
- Returns the HTML that should be displayed when the user selected this provider
- on the 'confirm order' page.
- """
- template = get_template('pretixplugins/paypal/checkout_payment_confirm.html')
- ctx = {'request': request, 'event': self.event, 'settings': self.settings}
- return template.render(ctx)
-
- def execute_payment(self, request: HttpRequest, payment: OrderPayment):
- if (request.session.get('payment_paypal_id', '') == '' or request.session.get('payment_paypal_payer', '') == ''):
- raise PaymentException(_('We were unable to process your payment. See below for details on how to '
- 'proceed.'))
-
- self.init_api()
- pp_payment = paypalrestsdk.Payment.find(request.session.get('payment_paypal_id'))
- ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=pp_payment.id)
- if str(pp_payment.transactions[0].amount.total) != str(payment.amount) or pp_payment.transactions[0].amount.currency \
- != self.event.currency:
- logger.error('Value mismatch: Payment %s vs paypal trans %s' % (payment.id, str(pp_payment)))
- raise PaymentException(_('We were unable to process your payment. See below for details on how to '
- 'proceed.'))
-
- return self._execute_payment(pp_payment, request, payment)
-
- def _execute_payment(self, payment, request, payment_obj):
- if payment.state == 'created':
- payment.replace([
- {
- "op": "replace",
- "path": "/transactions/0/item_list",
- "value": {
- "items": [
- {
- "name": '{prefix}{orderstring}{postfix}'.format(
- prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
- orderstring=__('Order {slug}-{code}').format(
- slug=self.event.slug.upper(),
- code=payment_obj.order.code
- ),
- postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
- ),
- "quantity": 1,
- "price": self.format_price(payment_obj.amount),
- "currency": payment_obj.order.event.currency
- }
- ]
- }
- },
- {
- "op": "replace",
- "path": "/transactions/0/description",
- "value": '{prefix}{orderstring}{postfix}'.format(
- prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
- orderstring=__('Order {order} for {event}').format(
- event=request.event.name,
- order=payment_obj.order.code
- ),
- postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
- ),
- }
- ])
- try:
- payment.execute({"payer_id": request.session.get('payment_paypal_payer')})
- except paypalrestsdk.exceptions.ConnectionError as e:
- messages.error(request, _('We had trouble communicating with PayPal'))
- logger.exception('Error on creating payment: ' + str(e))
- except RequestException as e:
- messages.error(request, _('We had trouble communicating with PayPal'))
- logger.exception('Error on creating payment: ' + str(e))
-
- for trans in payment.transactions:
- for rr in trans.related_resources:
- if hasattr(rr, 'sale') and rr.sale:
- if rr.sale.state == 'pending':
- messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as '
- 'soon as the payment completed.'))
- payment_obj.info = json.dumps(payment.to_dict())
- payment_obj.state = OrderPayment.PAYMENT_STATE_PENDING
- payment_obj.save()
- return
-
- payment_obj.refresh_from_db()
- if payment.state == 'pending':
- messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as soon as the '
- 'payment completed.'))
- payment_obj.info = json.dumps(payment.to_dict())
- payment_obj.state = OrderPayment.PAYMENT_STATE_PENDING
- payment_obj.save()
- return
-
- if payment.state != 'approved':
- payment_obj.fail(info=payment.to_dict())
- logger.error('Invalid state: %s' % str(payment))
- raise PaymentException(_('We were unable to process your payment. See below for details on how to '
- 'proceed.'))
-
- if payment_obj.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
- logger.warning('PayPal success event even though order is already marked as paid')
- return
-
- try:
- payment_obj.info = json.dumps(payment.to_dict())
- payment_obj.save(update_fields=['info'])
- payment_obj.confirm()
- except Quota.QuotaExceededException as e:
- raise PaymentException(str(e))
- return None
-
- def payment_pending_render(self, request, payment) -> str:
- retry = True
- try:
- if (
- payment.info
- and payment.info_data['transactions'][0]['related_resources'][0]['sale']['state'] == 'pending'
- ):
- retry = False
- except (KeyError, IndexError):
- pass
- template = get_template('pretixplugins/paypal/pending.html')
- ctx = {'request': request, 'event': self.event, 'settings': self.settings,
- 'retry': retry, 'order': payment.order}
- return template.render(ctx)
-
- def matching_id(self, payment: OrderPayment):
- sale_id = None
- for trans in payment.info_data.get('transactions', []):
- for res in trans.get('related_resources', []):
- if 'sale' in res and 'id' in res['sale']:
- sale_id = res['sale']['id']
- return sale_id or payment.info_data.get('id', None)
-
- def api_payment_details(self, payment: OrderPayment):
- sale_id = None
- for trans in payment.info_data.get('transactions', []):
- for res in trans.get('related_resources', []):
- if 'sale' in res and 'id' in res['sale']:
- sale_id = res['sale']['id']
- return {
- "payer_email": payment.info_data.get('payer', {}).get('payer_info', {}).get('email'),
- "payer_id": payment.info_data.get('payer', {}).get('payer_info', {}).get('payer_id'),
- "cart_id": payment.info_data.get('cart', None),
- "payment_id": payment.info_data.get('id', None),
- "sale_id": sale_id,
- }
-
- def payment_control_render(self, request: HttpRequest, payment: OrderPayment):
- template = get_template('pretixplugins/paypal/control.html')
- sale_id = None
- for trans in payment.info_data.get('transactions', []):
- for res in trans.get('related_resources', []):
- if 'sale' in res and 'id' in res['sale']:
- sale_id = res['sale']['id']
- ctx = {'request': request, 'event': self.event, 'settings': self.settings,
- 'payment_info': payment.info_data, 'order': payment.order, 'sale_id': sale_id}
- return template.render(ctx)
-
- def payment_control_render_short(self, payment: OrderPayment) -> str:
- return payment.info_data.get('payer', {}).get('payer_info', {}).get('email', '')
-
- def payment_partial_refund_supported(self, payment: OrderPayment):
- # Paypal refunds are possible for 180 days after purchase:
- # https://www.paypal.com/lc/smarthelp/article/how-do-i-issue-a-refund-faq780#:~:text=Refund%20after%20180%20days%20of,PayPal%20balance%20of%20the%20recipient.
- return (now() - payment.payment_date).days <= 180
-
- def payment_refund_supported(self, payment: OrderPayment):
- self.payment_partial_refund_supported(payment)
-
- def execute_refund(self, refund: OrderRefund):
- self.init_api()
-
- try:
- sale = None
- for res in refund.payment.info_data['transactions'][0]['related_resources']:
- for k, v in res.items():
- if k == 'sale':
- sale = paypalrestsdk.Sale.find(v['id'])
- break
-
- if not sale:
- pp_payment = paypalrestsdk.Payment.find(refund.payment.info_data['id'])
- for res in pp_payment.transactions[0].related_resources:
- for k, v in res.to_dict().items():
- if k == 'sale':
- sale = paypalrestsdk.Sale.find(v['id'])
- break
-
- pp_refund = sale.refund({
- "amount": {
- "total": self.format_price(refund.amount),
- "currency": refund.order.event.currency
- }
- })
- except paypalrestsdk.exceptions.ConnectionError as e:
- refund.order.log_action('pretix.event.order.refund.failed', {
- 'local_id': refund.local_id,
- 'provider': refund.provider,
- 'error': str(e)
- })
- raise PaymentException(_('Refunding the amount via PayPal failed: {}').format(str(e)))
- if not pp_refund.success():
- refund.order.log_action('pretix.event.order.refund.failed', {
- 'local_id': refund.local_id,
- 'provider': refund.provider,
- 'error': str(pp_refund.error)
- })
- raise PaymentException(_('Refunding the amount via PayPal failed: {}').format(pp_refund.error))
- else:
- sale = paypalrestsdk.Payment.find(refund.payment.info_data['id'])
- refund.payment.info = json.dumps(sale.to_dict())
- refund.info = json.dumps(pp_refund.to_dict())
- refund.done()
-
- def payment_prepare(self, request, payment_obj):
- self.init_api()
-
- try:
- if request.event.settings.payment_paypal_connect_user_id:
- try:
- tokeninfo = Tokeninfo.create_with_refresh_token(request.event.settings.payment_paypal_connect_refresh_token)
- except BadRequest as ex:
- ex = json.loads(ex.content)
- messages.error(request, '{}: {} ({})'.format(
- _('We had trouble communicating with PayPal'),
- ex['error_description'],
- ex['correlation_id'])
- )
- return
-
- # Even if the token has been refreshed, calling userinfo() can fail. In this case we just don't
- # get the userinfo again and use the payment_paypal_connect_user_id that we already have on file
- try:
- userinfo = tokeninfo.userinfo()
- request.event.settings.payment_paypal_connect_user_id = userinfo.email
- except UnauthorizedAccess:
- pass
-
- payee = {
- "email": request.event.settings.payment_paypal_connect_user_id,
- # If PayPal ever offers a good way to get the MerchantID via the Identifity API,
- # we should use it instead of the merchant's eMail-address
- # "merchant_id": request.event.settings.payment_paypal_connect_user_id,
- }
- else:
- payee = {}
-
- payment = paypalrestsdk.Payment({
- 'header': {'PayPal-Partner-Attribution-Id': 'ramiioSoftwareentwicklung_SP'},
- 'intent': 'sale',
- 'payer': {
- "payment_method": "paypal",
- },
- "redirect_urls": {
- "return_url": eventreverse_absolute(request.event, 'plugins:paypal:return'),
- "cancel_url": eventreverse_absolute(request.event, 'plugins:paypal:abort'),
- },
- "transactions": [
- {
- "item_list": {
- "items": [
- {
- "name": '{prefix}{orderstring}{postfix}'.format(
- prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
- orderstring=__('Order {slug}-{code}').format(
- slug=self.event.slug.upper(),
- code=payment_obj.order.code
- ),
- postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
- ),
- "quantity": 1,
- "price": self.format_price(payment_obj.amount),
- "currency": payment_obj.order.event.currency
- }
- ]
- },
- "amount": {
- "currency": request.event.currency,
- "total": self.format_price(payment_obj.amount)
- },
- "description": '{prefix}{orderstring}{postfix}'.format(
- prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
- orderstring=__('Order {order} for {event}').format(
- event=request.event.name,
- order=payment_obj.order.code
- ),
- postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
- ),
- "payee": payee,
- "custom": '{prefix}{slug}-{code}{postfix}'.format(
- prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
- slug=self.event.slug.upper(),
- code=payment_obj.order.code,
- postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
- ),
- }
- ]
- })
- request.session['payment_paypal_payment'] = payment_obj.pk
- return self._create_payment(request, payment)
- except paypalrestsdk.exceptions.ConnectionError as e:
- messages.error(request, _('We had trouble communicating with PayPal'))
- logger.exception('Error on creating payment: ' + str(e))
-
- def shred_payment_info(self, obj):
- if obj.info:
- d = json.loads(obj.info)
- new = {
- 'id': d.get('id'),
- 'payer': {
- 'payer_info': {
- 'email': '█'
- }
- },
- 'update_time': d.get('update_time'),
- 'transactions': [
- {
- 'amount': t.get('amount')
- } for t in d.get('transactions', [])
- ],
- '_shredded': True
- }
- obj.info = json.dumps(new)
- obj.save(update_fields=['info'])
-
- for le in obj.order.all_logentries().filter(action_type="pretix.plugins.paypal.event").exclude(data=""):
- d = le.parsed_data
- if 'resource' in d:
- d['resource'] = {
- 'id': d['resource'].get('id'),
- 'sale_id': d['resource'].get('sale_id'),
- 'parent_payment': d['resource'].get('parent_payment'),
- }
- le.data = json.dumps(d)
- le.shredded = True
- le.save(update_fields=['data', 'shredded'])
-
- def render_invoice_text(self, order: Order, payment: OrderPayment) -> str:
- if order.status == Order.STATUS_PAID:
- if payment.info_data.get('id', None):
- try:
- return '{}\r\n{}: {}\r\n{}: {}'.format(
- _('The payment for this invoice has already been received.'),
- _('PayPal payment ID'),
- payment.info_data['id'],
- _('PayPal sale ID'),
- payment.info_data['transactions'][0]['related_resources'][0]['sale']['id']
- )
- except (KeyError, IndexError):
- return '{}\r\n{}: {}'.format(
- _('The payment for this invoice has already been received.'),
- _('PayPal payment ID'),
- payment.info_data['id']
- )
- else:
- return super().render_invoice_text(order, payment)
-
- return self.settings.get('_invoice_text', as_type=LazyI18nString, default='')
diff --git a/src/pretix/plugins/paypal/signals.py b/src/pretix/plugins/paypal/signals.py
deleted file mode 100644
index 2d4dbd7d0..000000000
--- a/src/pretix/plugins/paypal/signals.py
+++ /dev/null
@@ -1,31 +0,0 @@
-#
-# This file is part of pretix (Community Edition).
-#
-# Copyright (C) 2014-2020 Raphael Michel and contributors
-# Copyright (C) 2020-today pretix 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
-# .
-#
-
-from django.dispatch import receiver
-
-from pretix.base.signals import register_payment_providers
-
-
-@receiver(register_payment_providers, dispatch_uid="payment_paypal")
-def register_payment_provider(sender, **kwargs):
- from .payment import Paypal
- return Paypal
diff --git a/src/pretix/plugins/paypal/static/pretixplugins/paypal/paypal_logo.svg b/src/pretix/plugins/paypal/static/pretixplugins/paypal/paypal_logo.svg
deleted file mode 100644
index a04e116dc..000000000
--- a/src/pretix/plugins/paypal/static/pretixplugins/paypal/paypal_logo.svg
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/src/pretix/plugins/paypal/templates/pretixplugins/paypal/checkout_payment_confirm.html b/src/pretix/plugins/paypal/templates/pretixplugins/paypal/checkout_payment_confirm.html
deleted file mode 100644
index f10976374..000000000
--- a/src/pretix/plugins/paypal/templates/pretixplugins/paypal/checkout_payment_confirm.html
+++ /dev/null
@@ -1,6 +0,0 @@
-{% load i18n %}
-
-
{% blocktrans trimmed %}
- The total amount listed above will be withdrawn from your PayPal account after the
- confirmation of your purchase.
-{% endblocktrans %}
{% blocktrans trimmed %}
- After you clicked continue, we will redirect you to PayPal to fill in your payment
- details. You will then be redirected back here to review and confirm your order.
-{% endblocktrans %}
{% blocktrans trimmed %}
- Our attempt to execute your Payment via PayPal has failed. Please try again or contact us.
- {% endblocktrans %}
-{% else %}
-
{% blocktrans trimmed %}
- We're waiting for an answer from PayPal regarding your payment. Please contact us, if this
- takes more than a few hours.
- {% endblocktrans %}
-{% endif %}
diff --git a/src/pretix/plugins/paypal/urls.py b/src/pretix/plugins/paypal/urls.py
deleted file mode 100644
index 8b69853e1..000000000
--- a/src/pretix/plugins/paypal/urls.py
+++ /dev/null
@@ -1,39 +0,0 @@
-#
-# This file is part of pretix (Community Edition).
-#
-# Copyright (C) 2014-2020 Raphael Michel and contributors
-# Copyright (C) 2020-today pretix 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
-# .
-#
-from django.urls import include, re_path
-
-from .views import abort, oauth_disconnect, success
-
-event_patterns = [
- re_path(r'^paypal/', include([
- re_path(r'^abort/$', abort, name='abort'),
- re_path(r'^return/$', success, name='return'),
-
- re_path(r'w/(?P[a-zA-Z0-9]{16})/abort/', abort, name='abort'),
- re_path(r'w/(?P[a-zA-Z0-9]{16})/return/', success, name='return'),
- ])),
-]
-
-urlpatterns = [
- re_path(r'^control/event/(?P[^/]+)/(?P[^/]+)/paypal/disconnect/',
- oauth_disconnect, name='oauth.disconnect'),
-]
diff --git a/src/pretix/plugins/paypal/views.py b/src/pretix/plugins/paypal/views.py
deleted file mode 100644
index 3e4049d8b..000000000
--- a/src/pretix/plugins/paypal/views.py
+++ /dev/null
@@ -1,249 +0,0 @@
-#
-# This file is part of pretix (Community Edition).
-#
-# Copyright (C) 2014-2020 Raphael Michel and contributors
-# Copyright (C) 2020-today pretix 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: Flavia Bastos
-#
-# 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 json
-import logging
-from decimal import Decimal
-
-import paypalrestsdk
-import paypalrestsdk.exceptions
-from django.contrib import messages
-from django.db.models import Sum
-from django.http import HttpResponse
-from django.urls import reverse
-from django.utils.translation import gettext_lazy as _
-from django.views.decorators.csrf import csrf_exempt
-from django.views.decorators.http import require_POST
-from django_scopes import scopes_disabled
-
-from pretix.base.models import Order, OrderPayment, OrderRefund, Quota
-from pretix.base.payment import PaymentException
-from pretix.control.permissions import event_permission_required
-from pretix.helpers.http import redirect_to_url
-from pretix.multidomain.urlreverse import eventreverse
-from pretix.plugins.paypal.models import ReferencedPayPalObject
-from pretix.plugins.paypal.payment import Paypal
-
-logger = logging.getLogger('pretix.plugins.paypal')
-
-
-def success(request, *args, **kwargs):
- pid = request.GET.get('paymentId')
- token = request.GET.get('token')
- payer = request.GET.get('PayerID')
- request.session['payment_paypal_token'] = token
- request.session['payment_paypal_payer'] = payer
-
- urlkwargs = {}
- if 'cart_namespace' in kwargs:
- urlkwargs['cart_namespace'] = kwargs['cart_namespace']
-
- if request.session.get('payment_paypal_payment'):
- payment = OrderPayment.objects.get(pk=request.session.get('payment_paypal_payment'))
- else:
- payment = None
-
- if pid == request.session.get('payment_paypal_id', None):
- if payment:
- prov = Paypal(request.event)
- try:
- resp = prov.execute_payment(request, payment)
- except PaymentException as e:
- messages.error(request, str(e))
- urlkwargs['step'] = 'payment'
- return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
- if resp:
- return resp
- else:
- messages.error(request, _('Invalid response from PayPal received.'))
- logger.error('Session did not contain payment_paypal_id')
- urlkwargs['step'] = 'payment'
- return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
-
- if payment:
- return redirect_to_url(eventreverse(request.event, 'presale:event.order', kwargs={
- 'order': payment.order.code,
- 'secret': payment.order.secret
- }) + ('?paid=yes' if payment.order.status == Order.STATUS_PAID else ''))
- else:
- urlkwargs['step'] = 'confirm'
- return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
-
-
-def abort(request, *args, **kwargs):
- messages.error(request, _('It looks like you canceled the PayPal payment'))
-
- if request.session.get('payment_paypal_payment'):
- payment = OrderPayment.objects.get(pk=request.session.get('payment_paypal_payment'))
- else:
- payment = None
-
- if payment:
- return redirect_to_url(eventreverse(request.event, 'presale:event.order', kwargs={
- 'order': payment.order.code,
- 'secret': payment.order.secret
- }) + ('?paid=yes' if payment.order.status == Order.STATUS_PAID else ''))
- else:
- return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'}))
-
-
-@csrf_exempt
-@require_POST
-@scopes_disabled()
-def webhook(request, *args, **kwargs):
- event_body = request.body.decode('utf-8').strip()
- event_json = json.loads(event_body)
-
- # We do not check the signature, we just use it as a trigger to look the charge up.
- if 'resource_type' not in event_json:
- return HttpResponse("Invalid body, no resource_type given", status=400)
- if event_json['resource_type'] not in ('sale', 'refund'):
- return HttpResponse("Not interested in this resource type", status=200)
-
- if event_json['resource_type'] == 'sale':
- saleid = event_json['resource']['id']
- else:
- saleid = event_json['resource']['sale_id']
-
- try:
- refs = [saleid]
- if event_json['resource'].get('parent_payment'):
- refs.append(event_json['resource'].get('parent_payment'))
-
- rso = ReferencedPayPalObject.objects.select_related('order', 'order__event').get(
- reference__in=refs
- )
- event = rso.order.event
- except ReferencedPayPalObject.DoesNotExist:
- rso = None
- if hasattr(request, 'event'):
- event = request.event
- else:
- return HttpResponse("Unable to detect event", status=200)
-
- prov = Paypal(event)
- prov.init_api()
-
- try:
- sale = paypalrestsdk.Sale.find(saleid)
- except paypalrestsdk.exceptions.ConnectionError:
- logger.exception('PayPal error on webhook. Event data: %s' % str(event_json))
- return HttpResponse('Sale not found', status=500)
-
- if rso and rso.payment:
- payment = rso.payment
- else:
- payments = OrderPayment.objects.filter(order__event=event, provider='paypal',
- info__icontains=sale['id'])
- payment = None
- for p in payments:
- payment_info = p.info_data
- for res in payment_info['transactions'][0]['related_resources']:
- for k, v in res.items():
- if k == 'sale' and v['id'] == sale['id']:
- payment = p
- break
-
- if not payment:
- return HttpResponse('Payment not found', status=200)
-
- payment.order.log_action('pretix.plugins.paypal.event', data=event_json)
-
- if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED and sale['state'] in ('partially_refunded', 'refunded'):
- if event_json['resource_type'] == 'refund':
- try:
- refund = paypalrestsdk.Refund.find(event_json['resource']['id'])
- except paypalrestsdk.exceptions.ConnectionError:
- logger.exception('PayPal error on webhook. Event data: %s' % str(event_json))
- return HttpResponse('Refund not found', status=500)
-
- known_refunds = {r.info_data.get('id'): r for r in payment.refunds.all()}
- if refund['id'] not in known_refunds:
- payment.create_external_refund(
- amount=abs(Decimal(refund['amount']['total'])),
- info=json.dumps(refund.to_dict() if not isinstance(refund, dict) else refund)
- )
- elif known_refunds.get(refund['id']).state in (
- OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT) and refund['state'] == 'completed':
- known_refunds.get(refund['id']).done()
-
- if 'total_refunded_amount' in refund:
- known_sum = payment.refunds.filter(
- state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
- OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_SOURCE_EXTERNAL)
- ).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
- total_refunded_amount = Decimal(refund['total_refunded_amount']['value'])
- if known_sum < total_refunded_amount:
- payment.create_external_refund(
- amount=total_refunded_amount - known_sum
- )
- elif sale['state'] == 'refunded':
- known_sum = payment.refunds.filter(
- state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
- OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_SOURCE_EXTERNAL)
- ).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
-
- if known_sum < payment.amount:
- payment.create_external_refund(
- amount=payment.amount - known_sum
- )
- elif payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED,
- OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED) and sale['state'] == 'completed':
- try:
- payment.confirm()
- except Quota.QuotaExceededException:
- pass
-
- return HttpResponse(status=200)
-
-
-@event_permission_required('event.settings.general:write')
-@require_POST
-def oauth_disconnect(request, **kwargs):
- del request.event.settings.payment_paypal_connect_refresh_token
- del request.event.settings.payment_paypal_connect_user_id
- request.event.settings.payment_paypal__enabled = False
- messages.success(request, _('Your PayPal account has been disconnected.'))
-
- # Migrate User to PayPal v2
- event = request.event
- event.disable_plugin("pretix.plugins.paypal")
- event.enable_plugin("pretix.plugins.paypal2")
- event.save()
-
- return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={
- 'organizer': request.event.organizer.slug,
- 'event': request.event.slug,
- 'provider': 'paypal_settings'
- }))
diff --git a/src/pretix/plugins/paypal2/migrations/0001_initial.py b/src/pretix/plugins/paypal2/migrations/0001_initial.py
new file mode 100644
index 000000000..c38897772
--- /dev/null
+++ b/src/pretix/plugins/paypal2/migrations/0001_initial.py
@@ -0,0 +1,54 @@
+# Generated by Django 5.2.17 on 2026-09-08 08:01
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ("pretixbase", "0310_question_valid_string_length_min"),
+ ]
+
+ operations = [
+ migrations.SeparateDatabaseAndState(
+ state_operations=[
+ migrations.CreateModel(
+ name="ReferencedPayPalObject",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True, primary_key=True, serialize=False
+ ),
+ ),
+ (
+ "reference",
+ models.CharField(db_index=True, max_length=190, unique=True),
+ ),
+ (
+ "order",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ to="pretixbase.order",
+ ),
+ ),
+ (
+ "payment",
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="pretixbase.orderpayment",
+ ),
+ ),
+ ],
+ options={
+ "db_table": "paypal_referencedpaypalobject",
+ },
+ ),
+ ],
+ database_operations=[]
+ )
+ ]
diff --git a/src/pretix/plugins/paypal/models.py b/src/pretix/plugins/paypal2/models.py
similarity index 95%
rename from src/pretix/plugins/paypal/models.py
rename to src/pretix/plugins/paypal2/models.py
index b6ffe7c50..ec95e95b3 100644
--- a/src/pretix/plugins/paypal/models.py
+++ b/src/pretix/plugins/paypal2/models.py
@@ -26,3 +26,6 @@ class ReferencedPayPalObject(models.Model):
reference = models.CharField(max_length=190, db_index=True, unique=True)
order = models.ForeignKey('pretixbase.Order', on_delete=models.CASCADE)
payment = models.ForeignKey('pretixbase.OrderPayment', null=True, blank=True, on_delete=models.CASCADE)
+
+ class Meta:
+ db_table = 'paypal_referencedpaypalobject'
diff --git a/src/pretix/plugins/paypal2/payment.py b/src/pretix/plugins/paypal2/payment.py
index d89b8dada..c918874f7 100644
--- a/src/pretix/plugins/paypal2/payment.py
+++ b/src/pretix/plugins/paypal2/payment.py
@@ -68,7 +68,7 @@ from pretix.plugins.paypal2.client.core.paypal_http_client import (
from pretix.plugins.paypal2.client.customer.partner_referral_create_request import (
PartnerReferralCreateRequest,
)
-from pretix.plugins.paypal.models import ReferencedPayPalObject
+from pretix.plugins.paypal2.models import ReferencedPayPalObject
logger = logging.getLogger('pretix.plugins.paypal2')
diff --git a/src/pretix/plugins/paypal2/views.py b/src/pretix/plugins/paypal2/views.py
index e8ff5592e..761e25c10 100644
--- a/src/pretix/plugins/paypal2/views.py
+++ b/src/pretix/plugins/paypal2/views.py
@@ -70,7 +70,7 @@ from pretix.plugins.paypal2.client.customer.partners_merchantintegrations_get_re
from pretix.plugins.paypal2.payment import (
PaypalMethod, PaypalMethod as Paypal, PaypalWallet,
)
-from pretix.plugins.paypal.models import ReferencedPayPalObject
+from pretix.plugins.paypal2.models import ReferencedPayPalObject
from pretix.presale.views import get_cart
from pretix.presale.views.cart import cart_session
@@ -350,8 +350,8 @@ def webhook(request, *args, **kwargs):
return HttpResponse("Invalid body, no event_type given", status=400)
if event_json['event_type'].startswith('PAYMENT.SALE.'):
- from pretix.plugins.paypal.views import webhook
- return webhook(request, *args, **kwargs)
+ logger.info(f"Received PPv1 webhook: {json.dumps(event_json)}")
+ return HttpResponse("PayPal V1 no longer supported", status=400)
# V1/V2 Sorting -- End
# We do not check the signature, we just use it as a trigger to look the charge up.
diff --git a/src/tests/plugins/paypal/__init__.py b/src/tests/plugins/paypal/__init__.py
deleted file mode 100644
index fe627f44f..000000000
--- a/src/tests/plugins/paypal/__init__.py
+++ /dev/null
@@ -1,33 +0,0 @@
-#
-# This file is part of pretix (Community Edition).
-#
-# Copyright (C) 2014-2020 Raphael Michel and contributors
-# Copyright (C) 2020-today pretix 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: Tobias Kunze
-#
-# 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.
diff --git a/src/tests/plugins/paypal/test_checkout.py b/src/tests/plugins/paypal/test_checkout.py
deleted file mode 100644
index 9eb068736..000000000
--- a/src/tests/plugins/paypal/test_checkout.py
+++ /dev/null
@@ -1,93 +0,0 @@
-#
-# This file is part of pretix (Community Edition).
-#
-# Copyright (C) 2014-2020 Raphael Michel and contributors
-# Copyright (C) 2020-today pretix 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: Tobias Kunze
-#
-# 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 datetime
-
-import pytest
-from django.utils.timezone import now
-
-from pretix.base.models import (
- CartPosition, Event, Item, ItemCategory, Organizer, Quota,
-)
-from pretix.testutils.sessions import add_cart_session, get_cart_session_key
-
-
-@pytest.fixture
-def env(client):
- orga = Organizer.objects.create(name='CCC', slug='ccc')
- event = Event.objects.create(
- organizer=orga, name='30C3', slug='30c3',
- date_from=datetime.datetime(now().year + 1, 12, 26, tzinfo=datetime.timezone.utc),
- plugins='pretix.plugins.paypal',
- live=True
- )
- category = ItemCategory.objects.create(event=event, name="Everything", position=0)
- quota_tickets = Quota.objects.create(event=event, name='Tickets', size=5)
- ticket = Item.objects.create(event=event, name='Early-bird ticket',
- category=category, default_price=23, admission=True)
- quota_tickets.items.add(ticket)
- event.settings.set('attendee_names_asked', False)
- event.settings.set('payment_paypal__enabled', True)
- event.settings.set('payment_paypal__fee_abs', 3)
- event.settings.set('payment_paypal_endpoint', 'sandbox')
- event.settings.set('payment_paypal_client_id', '12345')
- event.settings.set('payment_paypal_secret', '12345')
- add_cart_session(client, event, {'email': 'admin@localhost'})
- return client, ticket
-
-
-@pytest.mark.django_db
-def test_payment(env, monkeypatch):
- def create_payment(self, request, payment):
- assert payment['intent'] == 'sale'
- assert payment['transactions'][0]['amount']['currency'] == 'EUR'
- assert payment['transactions'][0]['amount']['total'] == '26.00'
- create_payment.called = True
- return 'https://approve.url'
- monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal._create_payment", create_payment)
-
- client, ticket = env
- session_key = get_cart_session_key(client, ticket.event)
- CartPosition.objects.create(
- event=ticket.event, cart_id=session_key, item=ticket,
- price=23, expires=now() + datetime.timedelta(minutes=10)
- )
- client.get('/%s/%s/checkout/payment/' % (ticket.event.organizer.slug, ticket.event.slug), follow=True)
- client.post('/%s/%s/checkout/questions/' % (ticket.event.organizer.slug, ticket.event.slug), {
- 'email': 'admin@localhost'
- }, follow=True)
- response = client.post('/%s/%s/checkout/payment/' % (ticket.event.organizer.slug, ticket.event.slug), {
- 'payment': 'paypal'
- })
- assert response['Location'] == 'https://approve.url'
diff --git a/src/tests/plugins/paypal/test_settings.py b/src/tests/plugins/paypal/test_settings.py
deleted file mode 100644
index e6b969c54..000000000
--- a/src/tests/plugins/paypal/test_settings.py
+++ /dev/null
@@ -1,67 +0,0 @@
-#
-# This file is part of pretix (Community Edition).
-#
-# Copyright (C) 2014-2020 Raphael Michel and contributors
-# Copyright (C) 2020-today pretix 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: Tobias Kunze
-#
-# 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 datetime
-
-import pytest
-
-from pretix.base.models import Event, Organizer, Team, User
-
-
-@pytest.fixture
-def env(client):
- orga = Organizer.objects.create(name='CCC', slug='ccc')
- event = Event.objects.create(
- organizer=orga, name='30C3', slug='30c3',
- date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc),
- plugins='pretix.plugins.paypal',
- live=True
- )
- event.settings.set('attendee_names_asked', False)
- event.settings.set('payment_paypal__enabled', True)
- user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
- t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
- t.members.add(user)
- t.limit_events.add(event)
- client.force_login(user)
- return client, event
-
-
-@pytest.mark.django_db
-def test_settings(env):
- client, event = env
- response = client.get('/control/event/%s/%s/settings/payment/paypal' % (event.organizer.slug, event.slug),
- follow=True)
- assert response.status_code == 200
- assert 'paypal__enabled' in response.rendered_content
diff --git a/src/tests/plugins/paypal/test_webhook.py b/src/tests/plugins/paypal/test_webhook.py
deleted file mode 100644
index c6dd2223c..000000000
--- a/src/tests/plugins/paypal/test_webhook.py
+++ /dev/null
@@ -1,391 +0,0 @@
-#
-# This file is part of pretix (Community Edition).
-#
-# Copyright (C) 2014-2020 Raphael Michel and contributors
-# Copyright (C) 2020-today pretix 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
-# .
-#
-import json
-from datetime import timedelta
-from decimal import Decimal
-
-import pytest
-from django.utils.timezone import now
-from django_scopes import scopes_disabled
-
-from pretix.base.models import (
- Event, Order, OrderPayment, OrderRefund, Organizer, Team, User,
-)
-from pretix.plugins.paypal.models import ReferencedPayPalObject
-
-
-@pytest.fixture
-def env():
- user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
- o = Organizer.objects.create(name='Dummy', slug='dummy')
- event = Event.objects.create(
- organizer=o, name='Dummy', slug='dummy', plugins='pretix.plugins.paypal',
- date_from=now(), live=True
- )
- t = Team.objects.create(organizer=event.organizer, all_event_permissions=True)
- t.members.add(user)
- t.limit_events.add(event)
- o1 = Order.objects.create(
- code='FOOBAR', event=event, email='dummy@dummy.test',
- status=Order.STATUS_PAID,
- datetime=now(), expires=now() + timedelta(days=10),
- total=Decimal('13.37'),
- sales_channel=o.sales_channels.get(identifier="web"),
- )
- o1.payments.create(
- amount=o1.total,
- provider='paypal',
- state=OrderPayment.PAYMENT_STATE_CONFIRMED,
- info=json.dumps({
- "id": "PAY-5YK922393D847794YKER7MUI",
- "create_time": "2013-02-19T22:01:53Z",
- "update_time": "2013-02-19T22:01:55Z",
- "state": "approved",
- "intent": "sale",
- "payer": {
- "payment_method": "credit_card",
- "funding_instruments": [
- {
- "credit_card": {
- "type": "mastercard",
- "number": "xxxxxxxxxxxx5559",
- "expire_month": 2,
- "expire_year": 2018,
- "first_name": "Betsy",
- "last_name": "Buyer"
- }
- }
- ]
- },
- "transactions": [
- {
- "amount": {
- "total": "7.47",
- "currency": "USD",
- "details": {
- "subtotal": "7.47"
- }
- },
- "description": "This is the payment transaction description.",
- "note_to_payer": "Contact us for any questions on your order.",
- "related_resources": [
- {
- "sale": {
- "id": "36C38912MN9658832",
- "create_time": "2013-02-19T22:01:53Z",
- "update_time": "2013-02-19T22:01:55Z",
- "state": "completed",
- "amount": {
- "total": "7.47",
- "currency": "USD"
- },
- "protection_eligibility": "ELIGIBLE",
- "protection_eligibility_type": "ITEM_NOT_RECEIVED_ELIGIBLE",
- "transaction_fee": {
- "value": "1.75",
- "currency": "USD"
- },
- "parent_payment": "PAY-5YK922393D847794YKER7MUI",
- "links": [
- {
- "href": "https://api.paypal.com/v1/payments/sale/36C38912MN9658832",
- "rel": "self",
- "method": "GET"
- },
- {
- "href": "https://api.paypal.com/v1/payments/sale/36C38912MN9658832/refund",
- "rel": "refund",
- "method": "POST"
- },
- {
- "href":
- "https://api.paypal.com/v1/payments/payment/PAY-5YK922393D847794YKER7MUI",
- "rel": "parent_payment",
- "method": "GET"
- }
- ]
- }
- }
- ]
- }
- ],
- "links": [
- {
- "href": "https://api.paypal.com/v1/payments/payment/PAY-5YK922393D847794YKER7MUI",
- "rel": "self",
- "method": "GET"
- }
- ]
-
- })
- )
- return event, o1
-
-
-def get_test_charge(order: Order):
- return {
- "id": "36C38912MN9658832",
- "create_time": "2013-02-19T22:01:53Z",
- "update_time": "2013-02-19T22:01:55Z",
- "state": "completed",
- "amount": {
- "total": "7.47",
- "currency": "USD"
- },
- "protection_eligibility": "ELIGIBLE",
- "protection_eligibility_type": "ITEM_NOT_RECEIVED_ELIGIBLE,UNAUTHORIZED_PAYMENT_ELIGIBLE",
- "transaction_fee": {
- "value": "1.75",
- "currency": "USD"
- },
- "parent_payment": "PAY-5YK922393D847794YKER7MUI",
- "links": [
- {
- "href": "https://api.paypal.com/v1/payments/sale/36C38912MN9658832",
- "rel": "self",
- "method": "GET"
- },
- {
- "href": "https://api.paypal.com/v1/payments/sale/36C38912MN9658832/refund",
- "rel": "refund",
- "method": "POST"
- },
- {
- "href": "https://api.paypal.com/v1/payments/payment/PAY-5YK922393D847794YKER7MUI",
- "rel": "parent_payment",
- "method": "GET"
- }
- ]
- }
-
-
-def get_test_refund(order: Order):
- return {
- 'refund_from_received_amount': {'value': '13.30', 'currency': 'EUR'},
- 'amount': {'total': '13.37', 'currency': 'EUR'},
- 'sale_id': '1G495778AR8401726',
- 'update_time': '2018-07-24T07:50:07Z',
- 'total_refunded_amount': {'value': '13.37', 'currency': 'EUR'},
- 'refund_reason_code': 'REFUND',
- 'invoice_number': 'Test',
- 'parent_payment': 'PAY-0UB50445HE155450FLNLNMUY',
- 'state': 'completed',
- 'create_time': '2018-07-24T07:50:07Z',
- 'refund_from_transaction_fee': {'value': '0.07', 'currency': 'EUR'},
- 'id': '93M41501U3542574L',
- 'refund_to_payer': {'value': '13.37', 'currency': 'EUR'},
- 'links': [
- {'method': 'GET', 'rel': 'self',
- 'href': 'https://api.sandbox.paypal.com/v1/payments/refund/93M41501U3542574L'},
- {'method': 'GET',
- 'rel': 'parent_payment',
- 'href': 'https://api.sandbox.paypal.com/v1/payments/payment/PAY-0UB50445HE155450FLNLNMUY'},
- {'method': 'GET', 'rel': 'sale',
- 'href': 'https://api.sandbox.paypal.com/v1/payments/sale/1G495778AR8401726'}
- ]
- }
-
-
-@pytest.mark.django_db
-def test_webhook_all_good(env, client, monkeypatch):
- charge = get_test_charge(env[1])
- monkeypatch.setattr("paypalrestsdk.Sale.find", lambda *args: charge)
- monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal.init_api", lambda *args: None)
-
- client.post('/dummy/dummy/paypal/webhook/', json.dumps(
- {
- "id": "WH-2WR32451HC0233532-67976317FL4543714",
- "create_time": "2014-10-23T17:23:52Z",
- "resource_type": "sale",
- "event_type": "PAYMENT.SALE.COMPLETED",
- "summary": "A successful sale payment was made for $ 0.48 USD",
- "resource": {
- "amount": {
- "total": "-0.01",
- "currency": "USD"
- },
- "id": "36C38912MN9658832",
- "parent_payment": "PAY-5YK922393D847794YKER7MUI",
- "update_time": "2014-10-31T15:41:51Z",
- "state": "completed",
- "create_time": "2014-10-31T15:41:51Z",
- "links": [],
- "sale_id": "9T0916710M1105906"
- },
- "links": [],
- "event_version": "1.0"
- }
- ), content_type='application_json')
-
- order = env[1]
- order.refresh_from_db()
- assert order.status == Order.STATUS_PAID
-
-
-@pytest.mark.django_db
-def test_webhook_mark_paid(env, client, monkeypatch):
- order = env[1]
- order.status = Order.STATUS_PENDING
- order.save()
- with scopes_disabled():
- order.payments.update(state=OrderPayment.PAYMENT_STATE_PENDING)
-
- charge = get_test_charge(env[1])
- monkeypatch.setattr("paypalrestsdk.Sale.find", lambda *args: charge)
- monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal.init_api", lambda *args: None)
- ReferencedPayPalObject.objects.create(order=order, reference="PAY-5YK922393D847794YKER7MUI")
-
- client.post('/_paypal/webhook/', json.dumps(
- {
- "id": "WH-2WR32451HC0233532-67976317FL4543714",
- "create_time": "2014-10-23T17:23:52Z",
- "resource_type": "sale",
- "event_type": "PAYMENT.SALE.COMPLETED",
- "summary": "A successful sale payment was made for $ 0.48 USD",
- "resource": {
- "amount": {
- "total": "-0.01",
- "currency": "USD"
- },
- "id": "36C38912MN9658832",
- "parent_payment": "PAY-5YK922393D847794YKER7MUI",
- "update_time": "2014-10-31T15:41:51Z",
- "state": "completed",
- "create_time": "2014-10-31T15:41:51Z",
- "links": [],
- "sale_id": "9T0916710M1105906"
- },
- "links": [],
- "event_version": "1.0"
- }
- ), content_type='application_json')
-
- order.refresh_from_db()
- assert order.status == Order.STATUS_PAID
-
-
-@pytest.mark.django_db
-def test_webhook_refund1(env, client, monkeypatch):
- order = env[1]
- charge = get_test_charge(env[1])
- charge['state'] = 'refunded'
- refund = get_test_refund(env[1])
-
- monkeypatch.setattr("paypalrestsdk.Sale.find", lambda *args: charge)
- monkeypatch.setattr("paypalrestsdk.Refund.find", lambda *args: refund)
- monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal.init_api", lambda *args: None)
- ReferencedPayPalObject.objects.create(order=order, reference="PAY-5YK922393D847794YKER7MUI")
-
- client.post('/_paypal/webhook/', json.dumps(
- {
- # Sample obtained in a sandbox webhook
- "id": "WH-9K829080KA1622327-31011919VC6498738",
- "create_time": "2017-01-15T20:15:36Z",
- "resource_type": "refund",
- "event_type": "PAYMENT.SALE.REFUNDED",
- "summary": "A EUR 255.41 EUR sale payment was refunded",
- "resource": {
- "amount": {
- "total": "255.41",
- "currency": "EUR"
- },
- "id": "75S46770PP192124D",
- "parent_payment": "PAY-5YK922393D847794YKER7MUI",
- "update_time": "2017-01-15T20:15:06Z",
- "create_time": "2017-01-15T20:14:29Z",
- "state": "completed",
- "links": [],
- "refund_to_payer": {
- "value": "255.41",
- "currency": "EUR"
- },
- "invoice_number": "",
- "refund_reason_code": "REFUND",
- "sale_id": "9T0916710M1105906"
- },
- "links": [],
- "event_version": "1.0"
- }
- ), content_type='application_json')
-
- order = env[1]
- order.refresh_from_db()
- assert order.status == Order.STATUS_PAID
-
- with scopes_disabled():
- r = order.refunds.first()
- assert r.provider == 'paypal'
- assert r.amount == order.total
- assert r.payment == order.payments.first()
- assert r.state == OrderRefund.REFUND_STATE_EXTERNAL
- assert r.source == OrderRefund.REFUND_SOURCE_EXTERNAL
-
-
-@pytest.mark.django_db
-def test_webhook_refund2(env, client, monkeypatch):
- order = env[1]
- charge = get_test_charge(env[1])
- charge['state'] = 'refunded'
- refund = get_test_refund(env[1])
-
- monkeypatch.setattr("paypalrestsdk.Sale.find", lambda *args: charge)
- monkeypatch.setattr("paypalrestsdk.Refund.find", lambda *args: refund)
- monkeypatch.setattr("pretix.plugins.paypal.payment.Paypal.init_api", lambda *args: None)
- ReferencedPayPalObject.objects.create(order=order, reference="PAY-5YK922393D847794YKER7MUI")
-
- client.post('/_paypal/webhook/', json.dumps(
- {
- # Sample obtained in the webhook simulator
- "id": "WH-2N242548W9943490U-1JU23391CS4765624",
- "create_time": "2014-10-31T15:42:24Z",
- "resource_type": "refund",
- "event_type": "PAYMENT.SALE.REFUNDED",
- "summary": "A 0.01 USD sale payment was refunded",
- "resource": {
- "amount": {
- "total": "-0.01",
- "currency": "USD"
- },
- "id": "36C38912MN9658832",
- "parent_payment": "PAY-5YK922393D847794YKER7MUI",
- "update_time": "2014-10-31T15:41:51Z",
- "state": "completed",
- "create_time": "2014-10-31T15:41:51Z",
- "links": [],
- "sale_id": "9T0916710M1105906"
- },
- "links": [],
- "event_version": "1.0"
- }
- ), content_type='application_json')
-
- order = env[1]
- order.refresh_from_db()
- assert order.status == Order.STATUS_PAID
-
- with scopes_disabled():
- r = order.refunds.first()
- assert r.provider == 'paypal'
- assert r.amount == order.total
- assert r.payment == order.payments.first()
- assert r.state == OrderRefund.REFUND_STATE_EXTERNAL
- assert r.source == OrderRefund.REFUND_SOURCE_EXTERNAL
diff --git a/src/tests/plugins/paypal2/test_webhook.py b/src/tests/plugins/paypal2/test_webhook.py
index 1b410735c..0c0f24be9 100644
--- a/src/tests/plugins/paypal2/test_webhook.py
+++ b/src/tests/plugins/paypal2/test_webhook.py
@@ -32,7 +32,7 @@ from paypalhttp.http_response import Result
from pretix.base.models import (
Event, Order, OrderPayment, OrderRefund, Organizer, Team, User,
)
-from pretix.plugins.paypal.models import ReferencedPayPalObject
+from pretix.plugins.paypal2.models import ReferencedPayPalObject
@pytest.fixture