Fix #369 -- Connect with PayPal (#1084)

* Connect with PayPal

* PayPal connect code-review fixes

* PayPal Connect: Global Env selection; Fix for payee-dict

* Fix missing PayPal Connect indicator for Endpoint

* Fix backwards compatibility
This commit is contained in:
Martin Gross
2018-11-21 11:14:33 +01:00
committed by Raphael Michel
parent a3489eea04
commit a3a63def55
4 changed files with 198 additions and 29 deletions

View File

@@ -9,12 +9,16 @@ from django.contrib import messages
from django.core import signing from django.core import signing
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import get_template from django.template.loader import get_template
from django.urls import reverse
from django.utils.http import urlquote
from django.utils.translation import ugettext as __, ugettext_lazy as _ from django.utils.translation import ugettext as __, ugettext_lazy as _
from paypalrestsdk.openid_connect import Tokeninfo
from pretix.base.decimal import round_decimal from pretix.base.decimal import round_decimal
from pretix.base.models import OrderPayment, OrderRefund, Quota from pretix.base.models import Event, OrderPayment, OrderRefund, Quota
from pretix.base.payment import BasePaymentProvider, PaymentException from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.services.mail import SendMailException from pretix.base.services.mail import SendMailException
from pretix.base.settings import SettingsSandbox
from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.plugins.paypal.models import ReferencedPayPalObject from pretix.plugins.paypal.models import ReferencedPayPalObject
@@ -28,19 +32,26 @@ class Paypal(BasePaymentProvider):
payment_form_fields = OrderedDict([ payment_form_fields = OrderedDict([
]) ])
def __init__(self, event: Event):
super().__init__(event)
self.settings = SettingsSandbox('payment', 'paypal', event)
@property @property
def settings_form_fields(self): def settings_form_fields(self):
d = OrderedDict( if self.settings.connect_client_id and not self.settings.secret:
[ # PayPal connect
('endpoint', if self.settings.connect_user_id:
forms.ChoiceField( fields = [
label=_('Endpoint'), ('connect_user_id',
initial='live', forms.CharField(
choices=( label=_('PayPal account'),
('live', 'Live'), disabled=True
('sandbox', 'Sandbox'), )),
), ]
)), else:
return {}
else:
fields = [
('client_id', ('client_id',
forms.CharField( forms.CharField(
label=_('Client ID'), label=_('Client ID'),
@@ -56,24 +67,76 @@ class Paypal(BasePaymentProvider):
label=_('Secret'), label=_('Secret'),
max_length=80, max_length=80,
min_length=80, min_length=80,
)) )),
] + list(super().settings_form_fields.items()) ('endpoint',
forms.ChoiceField(
label=_('Endpoint'),
initial='live',
choices=(
('live', 'Live'),
('sandbox', 'Sandbox'),
),
)),
]
d = OrderedDict(
fields + list(super().settings_form_fields.items())
) )
d.move_to_end('_enabled', False) d.move_to_end('_enabled', False)
return d 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): def settings_content_render(self, request):
return "<div class='alert alert-info'>%s<br /><code>%s</code></div>" % ( if self.settings.connect_client_id and not self.settings.secret:
_('Please configure a PayPal Webhook to the following endpoint in order to automatically cancel orders ' # Use PayPal connect
'when payments are refunded externally.'), if not self.settings.connect_user_id:
build_global_uri('plugins:paypal:webhook') return (
) "<p>{}</p>"
"<a href='{}' class='btn btn-primary btn-lg'>{}</a>"
).format(
_('To accept payments via PayPal, you will need an account at PayPal. By clicking on the '
'following button, you can either create a new PayPal account connect pretix to an existing '
'one.'),
self.get_connect_url(request),
_('Connect with {icon} PayPal').format(icon='<i class="fa fa-paypal"></i>')
)
else:
return (
"<button formaction='{}' class='btn btn-danger'>{}</button>"
).format(
reverse('plugins:paypal:oauth.disconnect', kwargs={
'organizer': self.event.organizer.slug,
'event': self.event.slug,
}),
_('Disconnect from PayPal')
)
else:
return "<div class='alert alert-info'>%s<br /><code>%s</code></div>" % (
_('Please configure a PayPal Webhook to the following endpoint in order to automatically cancel orders '
'when payments are refunded externally.'),
build_global_uri('plugins:paypal:webhook')
)
def init_api(self): def init_api(self):
paypalrestsdk.set_config( if self.settings.connect_client_id:
mode="sandbox" if "sandbox" in self.settings.get('endpoint') else 'live', paypalrestsdk.set_config(
client_id=self.settings.get('client_id'), mode="sandbox" if "sandbox" in self.settings.connect_endpoint else 'live',
client_secret=self.settings.get('secret')) 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,
openid_redirect_uri=urlquote(build_global_uri('plugins:paypal:oauth.return')))
else:
paypalrestsdk.set_config(
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): def payment_is_valid_session(self, request):
return (request.session.get('payment_paypal_id', '') != '' return (request.session.get('payment_paypal_id', '') != ''
@@ -90,6 +153,18 @@ class Paypal(BasePaymentProvider):
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs: if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace'] kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
if request.event.settings.payment_paypal_connect_user_id:
userinfo = Tokeninfo.create_with_refresh_token(request.event.settings.payment_paypal_connect_refresh_token).userinfo()
request.event.settings.payment_paypal_connect_user_id = userinfo.email
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({ payment = paypalrestsdk.Payment({
'intent': 'sale', 'intent': 'sale',
'payer': { 'payer': {
@@ -115,7 +190,8 @@ class Paypal(BasePaymentProvider):
"currency": request.event.currency, "currency": request.event.currency,
"total": self.format_price(cart['total']) "total": self.format_price(cart['total'])
}, },
"description": __('Event tickets for {event}').format(event=request.event.name) "description": __('Event tickets for {event}').format(event=request.event.name),
"payee": payee
} }
] ]
}) })
@@ -333,6 +409,19 @@ class Paypal(BasePaymentProvider):
def payment_prepare(self, request, payment_obj): def payment_prepare(self, request, payment_obj):
self.init_api() self.init_api()
if request.event.settings.payment_paypal_connect_user_id:
userinfo = Tokeninfo.create_with_refresh_token(request.event.settings.payment_paypal_connect_refresh_token).userinfo()
request.event.settings.payment_paypal_connect_user_id = userinfo.email
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({ payment = paypalrestsdk.Payment({
'intent': 'sale', 'intent': 'sale',
'payer': { 'payer': {
@@ -362,7 +451,8 @@ class Paypal(BasePaymentProvider):
"description": __('Order {order} for {event}').format( "description": __('Order {order} for {event}').format(
event=request.event.name, event=request.event.name,
order=payment_obj.order.code order=payment_obj.order.code
) ),
"payee": payee
} }
] ]
}) })

View File

@@ -1,11 +1,14 @@
import json import json
from collections import OrderedDict
from django import forms
from django.dispatch import receiver from django.dispatch import receiver
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from pretix.base.signals import ( from pretix.base.signals import (
logentry_display, register_payment_providers, requiredaction_display, logentry_display, register_global_settings, register_payment_providers,
requiredaction_display,
) )
@@ -53,3 +56,25 @@ def pretixcontrol_action_display(sender, action, request, **kwargs):
ctx = {'data': data, 'event': sender, 'action': action} ctx = {'data': data, 'event': sender, 'action': action}
return template.render(ctx, request) return template.render(ctx, request)
@receiver(register_global_settings, dispatch_uid='paypal_global_settings')
def register_global_settings(sender, **kwargs):
return OrderedDict([
('payment_paypal_connect_client_id', forms.CharField(
label=_('PayPal Connect: Client ID'),
required=False,
)),
('payment_paypal_connect_secret_key', forms.CharField(
label=_('PayPal Connect: Secret key'),
required=False,
)),
('payment_paypal_connect_endpoint', forms.ChoiceField(
label=_('PayPal Connect Endpoint'),
initial='live',
choices=(
('live', 'Live'),
('sandbox', 'Sandbox'),
),
)),
])

View File

@@ -2,7 +2,9 @@ from django.conf.urls import include, url
from pretix.multidomain import event_url from pretix.multidomain import event_url
from .views import abort, redirect_view, success, webhook from .views import (
abort, oauth_disconnect, oauth_return, redirect_view, success, webhook,
)
event_patterns = [ event_patterns = [
url(r'^paypal/', include([ url(r'^paypal/', include([
@@ -19,5 +21,8 @@ event_patterns = [
urlpatterns = [ urlpatterns = [
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/paypal/disconnect/',
oauth_disconnect, name='oauth.disconnect'),
url(r'^_paypal/webhook/$', webhook, name='webhook'), url(r'^_paypal/webhook/$', webhook, name='webhook'),
url(r'^_paypal/oauth_return/$', oauth_return, name='oauth.return'),
] ]

View File

@@ -7,14 +7,17 @@ from django.contrib import messages
from django.core import signing from django.core import signing
from django.db.models import Sum from django.db.models import Sum
from django.http import HttpResponse, HttpResponseBadRequest from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from paypalrestsdk.openid_connect import Tokeninfo
from pretix.base.models import Order, OrderPayment, OrderRefund, Quota from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.payment import PaymentException from pretix.base.payment import PaymentException
from pretix.control.permissions import event_permission_required
from pretix.multidomain.urlreverse import eventreverse from pretix.multidomain.urlreverse import eventreverse
from pretix.plugins.paypal.models import ReferencedPayPalObject from pretix.plugins.paypal.models import ReferencedPayPalObject
from pretix.plugins.paypal.payment import Paypal from pretix.plugins.paypal.payment import Paypal
@@ -37,6 +40,37 @@ def redirect_view(request, *args, **kwargs):
return r return r
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_oauth_event'])
prov = Paypal(event)
prov.init_api()
try:
tokeninfo = Tokeninfo.create(request.GET.get('code'))
userinfo = Tokeninfo.create_with_refresh_token(tokeninfo['refresh_token']).userinfo()
except:
logger.exception('Failed to obtain OAuth token')
messages.error(request, _('An error occurred during connecting with PayPal, please try again.'))
else:
messages.success(request,
_('Your PayPal account is now connected to pretix. You can change the settings in '
'detail below.'))
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'
}))
def success(request, *args, **kwargs): def success(request, *args, **kwargs):
pid = request.GET.get('paymentId') pid = request.GET.get('paymentId')
token = request.GET.get('token') token = request.GET.get('token')
@@ -201,3 +235,18 @@ def webhook(request, *args, **kwargs):
pass pass
return HttpResponse(status=200) return HttpResponse(status=200)
@event_permission_required('can_change_event_settings')
@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.'))
return redirect(reverse('control:event.settings.payment.provider', kwargs={
'organizer': request.event.organizer.slug,
'event': request.event.slug,
'provider': 'paypal'
}))