From 0e20b780c2bbbcfeba293e3f19130b7d8426070e Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 8 Sep 2026 10:06:37 +0200 Subject: [PATCH] Delete PayPal integration v1 Leaving only migrations behind --- MANIFEST.in | 2 - setup.cfg | 1 - src/pretix/plugins/paypal/__init__.py | 12 - src/pretix/plugins/paypal/api.py | 53 -- src/pretix/plugins/paypal/apps.py | 69 -- .../0005_delete_referencedpaypalobject.py | 21 + src/pretix/plugins/paypal/payment.py | 715 ------------------ src/pretix/plugins/paypal/signals.py | 31 - .../pretixplugins/paypal/paypal_logo.svg | 15 - .../paypal/checkout_payment_confirm.html | 6 - .../paypal/checkout_payment_form.html | 6 - .../pretixplugins/paypal/control.html | 18 - .../pretixplugins/paypal/pending.html | 12 - src/pretix/plugins/paypal/urls.py | 39 - src/pretix/plugins/paypal/views.py | 249 ------ .../paypal2/migrations/0001_initial.py | 54 ++ .../plugins/{paypal => paypal2}/models.py | 3 + src/pretix/plugins/paypal2/payment.py | 2 +- src/pretix/plugins/paypal2/views.py | 6 +- src/tests/plugins/paypal/__init__.py | 33 - src/tests/plugins/paypal/test_checkout.py | 93 --- src/tests/plugins/paypal/test_settings.py | 67 -- src/tests/plugins/paypal/test_webhook.py | 391 ---------- src/tests/plugins/paypal2/test_webhook.py | 2 +- 24 files changed, 83 insertions(+), 1817 deletions(-) delete mode 100644 src/pretix/plugins/paypal/api.py delete mode 100644 src/pretix/plugins/paypal/apps.py create mode 100644 src/pretix/plugins/paypal/migrations/0005_delete_referencedpaypalobject.py delete mode 100644 src/pretix/plugins/paypal/payment.py delete mode 100644 src/pretix/plugins/paypal/signals.py delete mode 100644 src/pretix/plugins/paypal/static/pretixplugins/paypal/paypal_logo.svg delete mode 100644 src/pretix/plugins/paypal/templates/pretixplugins/paypal/checkout_payment_confirm.html delete mode 100644 src/pretix/plugins/paypal/templates/pretixplugins/paypal/checkout_payment_form.html delete mode 100644 src/pretix/plugins/paypal/templates/pretixplugins/paypal/control.html delete mode 100644 src/pretix/plugins/paypal/templates/pretixplugins/paypal/pending.html delete mode 100644 src/pretix/plugins/paypal/urls.py delete mode 100644 src/pretix/plugins/paypal/views.py create mode 100644 src/pretix/plugins/paypal2/migrations/0001_initial.py rename src/pretix/plugins/{paypal => paypal2}/models.py (95%) delete mode 100644 src/tests/plugins/paypal/__init__.py delete mode 100644 src/tests/plugins/paypal/test_checkout.py delete mode 100644 src/tests/plugins/paypal/test_settings.py delete mode 100644 src/tests/plugins/paypal/test_webhook.py 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 %}

diff --git a/src/pretix/plugins/paypal/templates/pretixplugins/paypal/checkout_payment_form.html b/src/pretix/plugins/paypal/templates/pretixplugins/paypal/checkout_payment_form.html deleted file mode 100644 index 3ec589811..000000000 --- a/src/pretix/plugins/paypal/templates/pretixplugins/paypal/checkout_payment_form.html +++ /dev/null @@ -1,6 +0,0 @@ -{% load i18n %} - -

{% 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 %}

diff --git a/src/pretix/plugins/paypal/templates/pretixplugins/paypal/control.html b/src/pretix/plugins/paypal/templates/pretixplugins/paypal/control.html deleted file mode 100644 index 165c0c917..000000000 --- a/src/pretix/plugins/paypal/templates/pretixplugins/paypal/control.html +++ /dev/null @@ -1,18 +0,0 @@ -{% load i18n %} - -{% if payment_info %} -
-
{% trans "Payment ID" %}
-
{{ payment_info.id }}
-
{% trans "Sale ID" %}
-
{{ sale_id|default_if_none:"?" }}
-
{% trans "Payer" %}
-
{{ payment_info.payer.payer_info.email }}
-
{% trans "Last update" %}
-
{{ payment_info.update_time }}
-
{% trans "Total value" %}
-
{{ payment_info.transactions.0.amount.total }}
-
{% trans "Currency" %}
-
{{ payment_info.transactions.0.amount.currency }}
-
-{% endif %} diff --git a/src/pretix/plugins/paypal/templates/pretixplugins/paypal/pending.html b/src/pretix/plugins/paypal/templates/pretixplugins/paypal/pending.html deleted file mode 100644 index 7ea6214ea..000000000 --- a/src/pretix/plugins/paypal/templates/pretixplugins/paypal/pending.html +++ /dev/null @@ -1,12 +0,0 @@ -{% load i18n %} - -{% if retry %} -

{% 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