mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Revert "PayPal: Migrate to Order v2 API and ISU authentication (#2493)"
This reverts commit 9af1565db1.
This commit is contained in:
@@ -33,6 +33,7 @@
|
||||
# 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
|
||||
@@ -54,3 +55,12 @@ class PaypalApp(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
from . import signals # NOQA
|
||||
|
||||
@cached_property
|
||||
def compatibility_errors(self):
|
||||
errs = []
|
||||
try:
|
||||
import paypalrestsdk # NOQA
|
||||
except ImportError:
|
||||
errs.append("Python package 'paypalrestsdk' is not installed.")
|
||||
return errs
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# 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
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import jwt
|
||||
from paypalcheckoutsdk.core import PayPalEnvironment as VendorPayPalEnvironment
|
||||
|
||||
|
||||
class PayPalEnvironment(VendorPayPalEnvironment):
|
||||
def __init__(self, client_id, client_secret, api_url, web_url, merchant_id, partner_id):
|
||||
super(PayPalEnvironment, self).__init__(client_id, client_secret, api_url, web_url)
|
||||
self.merchant_id = merchant_id
|
||||
self.partner_id = partner_id
|
||||
|
||||
def authorization_assertation(self):
|
||||
if self.merchant_id:
|
||||
return jwt.encode(
|
||||
payload={
|
||||
'iss': self.client_id,
|
||||
'payer_id': self.merchant_id
|
||||
},
|
||||
key=None,
|
||||
algorithm=None,
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
class SandboxEnvironment(PayPalEnvironment):
|
||||
def __init__(self, client_id, client_secret, merchant_id=None, partner_id=None):
|
||||
super(SandboxEnvironment, self).__init__(
|
||||
client_id,
|
||||
client_secret,
|
||||
PayPalEnvironment.SANDBOX_API_URL,
|
||||
PayPalEnvironment.SANDBOX_WEB_URL,
|
||||
merchant_id,
|
||||
partner_id
|
||||
)
|
||||
|
||||
|
||||
class LiveEnvironment(PayPalEnvironment):
|
||||
def __init__(self, client_id, client_secret, merchant_id, partner_id):
|
||||
super(LiveEnvironment, self).__init__(
|
||||
client_id,
|
||||
client_secret,
|
||||
PayPalEnvironment.LIVE_API_URL,
|
||||
PayPalEnvironment.LIVE_WEB_URL,
|
||||
merchant_id,
|
||||
partner_id
|
||||
)
|
||||
@@ -1,70 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# 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
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import hashlib
|
||||
|
||||
from django.core.cache import cache
|
||||
from paypalcheckoutsdk.core import (
|
||||
AccessToken, PayPalHttpClient as VendorPayPalHttpClient,
|
||||
)
|
||||
|
||||
|
||||
class PayPalHttpClient(VendorPayPalHttpClient):
|
||||
def __call__(self, request):
|
||||
# First we get all the items that make up the current credentials and create a hash to detect changes
|
||||
|
||||
checksum = hashlib.sha256(''.join([
|
||||
self.environment.base_url, self.environment.client_id, self.environment.client_secret
|
||||
]).encode()).hexdigest()
|
||||
cache_key_hash = f'pretix_paypal_token_hash_{checksum}'
|
||||
token_hash = cache.get(cache_key_hash)
|
||||
|
||||
if token_hash:
|
||||
# First we set an optional access token
|
||||
self._access_token = AccessToken(
|
||||
access_token=token_hash['access_token'],
|
||||
expires_in=token_hash['expires_in'],
|
||||
token_type=token_hash['token_type'],
|
||||
)
|
||||
# This is not part of the constructor - so we need to set it after the fact.
|
||||
self._access_token.created_at = token_hash['created_at']
|
||||
|
||||
# Only then we'll call the original __call__() method, as it will verify the validity of the tokens
|
||||
# and request new ones if required.
|
||||
super().__call__(request)
|
||||
|
||||
# At this point - if there were any changes in access-token, we should have them and can cache them again
|
||||
if self._access_token and (not token_hash or token_hash['access_token'] != self._access_token.access_token):
|
||||
expiration = self._access_token.expires_in - 60 # For good measure, we expire 60 seconds earlier
|
||||
|
||||
cache.set(cache_key_hash, {
|
||||
'access_token': self._access_token.access_token,
|
||||
'expires_in': self._access_token.expires_in,
|
||||
'token_type': self._access_token.token_type,
|
||||
'created_at': self._access_token.created_at
|
||||
}, expiration)
|
||||
|
||||
# And now for some housekeeping.
|
||||
if self.environment.merchant_id:
|
||||
request.headers["PayPal-Auth-Assertion"] = self.environment.authorization_assertation()
|
||||
|
||||
if self.environment.partner_id:
|
||||
request.headers["PayPal-Partner-Attribution-Id"] = self.environment.partner_id
|
||||
@@ -1,38 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# 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
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
class PartnerReferralCreateRequest:
|
||||
"""
|
||||
Creates a Partner Referral.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.verb = "POST"
|
||||
self.path = "/v2/customer/partner-referrals?"
|
||||
self.headers = {}
|
||||
self.headers["Content-Type"] = "application/json"
|
||||
self.body = None
|
||||
|
||||
def prefer(self, prefer):
|
||||
self.headers["Prefer"] = str(prefer)
|
||||
|
||||
def request_body(self, order):
|
||||
self.body = order
|
||||
return self
|
||||
@@ -1,43 +0,0 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# 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
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
try:
|
||||
from urllib import quote # Python 2.X
|
||||
except ImportError:
|
||||
from urllib.parse import quote # Python 3+
|
||||
|
||||
|
||||
class PartnersMerchantIntegrationsGetRequest:
|
||||
"""
|
||||
Retrieves the Merchant Account Status of a Partner Merchant Integration.
|
||||
"""
|
||||
def __init__(self, partner_merchant_id, seller_merchant_id):
|
||||
self.verb = "GET"
|
||||
self.path = "/v1/customer/partners/{partner_merchant_id}/merchant-integrations/{seller_merchant_id}".format(
|
||||
partner_merchant_id=quote(str(partner_merchant_id)),
|
||||
seller_merchant_id=quote(str(seller_merchant_id))
|
||||
)
|
||||
self.headers = {}
|
||||
self.headers["Content-Type"] = "application/json"
|
||||
self.body = None
|
||||
|
||||
def prefer(self, prefer):
|
||||
self.headers["Prefer"] = str(prefer)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,27 +24,18 @@ from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.dispatch import receiver
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.template.loader import get_template
|
||||
from django.urls import resolve
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix import settings
|
||||
from pretix.base.forms import SecretKeySettingsField
|
||||
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
|
||||
from pretix.base.settings import settings_hierarkey
|
||||
from pretix.base.signals import (
|
||||
logentry_display, register_global_settings, register_payment_providers,
|
||||
)
|
||||
from pretix.plugins.paypal.payment import PaypalMethod
|
||||
from pretix.presale.signals import html_head, process_response
|
||||
|
||||
|
||||
@receiver(register_payment_providers, dispatch_uid="payment_paypal")
|
||||
def register_payment_provider(sender, **kwargs):
|
||||
from .payment import PaypalAPM, PaypalSettingsHolder, PaypalWallet
|
||||
return [PaypalSettingsHolder, PaypalWallet, PaypalAPM]
|
||||
from .payment import Paypal
|
||||
return Paypal
|
||||
|
||||
|
||||
@receiver(signal=logentry_display, dispatch_uid="paypal_logentry_display")
|
||||
@@ -61,7 +52,6 @@ def pretixcontrol_logentry_display(sender, logentry, **kwargs):
|
||||
'PAYMENT.SALE.REFUNDED': _('Payment refunded.'),
|
||||
'PAYMENT.SALE.REVERSED': _('Payment reversed.'),
|
||||
'PAYMENT.SALE.PENDING': _('Payment pending.'),
|
||||
'CHECKOUT.ORDER.APPROVED': _('Order approved.'),
|
||||
}
|
||||
|
||||
if event_type in plains:
|
||||
@@ -77,20 +67,15 @@ def pretixcontrol_logentry_display(sender, logentry, **kwargs):
|
||||
def register_global_settings(sender, **kwargs):
|
||||
return OrderedDict([
|
||||
('payment_paypal_connect_client_id', forms.CharField(
|
||||
label=_('PayPal ISU/Connect: Client ID'),
|
||||
label=_('PayPal Connect: Client ID'),
|
||||
required=False,
|
||||
)),
|
||||
('payment_paypal_connect_secret_key', SecretKeySettingsField(
|
||||
label=_('PayPal ISU/Connect: Secret key'),
|
||||
required=False,
|
||||
)),
|
||||
('payment_paypal_connect_partner_merchant_id', forms.CharField(
|
||||
label=_('PayPal ISU/Connect: Partner Merchant ID'),
|
||||
help_text=_('This is not the BN-code, but rather the ID of the merchant account which holds branding information for ISU.'),
|
||||
label=_('PayPal Connect: Secret key'),
|
||||
required=False,
|
||||
)),
|
||||
('payment_paypal_connect_endpoint', forms.ChoiceField(
|
||||
label=_('PayPal ISU/Connect Endpoint'),
|
||||
label=_('PayPal Connect Endpoint'),
|
||||
initial='live',
|
||||
choices=(
|
||||
('live', 'Live'),
|
||||
@@ -98,73 +83,3 @@ def register_global_settings(sender, **kwargs):
|
||||
),
|
||||
)),
|
||||
])
|
||||
|
||||
|
||||
@receiver(html_head, dispatch_uid="payment_paypal_html_head")
|
||||
def html_head_presale(sender, request=None, **kwargs):
|
||||
provider = PaypalMethod(sender)
|
||||
url = resolve(request.path_info)
|
||||
|
||||
if provider.settings.get('_enabled', as_type=bool) and (
|
||||
url.url_name == "event.order.pay.change" or
|
||||
(url.url_name == "event.checkout" and url.kwargs['step'] == "payment") or
|
||||
(url.namespace == "plugins:paypal" and url.url_name == "pay")
|
||||
):
|
||||
provider.init_api()
|
||||
template = get_template('pretixplugins/paypal/presale_head.html')
|
||||
|
||||
ctx = {
|
||||
'client_id': provider.client.environment.client_id,
|
||||
'merchant_id': provider.client.environment.merchant_id,
|
||||
'csp_nonce': _nonce(request),
|
||||
'debug': settings.DEBUG,
|
||||
'settings': provider.settings,
|
||||
# If we ever have more APMs that can be disabled, we should iterate over the
|
||||
# disable_method_*/enable_method*-keys
|
||||
'disable_funding': 'sepa' if provider.settings.get('disable_method_sepa', as_type=bool) else '',
|
||||
'enable_funding': 'paylater' if provider.settings.get('enable_method_paylater', as_type=bool) else ''
|
||||
}
|
||||
|
||||
return template.render(ctx)
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
@receiver(signal=process_response, dispatch_uid="payment_paypal_middleware_resp")
|
||||
def signal_process_response(sender, request: HttpRequest, response: HttpResponse, **kwargs):
|
||||
provider = PaypalMethod(sender)
|
||||
url = resolve(request.path_info)
|
||||
|
||||
if provider.settings.get('_enabled', as_type=bool) and (
|
||||
url.url_name == "event.order.pay.change" or
|
||||
(url.url_name == "event.checkout" and url.kwargs['step'] == "payment") or
|
||||
(url.namespace == "plugins:paypal" and url.url_name == "pay")
|
||||
):
|
||||
if 'Content-Security-Policy' in response:
|
||||
h = _parse_csp(response['Content-Security-Policy'])
|
||||
else:
|
||||
h = {}
|
||||
|
||||
csps = {
|
||||
'script-src': ['https://www.paypal.com', "'nonce-{}'".format(_nonce(request))],
|
||||
'frame-src': ['https://www.paypal.com', 'https://www.sandbox.paypal.com', "'nonce-{}'".format(_nonce(request))],
|
||||
'connect-src': ['https://www.paypal.com', 'https://www.sandbox.paypal.com'], # Or not - seems to only affect PayPal logging...
|
||||
'style-src': ["'nonce-{}'".format(_nonce(request))]
|
||||
}
|
||||
|
||||
_merge_csp(h, csps)
|
||||
|
||||
if h:
|
||||
response['Content-Security-Policy'] = _render_csp(h)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
settings_hierarkey.add_default('payment_paypal_debug_buyer_country', '', str)
|
||||
settings_hierarkey.add_default('payment_paypal_method_wallet', True, bool)
|
||||
|
||||
|
||||
def _nonce(request):
|
||||
if not hasattr(request, "_paypal_nonce"):
|
||||
request._paypal_nonce = get_random_string(32)
|
||||
return request._paypal_nonce
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
/*global $, paypal_client_id, paypal_loadingmessage, gettext */
|
||||
'use strict';
|
||||
|
||||
var pretixpaypal = {
|
||||
paypal: null,
|
||||
client_id: null,
|
||||
order_id: null,
|
||||
payer_id: null,
|
||||
merchant_id: null,
|
||||
currency: null,
|
||||
method: null,
|
||||
additional_disabled_funding: null,
|
||||
additional_enabled_funding: null,
|
||||
debug_buyer_country: null,
|
||||
continue_button: null,
|
||||
paypage: false,
|
||||
method_map: {
|
||||
wallet: {
|
||||
method: 'wallet',
|
||||
funding_source: 'paypal',
|
||||
//disable_funding: null,
|
||||
//enable_funding: 'paylater',
|
||||
early_auth: true,
|
||||
},
|
||||
apm: {
|
||||
method: 'apm',
|
||||
funding_source: null,
|
||||
//disable_funding: null,
|
||||
//enable_funding: null,
|
||||
early_auth: false,
|
||||
}
|
||||
},
|
||||
apm_map: {
|
||||
paypal: gettext('PayPal'),
|
||||
venmo: gettext('Venmo'),
|
||||
applepay: gettext('Apple Pay'),
|
||||
itau: gettext('Itaú'),
|
||||
credit: gettext('PayPal Credit'),
|
||||
card: gettext('Credit Card'),
|
||||
paylater: gettext('PayPal Pay Later'),
|
||||
ideal: gettext('iDEAL'),
|
||||
sepa: gettext('SEPA Direct Debit'),
|
||||
bancontact: gettext('Bancontact'),
|
||||
giropay: gettext('giropay'),
|
||||
sofort: gettext('SOFORT'),
|
||||
eps: gettext('eps'),
|
||||
mybank: gettext('MyBank'),
|
||||
p24: gettext('Przelewy24'),
|
||||
verkkopankki: gettext('Verkkopankki'),
|
||||
payu: gettext('PayU'),
|
||||
blik: gettext('BLIK'),
|
||||
trustly: gettext('Trustly'),
|
||||
zimpler: gettext('Zimpler'),
|
||||
maxima: gettext('Maxima'),
|
||||
oxxo: gettext('OXXO'),
|
||||
boleto: gettext('Boleto'),
|
||||
wechatpay: gettext('WeChat Pay'),
|
||||
mercadopago: gettext('Mercado Pago')
|
||||
},
|
||||
|
||||
load: function () {
|
||||
if (pretixpaypal.paypal === null) {
|
||||
pretixpaypal.client_id = $.trim($("#paypal_client_id").html());
|
||||
pretixpaypal.merchant_id = $.trim($("#paypal_merchant_id").html());
|
||||
pretixpaypal.debug_buyer_country = $.trim($("#paypal_buyer_country").html());
|
||||
pretixpaypal.continue_button = $('.checkout-button-row').closest("form").find(".checkout-button-row .btn-primary");
|
||||
pretixpaypal.continue_button.closest('div').append('<div id="paypal-button-container"></div>');
|
||||
pretixpaypal.additional_disabled_funding = $.trim($("#paypal_disable_funding").html());
|
||||
pretixpaypal.additional_enabled_funding = $.trim($("#paypal_enable_funding").html());
|
||||
pretixpaypal.paypage = Boolean($('#paypal-button-container').data('paypage'));
|
||||
pretixpaypal.order_id = $.trim($("#paypal_oid").html());
|
||||
pretixpaypal.currency = $("body").attr("data-currency");
|
||||
}
|
||||
|
||||
pretixpaypal.continue_button.prop("disabled", true);
|
||||
|
||||
// We are setting the cogwheel already here, as the renderAPM() method might take some time to get loaded.
|
||||
let apmtextselector = $("label[for=input_payment_paypal_apm]");
|
||||
apmtextselector.prepend('<span class="fa fa-cog fa-spin"></span> ');
|
||||
|
||||
let sdk_url = 'https://www.paypal.com/sdk/js' +
|
||||
'?client-id=' + pretixpaypal.client_id +
|
||||
'&components=buttons,funding-eligibility' +
|
||||
'¤cy=' + pretixpaypal.currency;
|
||||
|
||||
if (pretixpaypal.merchant_id) {
|
||||
sdk_url += '&merchant-id=' + pretixpaypal.merchant_id;
|
||||
}
|
||||
|
||||
if (pretixpaypal.additional_disabled_funding) {
|
||||
sdk_url += '&disable-funding=' + [pretixpaypal.additional_disabled_funding].filter(Boolean).join(',');
|
||||
}
|
||||
|
||||
if (pretixpaypal.additional_enabled_funding) {
|
||||
sdk_url += '&enable-funding=' + [pretixpaypal.additional_enabled_funding].filter(Boolean).join(',');
|
||||
}
|
||||
|
||||
if (pretixpaypal.debug_buyer_country) {
|
||||
sdk_url += '&buyer-country=' + pretixpaypal.debug_buyer_country;
|
||||
}
|
||||
|
||||
let ppscript = document.createElement('script');
|
||||
let ready = false;
|
||||
let head = document.getElementsByTagName("head")[0];
|
||||
ppscript.setAttribute('src', sdk_url);
|
||||
ppscript.setAttribute('data-csp-nonce', $.trim($("#csp_nonce").html()));
|
||||
ppscript.setAttribute('data-page-type', 'checkout');
|
||||
ppscript.setAttribute('data-partner-attribution-id', 'ramiioGmbH_Cart_PPCP');
|
||||
document.head.appendChild(ppscript);
|
||||
|
||||
ppscript.onload = ppscript.onreadystatechange = function () {
|
||||
if (!ready && (!this.readyState || this.readyState === "loaded" || this.readyState === "complete")) {
|
||||
ready = true;
|
||||
|
||||
pretixpaypal.paypal = paypal;
|
||||
|
||||
// Handle memory leak in IE
|
||||
ppscript.onload = ppscript.onreadystatechange = null;
|
||||
if (head && ppscript.parentNode) {
|
||||
head.removeChild(ppscript);
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
ready: function () {
|
||||
if ($("input[name=payment][value=paypal_apm]").length > 0) {
|
||||
pretixpaypal.renderAPMs();
|
||||
}
|
||||
|
||||
$("input[name=payment][value^='paypal']").change(function () {
|
||||
pretixpaypal.renderButton($(this).val());
|
||||
});
|
||||
|
||||
$("input[name=payment]").not("[value^='paypal']").change(function () {
|
||||
pretixpaypal.restore();
|
||||
});
|
||||
|
||||
if ($("input[name=payment][value^='paypal']").is(':checked') || $(".payment-redo-form").length) {
|
||||
pretixpaypal.renderButton($("input[name=payment][value^='paypal']:checked").val());
|
||||
}
|
||||
|
||||
if ($('#paypal-button-container').data('paypage')) {
|
||||
pretixpaypal.renderButton('paypal_apm');
|
||||
}
|
||||
},
|
||||
|
||||
restore: function () {
|
||||
// if PayPal has not been initialized, there shouldn't be anything to cleanup
|
||||
if (pretixpaypal.paypal !== null) {
|
||||
$('#paypal-button-container').empty()
|
||||
pretixpaypal.continue_button.text(gettext('Continue'));
|
||||
pretixpaypal.continue_button.show();
|
||||
pretixpaypal.continue_button.prop("disabled", false);
|
||||
}
|
||||
},
|
||||
|
||||
renderButton: function (method) {
|
||||
if (method === 'paypal') {
|
||||
method = "wallet"
|
||||
} else {
|
||||
method = method.split('paypal_').at(-1)
|
||||
}
|
||||
pretixpaypal.method = pretixpaypal.method_map[method];
|
||||
|
||||
if (pretixpaypal.method.method === 'apm' && !pretixpaypal.paypage) {
|
||||
pretixpaypal.restore();
|
||||
return;
|
||||
}
|
||||
|
||||
$('#paypal-button-container').empty()
|
||||
$('#paypal-card-container').empty()
|
||||
|
||||
let button = pretixpaypal.paypal.Buttons({
|
||||
fundingSource: pretixpaypal.method.funding_source,
|
||||
style: {
|
||||
layout: pretixpaypal.method.early_auth ? 'horizontal' : 'vertical',
|
||||
//color: 'white',
|
||||
shape: 'rect',
|
||||
label: 'pay',
|
||||
tagline: false
|
||||
},
|
||||
createOrder: function (data, actions) {
|
||||
if (pretixpaypal.order_id) {
|
||||
return pretixpaypal.order_id;
|
||||
}
|
||||
|
||||
// On the paypal:pay view, we already pregenerated the OID.
|
||||
// Since this view is also only used for APMs, we only need the XHR-calls for the Smart Payment Buttons.
|
||||
if (pretixpaypal.paypage) {
|
||||
return $("#payment_paypal_" + pretixpaypal.method.method + "_oid");
|
||||
} else {
|
||||
var xhrurl = $("#payment_paypal_" + pretixpaypal.method.method + "_xhr").val();
|
||||
}
|
||||
|
||||
return fetch(xhrurl, {
|
||||
method: 'POST'
|
||||
}).then(function (res) {
|
||||
return res.json();
|
||||
}).then(function (data) {
|
||||
return data.id;
|
||||
});
|
||||
},
|
||||
onApprove: function (data, actions) {
|
||||
waitingDialog.show(gettext("Confirming your payment …"));
|
||||
pretixpaypal.order_id = data.orderID;
|
||||
pretixpaypal.payer_id = data.payerID;
|
||||
|
||||
let method = pretixpaypal.paypage ? "wallet" : pretixpaypal.method.method;
|
||||
let selectorstub = "#payment_paypal_" + method;
|
||||
var $form = $(selectorstub + "_oid").closest("form");
|
||||
// Insert the tokens into the form so it gets submitted to the server
|
||||
$(selectorstub + "_oid").val(pretixpaypal.order_id);
|
||||
$(selectorstub + "_payer").val(pretixpaypal.payer_id);
|
||||
// and submit
|
||||
$form.get(0).submit();
|
||||
|
||||
// billingToken: null
|
||||
// facilitatorAccessToken: "A21AAL_fEu0gDD-sIXyOy65a6MjgSJJrhmxuPcxxUGnL5gW2DzTxiiAksfoC4x8hD-BjeY1LsFVKl7ceuO7UR1a9pQr8Q_AVw"
|
||||
// orderID: "7RF70259NY7589848"
|
||||
// payerID: "8M3BU92Z97VXA"
|
||||
// paymentID: null
|
||||
},
|
||||
});
|
||||
|
||||
if (button.isEligible()) {
|
||||
button.render('#paypal-button-container');
|
||||
pretixpaypal.continue_button.hide();
|
||||
} else {
|
||||
pretixpaypal.continue_button.text(gettext('Payment method unavailable'));
|
||||
pretixpaypal.continue_button.show();
|
||||
}
|
||||
},
|
||||
|
||||
renderAPMs: function () {
|
||||
pretixpaypal.restore();
|
||||
let inputselector = $("input[name=payment][value=paypal_apm]");
|
||||
// The first selector is used on the regular payment-step of the checkout flow
|
||||
// The second selector is used for the payment method change view.
|
||||
// In the long run, the layout of both pages should be adjusted to be one.
|
||||
let textselector = $("label[for=input_payment_paypal_apm]");
|
||||
let textselector2 = inputselector.next("strong");
|
||||
let eligibles = [];
|
||||
|
||||
pretixpaypal.paypal.getFundingSources().forEach(function (fundingSource) {
|
||||
// Let's always skip PayPal, since it's always a dedicated funding source
|
||||
if (fundingSource === 'paypal') {
|
||||
return;
|
||||
}
|
||||
|
||||
// This could also be paypal.Marks() - but they only expose images instead of cleartext...
|
||||
let button = pretixpaypal.paypal.Buttons({
|
||||
fundingSource: fundingSource
|
||||
});
|
||||
|
||||
if (button.isEligible()) {
|
||||
eligibles.push(gettext(pretixpaypal.apm_map[fundingSource] || fundingSource));
|
||||
}
|
||||
});
|
||||
|
||||
inputselector.attr('title', eligibles.join(', '));
|
||||
textselector.fadeOut(300, function () {
|
||||
textselector.text(eligibles.join(', '));
|
||||
textselector.fadeIn(300);
|
||||
});
|
||||
textselector2.fadeOut(300, function () {
|
||||
textselector2[0].textContent = eligibles.join(', ');
|
||||
textselector2.fadeIn(300);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$(function () {
|
||||
pretixpaypal.load();
|
||||
|
||||
(async() => {
|
||||
while(!pretixpaypal.paypal)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
pretixpaypal.ready();
|
||||
})();
|
||||
});
|
||||
@@ -1,14 +1,6 @@
|
||||
{% load i18n %}
|
||||
|
||||
<p>
|
||||
{% if method == "wallet" %}
|
||||
{% blocktrans trimmed %}
|
||||
The total amount listed above will be withdrawn from your PayPal account after the
|
||||
confirmation of your purchase.
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed %}
|
||||
After placing your order, you will be able to select your desired payment method, including PayPal.
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>{% blocktrans trimmed %}
|
||||
The total amount listed above will be withdrawn from your PayPal account after the
|
||||
confirmation of your purchase.
|
||||
{% endblocktrans %}</p>
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
{% load i18n %}
|
||||
|
||||
<p>
|
||||
{% if method == "wallet" %}
|
||||
{% blocktrans trimmed %}
|
||||
Please click the "Pay with PayPal" button below to start your payment.
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% 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 %}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<input type="hidden" name="payment_paypal_{{ method }}_oid" value="" id="payment_paypal_{{ method }}_oid" />
|
||||
<input type="hidden" name="payment_paypal_{{ method }}_payer" value="" id="payment_paypal_{{ method }}_payer" />
|
||||
<input type="hidden" name="payment_paypal_{{ method }}_xhr" value="{{ xhr }}" id="payment_paypal_{{ method }}_xhr" />
|
||||
<p>{% 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 %}</p>
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
{% if payment_info %}
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Order ID" %}</dt>
|
||||
<dt>{% trans "Payment ID" %}</dt>
|
||||
<dd>{{ payment_info.id }}</dd>
|
||||
<dt>{% trans "Status" %}</dt>
|
||||
<dd>{{ payment_info.status }}</dd>
|
||||
<dt>{% trans "Sale ID" %}</dt>
|
||||
<dd>{{ sale_id|default_if_none:"?" }}</dd>
|
||||
<dt>{% trans "Payer" %}</dt>
|
||||
<dd>{{ payment_info.payer.email_address }}</dd>
|
||||
<dd>{{ payment_info.payer.payer_info.email }}</dd>
|
||||
<dt>{% trans "Last update" %}</dt>
|
||||
<dd>{{ payment_info.purchase_units.0.payments.captures.0.update_time }}</dd>
|
||||
<dd>{{ payment_info.update_time }}</dd>
|
||||
<dt>{% trans "Total value" %}</dt>
|
||||
<dd>{{ payment_info.purchase_units.0.payments.captures.0.amount.value }}</dd>
|
||||
<dd>{{ payment_info.transactions.0.amount.total }}</dd>
|
||||
<dt>{% trans "Currency" %}</dt>
|
||||
<dd>{{ payment_info.purchase_units.0.payments.captures.0.amount.currency_code }}</dd>
|
||||
<dd>{{ payment_info.transactions.0.amount.currency }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if payment_info %}
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Payment ID" %}</dt>
|
||||
<dd>{{ payment_info.id }}</dd>
|
||||
<dt>{% trans "Sale ID" %}</dt>
|
||||
<dd>{{ sale_id|default_if_none:"?" }}</dd>
|
||||
<dt>{% trans "Payer" %}</dt>
|
||||
<dd>{{ payment_info.payer.payer_info.email }}</dd>
|
||||
<dt>{% trans "Last update" %}</dt>
|
||||
<dd>{{ payment_info.update_time }}</dd>
|
||||
<dt>{% trans "Total value" %}</dt>
|
||||
<dd>{{ payment_info.transactions.0.amount.total }}</dd>
|
||||
<dt>{% trans "Currency" %}</dt>
|
||||
<dd>{{ payment_info.transactions.0.amount.currency }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
@@ -1,45 +0,0 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Pay order" %}{% endblock %}
|
||||
{% block custom_header %}
|
||||
{{ block.super }}
|
||||
{% if oid %}
|
||||
<script type="text/plain" id="paypal_oid">{{ oid }}</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
Pay order: {{ code }}
|
||||
{% endblocktrans %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body" id="paymentcontainer">
|
||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<noscript>
|
||||
<div class="alert alert-warning">
|
||||
{% trans "Please turn on JavaScript." %}
|
||||
</div>
|
||||
</noscript>
|
||||
<p>{% trans "Please use the button/form below to complete your payment." %}</p>
|
||||
<div id="paypal-button-container" data-paypage="paypal_apm" class="text-center"></div>
|
||||
<input type="hidden" name="payment_paypal_{{ method }}_oid" value="{{ oid }}" id="payment_paypal_{{ method }}_oid" />
|
||||
<input type="hidden" name="payment_paypal_{{ method }}_payer" value="" id="payment_paypal_{{ method }}_payer" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
href="{% eventurl request.event "presale:event.order" secret=order.secret order=order.code %}">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,20 +0,0 @@
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
{% load i18n %}
|
||||
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "pretixplugins/paypal/pretix-paypal.js" %}"></script>
|
||||
{% endcompress %}
|
||||
|
||||
<script type="text/plain" id="csp_nonce">{{ csp_nonce }}</script>
|
||||
<script type="text/plain" id="paypal_client_id">{{ client_id }}</script>
|
||||
<script type="text/plain" id="paypal_merchant_id">{{ merchant_id }}</script>
|
||||
{% if disable_funding %}
|
||||
<script type="text/plain" id="paypal_disable_funding">{{ disable_funding }}</script>
|
||||
{% endif %}
|
||||
{% if enable_funding %}
|
||||
<script type="text/plain" id="paypal_enable_funding">{{ enable_funding }}</script>
|
||||
{% endif %}
|
||||
{% if debug %}
|
||||
<script type="text/plain" id="paypal_buyer_country">{{ settings.debug_buyer_country }}</script>
|
||||
{% endif %}
|
||||
@@ -24,8 +24,7 @@ from django.conf.urls import include, re_path
|
||||
from pretix.multidomain import event_url
|
||||
|
||||
from .views import (
|
||||
PayView, XHRView, abort, isu_disconnect, isu_return, redirect_view,
|
||||
success, webhook,
|
||||
abort, oauth_disconnect, oauth_return, redirect_view, success, webhook,
|
||||
)
|
||||
|
||||
event_patterns = [
|
||||
@@ -33,21 +32,17 @@ event_patterns = [
|
||||
re_path(r'^abort/$', abort, name='abort'),
|
||||
re_path(r'^return/$', success, name='return'),
|
||||
re_path(r'^redirect/$', redirect_view, name='redirect'),
|
||||
re_path(r'^xhr/$', XHRView.as_view(), name='xhr'),
|
||||
re_path(r'^pay/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[^/]+)/$', PayView.as_view(), name='pay'),
|
||||
re_path(r'^(?P<order>[^/][^w]+)/(?P<secret>[A-Za-z0-9]+)/xhr/$', XHRView.as_view(), name='xhr'),
|
||||
|
||||
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/abort/', abort, name='abort'),
|
||||
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/return/', success, name='return'),
|
||||
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/xhr/', XHRView.as_view(), name='xhr'),
|
||||
|
||||
event_url(r'^webhook/$', webhook, name='webhook', require_live=False),
|
||||
])),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/paypal/disconnect/', isu_disconnect,
|
||||
name='isu.disconnect'),
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/paypal/return/$', isu_return, name='isu.return'),
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/paypal/disconnect/',
|
||||
oauth_disconnect, name='oauth.disconnect'),
|
||||
re_path(r'^_paypal/webhook/$', webhook, name='webhook'),
|
||||
re_path(r'^_paypal/oauth_return/$', oauth_return, name='oauth.return'),
|
||||
]
|
||||
|
||||
@@ -31,73 +31,36 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
import hashlib
|
||||
|
||||
import json
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
import paypalrestsdk
|
||||
import paypalrestsdk.exceptions
|
||||
from django.contrib import messages
|
||||
from django.core import signing
|
||||
from django.db.models import Sum
|
||||
from django.http import (
|
||||
Http404, HttpResponse, HttpResponseBadRequest, JsonResponse,
|
||||
)
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.generic import TemplateView
|
||||
from django_scopes import scopes_disabled
|
||||
from paypalcheckoutsdk import orders as pp_orders, payments as pp_payments
|
||||
from paypalrestsdk.openid_connect import Tokeninfo
|
||||
|
||||
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.control.permissions import event_permission_required
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.plugins.paypal.client.customer.partners_merchantintegrations_get_request import (
|
||||
PartnersMerchantIntegrationsGetRequest,
|
||||
)
|
||||
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||
from pretix.plugins.paypal.payment import PaypalMethod, PaypalMethod as Paypal
|
||||
from pretix.presale.views import get_cart, get_cart_total
|
||||
from pretix.plugins.paypal.payment import Paypal
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.paypal')
|
||||
|
||||
|
||||
class PaypalOrderView:
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.order = request.event.orders.get(code=kwargs['order'])
|
||||
if hashlib.sha1(self.order.secret.lower().encode()).hexdigest() != kwargs['hash'].lower():
|
||||
raise Http404('Unknown order')
|
||||
except Order.DoesNotExist:
|
||||
# Do a hash comparison as well to harden timing attacks
|
||||
if 'abcdefghijklmnopq'.lower() == hashlib.sha1('abcdefghijklmnopq'.encode()).hexdigest():
|
||||
raise Http404('Unknown order')
|
||||
else:
|
||||
raise Http404('Unknown order')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@cached_property
|
||||
def payment(self):
|
||||
return get_object_or_404(
|
||||
self.order.payments,
|
||||
pk=self.kwargs['payment'],
|
||||
provider__istartswith='paypal',
|
||||
)
|
||||
|
||||
def _redirect_to_order(self):
|
||||
return redirect(eventreverse(self.request.event, 'presale:event.order', kwargs={
|
||||
'order': self.order.code,
|
||||
'secret': self.order.secret
|
||||
}) + ('?paid=yes' if self.order.status == Order.STATUS_PAID else ''))
|
||||
|
||||
|
||||
@xframe_options_exempt
|
||||
def redirect_view(request, *args, **kwargs):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
@@ -113,136 +76,40 @@ def redirect_view(request, *args, **kwargs):
|
||||
return r
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class XHRView(TemplateView):
|
||||
template_name = ''
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'order' in self.kwargs:
|
||||
order = self.request.event.orders.filter(code=self.kwargs['order']).select_related('event').first()
|
||||
if order:
|
||||
if order.secret.lower() == self.kwargs['secret'].lower():
|
||||
pass
|
||||
else:
|
||||
order = None
|
||||
else:
|
||||
order = None
|
||||
|
||||
prov = PaypalMethod(request.event)
|
||||
|
||||
if order:
|
||||
lp = order.payments.last()
|
||||
if lp and lp.state not in (OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED):
|
||||
fee = lp.fee.value - prov.calculate_fee(order.pending_sum - lp.fee.value)
|
||||
else:
|
||||
fee = prov.calculate_fee(order.pending_sum)
|
||||
|
||||
cart = {
|
||||
'positions': order.positions,
|
||||
'total': order.pending_sum,
|
||||
'fee': fee,
|
||||
}
|
||||
else:
|
||||
cart = {
|
||||
'positions': get_cart(request),
|
||||
'total': get_cart_total(request),
|
||||
'fee': prov.calculate_fee(get_cart_total(request)),
|
||||
}
|
||||
|
||||
paypal_order = prov._create_paypal_order(request, None, cart)
|
||||
r = JsonResponse(paypal_order.dict())
|
||||
r._csp_ignore = True
|
||||
return r
|
||||
|
||||
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class PayView(PaypalOrderView, TemplateView):
|
||||
template_name = ''
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if self.payment.state != OrderPayment.PAYMENT_STATE_CREATED:
|
||||
return self._redirect_to_order()
|
||||
else:
|
||||
r = render(request, 'pretixplugins/paypal/pay.html', self.get_context_data())
|
||||
return r
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.payment.payment_provider.execute_payment(request, self.payment)
|
||||
return self._redirect_to_order()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
ctx['order'] = self.order
|
||||
ctx['oid'] = self.payment.info_data['id']
|
||||
ctx['method'] = self.payment.payment_provider.method
|
||||
return ctx
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
@event_permission_required('can_change_event_settings')
|
||||
def isu_return(request, *args, **kwargs):
|
||||
getparams = ['merchantId', 'merchantIdInPayPal', 'permissionsGranted', 'accountStatus', 'consentStatus', 'productIntentID', 'isEmailConfirmed']
|
||||
sessionparams = ['payment_paypal_isu_event', 'payment_paypal_isu_tracking_id']
|
||||
if not any(k in request.GET for k in getparams) or not any(k in request.session for k in sessionparams):
|
||||
def oauth_return(request, *args, **kwargs):
|
||||
if 'payment_paypal_oauth_event' not in request.session:
|
||||
messages.error(request, _('An error occurred during connecting with PayPal, please try again.'))
|
||||
return redirect(reverse('control:index'))
|
||||
|
||||
event = get_object_or_404(Event, pk=request.session['payment_paypal_isu_event'])
|
||||
event = get_object_or_404(Event, pk=request.session['payment_paypal_oauth_event'])
|
||||
|
||||
gs = GlobalSettingsObject()
|
||||
prov = Paypal(event)
|
||||
prov.init_api()
|
||||
|
||||
try:
|
||||
req = PartnersMerchantIntegrationsGetRequest(
|
||||
gs.settings.get('payment_paypal_connect_partner_merchant_id'),
|
||||
request.GET.get('merchantIdInPayPal')
|
||||
)
|
||||
response = prov.client.execute(req)
|
||||
except IOError as e:
|
||||
tokeninfo = Tokeninfo.create(request.GET.get('code'))
|
||||
userinfo = Tokeninfo.create_with_refresh_token(tokeninfo['refresh_token']).userinfo()
|
||||
except paypalrestsdk.exceptions.ConnectionError:
|
||||
logger.exception('Failed to obtain OAuth token')
|
||||
messages.error(request, _('An error occurred during connecting with PayPal, please try again.'))
|
||||
logger.exception('PayPal PartnersMerchantIntegrationsGetRequest: {}'.format(str(e)))
|
||||
else:
|
||||
params = ['merchant_id', 'tracking_id', 'payments_receivable', 'primary_email_confirmed']
|
||||
if not any(k in response.result for k in params):
|
||||
if 'message' in response.result:
|
||||
messages.error(request, response.result.message)
|
||||
else:
|
||||
messages.error(request, _('An error occurred during connecting with PayPal, please try again.'))
|
||||
else:
|
||||
if response.result.tracking_id != request.session['payment_paypal_isu_tracking_id']:
|
||||
messages.error(request, _('An error occurred during connecting with PayPal, please try again.'))
|
||||
else:
|
||||
if request.GET.get("isEmailConfirmed") == "false": # Yes - literal!
|
||||
messages.warning(
|
||||
request,
|
||||
_('The e-mail address on your PayPal account has not yet been confirmed. You will need to do '
|
||||
'this before you can start accepting payments.')
|
||||
)
|
||||
messages.success(
|
||||
request,
|
||||
_('Your PayPal account is now connected to pretix. You can change the settings in detail below.')
|
||||
)
|
||||
messages.success(request,
|
||||
_('Your PayPal account is now connected to pretix. You can change the settings in '
|
||||
'detail below.'))
|
||||
|
||||
event.settings.payment_paypal_isu_merchant_id = response.result.merchant_id
|
||||
|
||||
# Just for good measure: Let's keep a copy of the granted scopes
|
||||
for integration in response.result.oauth_integrations:
|
||||
if integration.integration_type == 'OAUTH_THIRD_PARTY':
|
||||
for third_party in integration.oauth_third_party:
|
||||
if third_party.partner_client_id == prov.client.environment.client_id:
|
||||
event.settings.payment_paypal_isu_scopes = third_party.scopes
|
||||
event.settings.payment_paypal_connect_refresh_token = tokeninfo['refresh_token']
|
||||
event.settings.payment_paypal_connect_user_id = userinfo.email
|
||||
|
||||
return redirect(reverse('control:event.settings.payment.provider', kwargs={
|
||||
'organizer': event.organizer.slug,
|
||||
'event': event.slug,
|
||||
'provider': 'paypal_settings'
|
||||
'provider': '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
|
||||
@@ -257,7 +124,7 @@ def success(request, *args, **kwargs):
|
||||
else:
|
||||
payment = None
|
||||
|
||||
if request.session.get('payment_paypal_id', None):
|
||||
if pid == request.session.get('payment_paypal_id', None):
|
||||
if payment:
|
||||
prov = Paypal(request.event)
|
||||
try:
|
||||
@@ -311,20 +178,18 @@ def webhook(request, *args, **kwargs):
|
||||
# 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 ["checkout-order", "refund", "capture"]:
|
||||
if event_json['resource_type'] not in ('sale', 'refund'):
|
||||
return HttpResponse("Not interested in this resource type", status=200)
|
||||
|
||||
# Retrieve the Charge ID of the refunded payment
|
||||
if event_json['resource_type'] == 'refund':
|
||||
payloadid = get_link(event_json['resource']['links'], 'up')['href'].split('/')[-1]
|
||||
if event_json['resource_type'] == 'sale':
|
||||
saleid = event_json['resource']['id']
|
||||
else:
|
||||
payloadid = event_json['resource']['id']
|
||||
saleid = event_json['resource']['sale_id']
|
||||
|
||||
try:
|
||||
refs = [payloadid]
|
||||
if event_json['resource'].get('supplementary_data', {}).get('related_ids', {}).get('order_id'):
|
||||
refs.append(event_json['resource'].get('supplementary_data').get('related_ids').get('order_id'))
|
||||
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
|
||||
@@ -341,10 +206,8 @@ def webhook(request, *args, **kwargs):
|
||||
prov.init_api()
|
||||
|
||||
try:
|
||||
if rso:
|
||||
payloadid = rso.payment.info_data['id']
|
||||
sale = prov.client.execute(pp_orders.OrdersGetRequest(payloadid)).result
|
||||
except IOError:
|
||||
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)
|
||||
|
||||
@@ -355,58 +218,47 @@ def webhook(request, *args, **kwargs):
|
||||
info__icontains=sale['id'])
|
||||
payment = None
|
||||
for p in payments:
|
||||
# Legacy PayPal info-data
|
||||
if "purchase_units" not in p.info_data:
|
||||
try:
|
||||
req = pp_orders.OrdersGetRequest(p.info_data['cart'])
|
||||
response = prov.client.execute(req)
|
||||
p.info = json.dumps(response.result.dict())
|
||||
p.save(update_fields=['info'])
|
||||
p.refresh_from_db()
|
||||
except IOError:
|
||||
logger.exception('PayPal error on webhook. Event data: %s' % str(event_json))
|
||||
return HttpResponse('Could not retrieve Order Data', status=500)
|
||||
|
||||
for res in p.info_data['purchase_units'][0]['payments']['captures']:
|
||||
if res['status'] in ['COMPLETED', 'PARTIALLY_REFUNDED'] and res['id'] == sale['id']:
|
||||
payment = p
|
||||
break
|
||||
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['status'] in ('PARTIALLY_REFUNDED', 'REFUNDED', 'COMPLETED'):
|
||||
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED and sale['state'] in ('partially_refunded', 'refunded'):
|
||||
if event_json['resource_type'] == 'refund':
|
||||
try:
|
||||
req = pp_payments.RefundsGetRequest(event_json['resource']['id'])
|
||||
refund = prov.client.execute(req).result
|
||||
except IOError:
|
||||
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']['value'])),
|
||||
info=json.dumps(refund.dict() if not isinstance(refund, dict) else 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['status'] == 'COMPLETED':
|
||||
OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT) and refund['state'] == 'completed':
|
||||
known_refunds.get(refund['id']).done()
|
||||
|
||||
if 'seller_payable_breakdown' in refund and 'total_refunded_amount' in refund['seller_payable_breakdown']:
|
||||
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['seller_payable_breakdown']['total_refunded_amount']['value'])
|
||||
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['status'] == 'REFUNDED':
|
||||
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)
|
||||
@@ -417,8 +269,7 @@ def webhook(request, *args, **kwargs):
|
||||
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['status'] == 'COMPLETED':
|
||||
OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED) and sale['state'] == 'completed':
|
||||
try:
|
||||
payment.confirm()
|
||||
except Quota.QuotaExceededException:
|
||||
@@ -429,24 +280,14 @@ def webhook(request, *args, **kwargs):
|
||||
|
||||
@event_permission_required('can_change_event_settings')
|
||||
@require_POST
|
||||
def isu_disconnect(request, **kwargs):
|
||||
def oauth_disconnect(request, **kwargs):
|
||||
del request.event.settings.payment_paypal_connect_refresh_token
|
||||
del request.event.settings.payment_paypal_connect_user_id
|
||||
del request.event.settings.payment_paypal_isu_merchant_id
|
||||
del request.event.settings.payment_paypal_isu_scopes
|
||||
request.event.settings.payment_paypal__enabled = False
|
||||
messages.success(request, _('Your PayPal account has been disconnected.'))
|
||||
|
||||
return redirect(reverse('control:event.settings.payment.provider', kwargs={
|
||||
'organizer': request.event.organizer.slug,
|
||||
'event': request.event.slug,
|
||||
'provider': 'paypal_settings'
|
||||
'provider': 'paypal'
|
||||
}))
|
||||
|
||||
|
||||
def get_link(links, rel):
|
||||
for link in links:
|
||||
if link['rel'] == rel:
|
||||
return link
|
||||
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user