forked from CGM_Public/pretix_original
536 lines
24 KiB
Python
536 lines
24 KiB
Python
#
|
|
# 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/>.
|
|
#
|
|
|
|
# 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 <http://www.apache.org/licenses/LICENSE-2.0>.
|
|
#
|
|
# 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 <https://github.com/pretix/pretix>.
|
|
#
|
|
# 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
|
|
|
|
from django.contrib import messages
|
|
from django.core import signing
|
|
from django.core.cache import cache
|
|
from django.db.models import Sum
|
|
from django.http import (
|
|
Http404, HttpResponse, HttpResponseBadRequest, JsonResponse,
|
|
)
|
|
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 import View
|
|
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 pretix.base.models import (
|
|
Event, Order, OrderPayment, OrderRefund, Quota, TaxRule,
|
|
)
|
|
from pretix.base.payment import PaymentException
|
|
from pretix.base.services.cart import add_payment_to_cart, get_fees
|
|
from pretix.base.settings import GlobalSettingsObject
|
|
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.paypal2.client.customer.partners_merchantintegrations_get_request import (
|
|
PartnersMerchantIntegrationsGetRequest,
|
|
)
|
|
from pretix.plugins.paypal2.payment import (
|
|
PaypalMethod, PaypalMethod as Paypal, PaypalWallet,
|
|
)
|
|
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
|
from pretix.presale.views import get_cart, get_cart_total
|
|
from pretix.presale.views.cart import cart_session
|
|
|
|
logger = logging.getLogger('pretix.plugins.paypal2')
|
|
|
|
|
|
class PaypalOrderView:
|
|
def dispatch(self, request, *args, **kwargs):
|
|
try:
|
|
self.order = request.event.orders.get_with_secret_check(
|
|
code=kwargs['order'], received_secret=kwargs['hash'].lower(), tag='plugins:paypal2:pay'
|
|
)
|
|
except Order.DoesNotExist:
|
|
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')
|
|
try:
|
|
url = signer.unsign(request.GET.get('url', ''))
|
|
except signing.BadSignature:
|
|
return HttpResponseBadRequest('Invalid parameter')
|
|
|
|
r = render(request, 'pretixplugins/paypal2/redirect.html', {
|
|
'url': url,
|
|
})
|
|
r._csp_ignore = True
|
|
return r
|
|
|
|
|
|
@method_decorator(csrf_exempt, name='dispatch')
|
|
@method_decorator(xframe_options_exempt, 'dispatch')
|
|
class XHRView(View):
|
|
|
|
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.fee 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_total = order.pending_sum + fee
|
|
else:
|
|
cart_total = get_cart_total(request)
|
|
cart_payments = cart_session(request).get('payments', [])
|
|
multi_use_cart_payments = [p for p in cart_payments if p.get('multi_use_supported')]
|
|
simulated_payments = multi_use_cart_payments + [{
|
|
'provider': 'paypal',
|
|
'multi_use_supported': False,
|
|
'min_value': None,
|
|
'max_value': None,
|
|
'info_data': {},
|
|
}]
|
|
|
|
try:
|
|
for fee in get_fees(request.event, request, cart_total, None, simulated_payments, get_cart(request)):
|
|
cart_total += fee.value
|
|
except TaxRule.SaleNotAllowed:
|
|
# ignore for now, will fail on order creation
|
|
pass
|
|
|
|
total_remaining = cart_total
|
|
for p in multi_use_cart_payments:
|
|
if p.get('min_value') and total_remaining < Decimal(p['min_value']):
|
|
continue
|
|
|
|
to_pay = total_remaining
|
|
if p.get('max_value') and to_pay > Decimal(p['max_value']):
|
|
to_pay = min(to_pay, Decimal(p['max_value']))
|
|
total_remaining -= to_pay
|
|
|
|
cart_total = total_remaining
|
|
|
|
paypal_order = prov._create_paypal_order(request, None, cart_total)
|
|
r = JsonResponse(paypal_order.dict() if paypal_order else {})
|
|
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/paypal2/pay.html', self.get_context_data())
|
|
return r
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
try:
|
|
self.payment.payment_provider.execute_payment(request, self.payment)
|
|
except PaymentException as e:
|
|
messages.error(request, str(e))
|
|
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):
|
|
messages.error(request, _('An error occurred returning from PayPal: request parameters missing. Please try again.'))
|
|
missing_getparams = set(getparams) - set(request.GET)
|
|
missing_sessionparams = {p for p in sessionparams if p not in request.session}
|
|
logger.exception('PayPal2 - Missing params in GET {} and/or Session {}'.format(missing_getparams, missing_sessionparams))
|
|
return redirect('control:index')
|
|
|
|
event = get_object_or_404(Event, pk=request.session['payment_paypal_isu_event'])
|
|
|
|
# Cached access tokens are not updated by PayPal to include new Merchants that granted access rights since
|
|
# the access token was generated. Therefor we increment the cycle count and by that invalidate the cached
|
|
# token and pull a new one.
|
|
try:
|
|
cache.incr('pretix_paypal_token_hash_cycle')
|
|
except ValueError:
|
|
cache.set('pretix_paypal_token_hash_cycle', 1, None)
|
|
|
|
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:
|
|
messages.error(request, _('An error occurred during connecting with PayPal, please try again.'))
|
|
logger.exception('PayPal2 - 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)
|
|
logger.exception('PayPal2 - Error-message in response: {}'.format(response.result.message))
|
|
else:
|
|
messages.error(request, _('An error occurred returning from PayPal: result parameters missing. Please try again.'))
|
|
missing_params = set(params) - set(response.result)
|
|
logger.exception('PayPal2 - Missing params {} in response.result'.format(missing_params))
|
|
else:
|
|
if response.result.tracking_id != request.session['payment_paypal_isu_tracking_id']:
|
|
messages.error(request, _('An error occurred returning from PayPal: session parameter not matching. Please try again.'))
|
|
logger.exception('PayPal2 - tracking_id not matching session.payment_paypal_isu_tracking_id')
|
|
elif request.GET.get("isEmailConfirmed") == "false": # Yes - literal!
|
|
messages.error(
|
|
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.')
|
|
)
|
|
else:
|
|
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
|
|
|
|
return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={
|
|
'organizer': event.organizer.slug,
|
|
'event': event.slug,
|
|
'provider': 'paypal_settings'
|
|
}))
|
|
|
|
|
|
def success(request, *args, **kwargs):
|
|
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 request.session.get('payment_paypal_oid', 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(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_oid')
|
|
urlkwargs['step'] = 'payment'
|
|
return redirect(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
|
|
|
|
if payment:
|
|
return redirect(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:
|
|
# There can only be one payment method that does not have multi_use_supported, remove all
|
|
# previous ones.
|
|
cs = cart_session(request)
|
|
cs['payments'] = [p for p in cs.get('payments', []) if p.get('multi_use_supported')]
|
|
add_payment_to_cart(request, PaypalWallet(request.event), None, None, None)
|
|
urlkwargs['step'] = 'confirm'
|
|
return redirect(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)
|
|
|
|
# V1/V2 Sorting -- Start
|
|
if 'event_type' not in event_json:
|
|
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)
|
|
# V1/V2 Sorting -- End
|
|
|
|
# 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"]:
|
|
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]
|
|
else:
|
|
payloadid = event_json['resource']['id']
|
|
|
|
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'))
|
|
|
|
rso = ReferencedPayPalObject.objects.select_related('order', 'order__event').filter(
|
|
reference__in=refs
|
|
).first()
|
|
if rso:
|
|
event = rso.order.event
|
|
else:
|
|
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:
|
|
if rso and 'id' in rso.payment.info_data:
|
|
payloadid = rso.payment.info_data['id']
|
|
sale = prov.client.execute(pp_orders.OrdersGetRequest(payloadid)).result
|
|
except IOError:
|
|
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:
|
|
# 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
|
|
|
|
if not payment:
|
|
return HttpResponse('Payment not found', status=200)
|
|
|
|
payment.order.log_action('pretix.plugins.paypal.event', data={
|
|
**event_json,
|
|
'_order_state': sale.dict(),
|
|
})
|
|
|
|
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED and sale['status'] in ('PARTIALLY_REFUNDED', 'REFUNDED', 'COMPLETED'):
|
|
if event_json['resource_type'] == 'refund':
|
|
try:
|
|
req = pp_payments.RefundsGetRequest(event_json['resource']['id'])
|
|
refund = prov.client.execute(req).result
|
|
except IOError:
|
|
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)
|
|
)
|
|
elif known_refunds.get(refund['id']).state in (
|
|
OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT) and refund['status'] == 'COMPLETED':
|
|
known_refunds.get(refund['id']).done()
|
|
|
|
if 'seller_payable_breakdown' in refund and 'total_refunded_amount' in refund['seller_payable_breakdown']:
|
|
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'])
|
|
if known_sum < total_refunded_amount:
|
|
payment.create_external_refund(
|
|
amount=total_refunded_amount - known_sum
|
|
)
|
|
elif sale['status'] == '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):
|
|
if sale['status'] == 'COMPLETED':
|
|
any_captures = False
|
|
all_captures_completed = True
|
|
for purchaseunit in sale['purchase_units']:
|
|
for capture in purchaseunit['payments']['captures']:
|
|
try:
|
|
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment,
|
|
reference=capture['id'])
|
|
except ReferencedPayPalObject.MultipleObjectsReturned:
|
|
pass
|
|
|
|
if capture['status'] not in ('COMPLETED', 'REFUNDED', 'PARTIALLY_REFUNDED'):
|
|
all_captures_completed = False
|
|
else:
|
|
any_captures = True
|
|
if any_captures and all_captures_completed:
|
|
try:
|
|
payment.info = json.dumps(sale.dict())
|
|
payment.save(update_fields=['info'])
|
|
payment.confirm()
|
|
except Quota.QuotaExceededException:
|
|
pass
|
|
elif sale['status'] == 'APPROVED':
|
|
request.session['payment_paypal_oid'] = payment.info_data['id']
|
|
try:
|
|
payment.payment_provider.execute_payment(request, payment)
|
|
except PaymentException as e:
|
|
logger.exception('PayPal2 - Could not capture/execute_payment from Webhook: {}'.format(str(e)))
|
|
|
|
return HttpResponse(status=200)
|
|
|
|
|
|
@event_permission_required('can_change_event_settings')
|
|
@require_POST
|
|
def isu_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_to_url(reverse('control:event.settings.payment.provider', kwargs={
|
|
'organizer': request.event.organizer.slug,
|
|
'event': request.event.slug,
|
|
'provider': 'paypal_settings'
|
|
}))
|
|
|
|
|
|
def get_link(links, rel):
|
|
for link in links:
|
|
if link['rel'] == rel:
|
|
return link
|
|
|
|
return None
|