mirror of
https://github.com/pretix/pretix.git
synced 2026-04-23 23:22:32 +00:00
Compare commits
16 Commits
v4.10.1
...
remove-pro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
873de52ff6 | ||
|
|
ea6c698b3a | ||
|
|
d2d6a30623 | ||
|
|
68097291ca | ||
|
|
a8286f77d8 | ||
|
|
d8e96c16bb | ||
|
|
e20c2c56f0 | ||
|
|
823de60e8c | ||
|
|
25fb5fb741 | ||
|
|
017638cc29 | ||
|
|
4e37acf8d4 | ||
|
|
40d273e145 | ||
|
|
88f4ee0f95 | ||
|
|
925b8334a9 | ||
|
|
2e0be8c801 | ||
|
|
6306b8e97d |
@@ -611,12 +611,8 @@ Order position endpoints
|
||||
Tries to redeem an order position, identified by its internal ID, i.e. checks the attendee in. This endpoint
|
||||
accepts a number of optional requests in the body.
|
||||
|
||||
**Tip:** Instead of an ID, you can also use the ``secret`` field as the lookup parameter. In this case, you should
|
||||
always set ``untrusted_input=true`` as a query parameter to avoid security issues.
|
||||
**Tip:** Instead of an ID, you can also use the ``secret`` field as the lookup parameter.
|
||||
|
||||
:query boolean untrusted_input: If set to true, the lookup parameter is **always** interpreted as a ``secret``, never
|
||||
as an ``id``. This should be always set if you are passing through untrusted, scanned
|
||||
data to avoid guessing of ticket IDs.
|
||||
:<json boolean questions_supported: When this parameter is set to ``true``, handling of questions is supported. If
|
||||
you do not implement question handling in your user interface, you **must**
|
||||
set this to ``false``. In that case, questions will just be ignored. Defaults
|
||||
|
||||
@@ -474,6 +474,7 @@ Endpoints
|
||||
:query is_future: If set to ``true`` (``false``), only events that happen currently or in the future are (not) returned.
|
||||
:query is_past: If set to ``true`` (``false``), only events that are over are (not) returned.
|
||||
:query ends_after: If set to a date and time, only events that happen during of after the given time are returned.
|
||||
:query sales_channel: If set to a sales channel identifier, the response will only contain subevents from events available on this sales channel.
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# 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/>.
|
||||
#
|
||||
__version__ = "4.10.1"
|
||||
__version__ = "4.11.0.dev0"
|
||||
|
||||
@@ -409,11 +409,6 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
|
||||
nonce = self.request.data.get('nonce')
|
||||
|
||||
untrusted_input = (
|
||||
self.request.GET.get('untrusted_input', '') not in ('0', 'false', 'False', '')
|
||||
or (isinstance(self.request.auth, Device) and 'pretixscan' in (self.request.auth.software_brand or '').lower())
|
||||
)
|
||||
|
||||
if 'datetime' in self.request.data:
|
||||
dt = DateTimeField().to_internal_value(self.request.data.get('datetime'))
|
||||
else:
|
||||
@@ -434,7 +429,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
try:
|
||||
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
|
||||
if self.kwargs['pk'].isnumeric() and not untrusted_input:
|
||||
if self.kwargs['pk'].isnumeric():
|
||||
op = queryset.get(Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
|
||||
else:
|
||||
# In application/x-www-form-urlencoded, you can encodes space ' ' with '+' instead of '%20'.
|
||||
|
||||
@@ -321,6 +321,7 @@ with scopes_disabled():
|
||||
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
|
||||
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
|
||||
modified_since = django_filters.IsoDateTimeFilter(field_name='last_modified', lookup_expr='gte')
|
||||
sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs')
|
||||
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
@@ -353,6 +354,9 @@ with scopes_disabled():
|
||||
else:
|
||||
return queryset.exclude(expr)
|
||||
|
||||
def sales_channel_qs(self, queryset, name, value):
|
||||
return queryset.filter(event__sales_channels__contains=value)
|
||||
|
||||
|
||||
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = SubEventSerializer
|
||||
|
||||
@@ -1226,7 +1226,7 @@ class Event(EventMixin, LoggedModel):
|
||||
self.set_active_plugins(plugins_active)
|
||||
|
||||
plugins_available = self.get_available_plugins()
|
||||
if hasattr(plugins_available[module].app, 'uninstalled'):
|
||||
if module in plugins_available and hasattr(plugins_available[module].app, 'uninstalled'):
|
||||
getattr(plugins_available[module].app, 'uninstalled')(self)
|
||||
|
||||
regenerate_css.apply_async(args=(self.pk,))
|
||||
|
||||
@@ -84,13 +84,17 @@ def timeline_for_event(event, subevent=None):
|
||||
edit_url=ev_edit_url
|
||||
))
|
||||
|
||||
if ev.presale_end:
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=ev.presale_end,
|
||||
description=pgettext_lazy('timeline', 'End of ticket sales'),
|
||||
edit_url=ev_edit_url
|
||||
))
|
||||
tl.append(TimelineEvent(
|
||||
event=event, subevent=subevent,
|
||||
datetime=(
|
||||
ev.presale_end or ev.date_to or ev.date_from.astimezone(ev.timezone).replace(hour=23, minute=59, second=59)
|
||||
),
|
||||
description='{}{}'.format(
|
||||
pgettext_lazy('timeline', 'End of ticket sales'),
|
||||
f" ({pgettext_lazy('timeline', 'automatically because the event is over and no end of presale has been configured')})" if not ev.presale_end else ""
|
||||
),
|
||||
edit_url=ev_edit_url
|
||||
))
|
||||
|
||||
rd = event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
|
||||
if rd:
|
||||
|
||||
@@ -25768,7 +25768,7 @@ msgstr "minimale Bestellmenge: %(num)s"
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:62
|
||||
msgctxt "price"
|
||||
msgid "free"
|
||||
msgstr "kostenlos"
|
||||
msgstr "gratis"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:76
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:51
|
||||
|
||||
@@ -25718,7 +25718,7 @@ msgstr "Minimale Bestellmenge: %(num)s"
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:62
|
||||
msgctxt "price"
|
||||
msgid "free"
|
||||
msgstr "kostenlos"
|
||||
msgstr "gratis"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:76
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:51
|
||||
|
||||
@@ -56,6 +56,9 @@ class PaypalApp(AppConfig):
|
||||
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 = []
|
||||
|
||||
52
src/pretix/plugins/paypal/migrations/0003_migrate_to_v2.py
Normal file
52
src/pretix/plugins/paypal/migrations/0003_migrate_to_v2.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# Generated by Django 3.2.13 on 2022-05-25 08:39
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
def migrate_to_v2(app, schema_editor):
|
||||
GlobalSettingsObject_SettingsStore = app.get_model('pretixbase', 'GlobalSettingsObject_SettingsStore')
|
||||
EventSettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
|
||||
Event = app.get_model('pretixbase', 'Event')
|
||||
|
||||
# If there are no system-wide OAuth keys set, per-event API keys are used, and we can migrate all events
|
||||
if not GlobalSettingsObject_SettingsStore.objects.filter(
|
||||
Q(key__in=['payment_paypal_connect_client_id', 'payment_paypal_connect_secret_key'])
|
||||
& (Q(value__isnull=True) | ~Q(value=""))
|
||||
).exists():
|
||||
for ev in Event.objects.filter(plugins__contains='pretix.plugins.paypal'):
|
||||
switch_paypal_version(ev)
|
||||
|
||||
# There are system-wide OAuth keys set - so we need to check each event individually
|
||||
else:
|
||||
# Only look at events that have the PayPal plugin enabled
|
||||
for ev in Event.objects.filter(plugins__contains='pretix.plugins.paypal'):
|
||||
# If the payment method is enabled, we don't do anything
|
||||
if EventSettingsStore.objects.filter(object_id=ev.pk, key='payment_paypal__enabled', value="True").exists():
|
||||
pass
|
||||
# In all other cases, the payment method is either disabled or hasn't been set up - in this case we'll
|
||||
# migrate the event to v2
|
||||
else:
|
||||
switch_paypal_version(ev)
|
||||
|
||||
|
||||
def switch_paypal_version(event):
|
||||
plugins_active = event.plugins.split(',')
|
||||
|
||||
if 'pretix.plugins.paypal' in plugins_active:
|
||||
plugins_active.remove('pretix.plugins.paypal')
|
||||
plugins_active.append('pretix.plugins.paypal2')
|
||||
|
||||
event.plugins = ','.join(plugins_active)
|
||||
event.save(update_fields=['plugins'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('paypal', '0002_referencedpaypalobject_payment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_to_v2, migrations.RunPython.noop)
|
||||
]
|
||||
@@ -57,7 +57,6 @@ from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
|
||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||
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.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||
|
||||
@@ -171,16 +170,10 @@ class Paypal(BasePaymentProvider):
|
||||
if self.settings.connect_client_id and not self.settings.secret:
|
||||
# Use PayPal connect
|
||||
if not self.settings.connect_user_id:
|
||||
settings_content = (
|
||||
"<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>')
|
||||
)
|
||||
# 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 = (
|
||||
"<button formaction='{}' class='btn btn-danger'>{}</button>"
|
||||
@@ -192,29 +185,10 @@ class Paypal(BasePaymentProvider):
|
||||
_('Disconnect from PayPal')
|
||||
)
|
||||
else:
|
||||
settings_content = "<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')
|
||||
)
|
||||
|
||||
if self.event.currency not in SUPPORTED_CURRENCIES:
|
||||
settings_content += (
|
||||
'<br><br><div class="alert alert-warning">%s '
|
||||
'<a href="https://developer.paypal.com/docs/api/reference/currency-codes/">%s</a>'
|
||||
'</div>'
|
||||
) % (
|
||||
_("PayPal does not process payments in your event's currency."),
|
||||
_("Please check this PayPal page for a complete list of supported currencies.")
|
||||
)
|
||||
|
||||
if self.event.currency in LOCAL_ONLY_CURRENCIES:
|
||||
settings_content += '<br><br><div class="alert alert-warning">%s''</div>' % (
|
||||
_("Your event's currency is supported by PayPal as a payment and balance currency for in-country "
|
||||
"accounts only. This means, that the receiving as well as the sending PayPal account must have been "
|
||||
"created in the same country and use the same currency. Out of country accounts will not be able to "
|
||||
"send any payments.")
|
||||
)
|
||||
# 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
|
||||
|
||||
@@ -228,8 +202,8 @@ class Paypal(BasePaymentProvider):
|
||||
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=urllib.parse.quote(build_global_uri('plugins:paypal:oauth.return')))
|
||||
openid_client_secret=self.settings.connect_secret_key
|
||||
)
|
||||
else:
|
||||
paypalrestsdk.set_config(
|
||||
mode="sandbox" if "sandbox" in self.settings.get('endpoint') else 'live',
|
||||
|
||||
@@ -19,17 +19,10 @@
|
||||
# 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 json
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.forms import SecretKeySettingsField
|
||||
from pretix.base.signals import (
|
||||
logentry_display, register_global_settings, register_payment_providers,
|
||||
)
|
||||
from pretix.base.signals import logentry_display, register_payment_providers
|
||||
|
||||
|
||||
@receiver(register_payment_providers, dispatch_uid="payment_paypal")
|
||||
@@ -40,46 +33,6 @@ def register_payment_provider(sender, **kwargs):
|
||||
|
||||
@receiver(signal=logentry_display, dispatch_uid="paypal_logentry_display")
|
||||
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
|
||||
if logentry.action_type != 'pretix.plugins.paypal.event':
|
||||
return
|
||||
from pretix.plugins.paypal2.signals import pretixcontrol_logentry_display
|
||||
|
||||
data = json.loads(logentry.data)
|
||||
event_type = data.get('event_type')
|
||||
text = None
|
||||
plains = {
|
||||
'PAYMENT.SALE.COMPLETED': _('Payment completed.'),
|
||||
'PAYMENT.SALE.DENIED': _('Payment denied.'),
|
||||
'PAYMENT.SALE.REFUNDED': _('Payment refunded.'),
|
||||
'PAYMENT.SALE.REVERSED': _('Payment reversed.'),
|
||||
'PAYMENT.SALE.PENDING': _('Payment pending.'),
|
||||
}
|
||||
|
||||
if event_type in plains:
|
||||
text = plains[event_type]
|
||||
else:
|
||||
text = event_type
|
||||
|
||||
if text:
|
||||
return _('PayPal reported an event: {}').format(text)
|
||||
|
||||
|
||||
@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', SecretKeySettingsField(
|
||||
label=_('PayPal Connect: Secret key'),
|
||||
required=False,
|
||||
)),
|
||||
('payment_paypal_connect_endpoint', forms.ChoiceField(
|
||||
label=_('PayPal Connect Endpoint'),
|
||||
initial='live',
|
||||
choices=(
|
||||
('live', 'Live'),
|
||||
('sandbox', 'Sandbox'),
|
||||
),
|
||||
)),
|
||||
])
|
||||
return pretixcontrol_logentry_display(sender, logentry, **kwargs)
|
||||
|
||||
@@ -21,11 +21,7 @@
|
||||
#
|
||||
from django.conf.urls import include, re_path
|
||||
|
||||
from pretix.multidomain import event_url
|
||||
|
||||
from .views import (
|
||||
abort, oauth_disconnect, oauth_return, redirect_view, success, webhook,
|
||||
)
|
||||
from .views import abort, oauth_disconnect, redirect_view, success
|
||||
|
||||
event_patterns = [
|
||||
re_path(r'^paypal/', include([
|
||||
@@ -35,14 +31,10 @@ event_patterns = [
|
||||
|
||||
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'),
|
||||
|
||||
event_url(r'^webhook/$', webhook, name='webhook', require_live=False),
|
||||
])),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
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'),
|
||||
]
|
||||
|
||||
@@ -42,16 +42,15 @@ from django.contrib import messages
|
||||
from django.core import signing
|
||||
from django.db.models import Sum
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
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_scopes import scopes_disabled
|
||||
from paypalrestsdk.openid_connect import Tokeninfo
|
||||
|
||||
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
|
||||
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.multidomain.urlreverse import eventreverse
|
||||
@@ -76,38 +75,6 @@ def redirect_view(request, *args, **kwargs):
|
||||
return r
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
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 paypalrestsdk.exceptions.ConnectionError:
|
||||
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):
|
||||
pid = request.GET.get('paymentId')
|
||||
token = request.GET.get('token')
|
||||
@@ -286,8 +253,14 @@ def oauth_disconnect(request, **kwargs):
|
||||
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(reverse('control:event.settings.payment.provider', kwargs={
|
||||
'organizer': request.event.organizer.slug,
|
||||
'event': request.event.slug,
|
||||
'provider': 'paypal'
|
||||
'provider': 'paypal_settings'
|
||||
}))
|
||||
|
||||
21
src/pretix/plugins/paypal2/__init__.py
Normal file
21
src/pretix/plugins/paypal2/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
49
src/pretix/plugins/paypal2/apps.py
Normal file
49
src/pretix/plugins/paypal2/apps.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix import __version__ as version
|
||||
|
||||
|
||||
class Paypal2App(AppConfig):
|
||||
name = 'pretix.plugins.paypal2'
|
||||
verbose_name = "PayPal"
|
||||
|
||||
class PretixPluginMeta:
|
||||
name = "PayPal"
|
||||
author = _("the pretix team")
|
||||
version = version
|
||||
category = 'PAYMENT'
|
||||
featured = True
|
||||
picture = 'pretixplugins/paypal2/paypal_logo.svg'
|
||||
description = _("Accept payments with your PayPal account. In addition to regular PayPal payments, you can now "
|
||||
"also offer payments in a variety of local payment methods such as giropay, SOFORT, iDEAL and "
|
||||
"many more to your customers - they don't even need a 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' not in event.plugins.split(',')
|
||||
66
src/pretix/plugins/paypal2/client/core/environment.py
Normal file
66
src/pretix/plugins/paypal2/client/core/environment.py
Normal file
@@ -0,0 +1,66 @@
|
||||
#
|
||||
# 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
|
||||
)
|
||||
70
src/pretix/plugins/paypal2/client/core/paypal_http_client.py
Normal file
70
src/pretix/plugins/paypal2/client/core/paypal_http_client.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#
|
||||
# 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
|
||||
@@ -0,0 +1,38 @@
|
||||
#
|
||||
# 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
|
||||
@@ -0,0 +1,43 @@
|
||||
#
|
||||
# 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)
|
||||
0
src/pretix/plugins/paypal2/migrations/__init__.py
Normal file
0
src/pretix/plugins/paypal2/migrations/__init__.py
Normal file
978
src/pretix/plugins/paypal2/payment.py
Normal file
978
src/pretix/plugins/paypal2/payment.py
Normal file
@@ -0,0 +1,978 @@
|
||||
#
|
||||
# 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
|
||||
import json
|
||||
import logging
|
||||
import urllib.parse
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as __, gettext_lazy as _
|
||||
from django_countries import countries
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from paypalcheckoutsdk.orders import (
|
||||
OrdersCaptureRequest, OrdersCreateRequest, OrdersGetRequest,
|
||||
OrdersPatchRequest,
|
||||
)
|
||||
from paypalcheckoutsdk.payments import CapturesRefundRequest, RefundsGetRequest
|
||||
|
||||
from pretix import settings
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.forms.questions import guess_country
|
||||
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
|
||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||
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.multidomain.urlreverse import build_absolute_uri, eventreverse
|
||||
from pretix.plugins.paypal2.client.core.environment import (
|
||||
LiveEnvironment, SandboxEnvironment,
|
||||
)
|
||||
from pretix.plugins.paypal2.client.core.paypal_http_client import (
|
||||
PayPalHttpClient,
|
||||
)
|
||||
from pretix.plugins.paypal2.client.customer.partner_referral_create_request import (
|
||||
PartnerReferralCreateRequest,
|
||||
)
|
||||
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.paypal2')
|
||||
|
||||
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 PaypalSettingsHolder(BasePaymentProvider):
|
||||
identifier = 'paypal_settings'
|
||||
verbose_name = _('PayPal')
|
||||
is_enabled = False
|
||||
is_meta = True
|
||||
payment_form_fields = OrderedDict([])
|
||||
|
||||
def __init__(self, event: Event):
|
||||
super().__init__(event)
|
||||
self.settings = SettingsSandbox('payment', 'paypal', event)
|
||||
|
||||
@property
|
||||
def settings_form_fields(self):
|
||||
# ISU
|
||||
if self.settings.connect_client_id and self.settings.connect_secret_key and not self.settings.secret:
|
||||
if self.settings.isu_merchant_id:
|
||||
fields = [
|
||||
('isu_merchant_id',
|
||||
forms.CharField(
|
||||
label=_('PayPal Merchant ID'),
|
||||
disabled=True
|
||||
)),
|
||||
]
|
||||
else:
|
||||
return {}
|
||||
# Manual API integration
|
||||
else:
|
||||
fields = [
|
||||
('client_id',
|
||||
forms.CharField(
|
||||
label=_('Client ID'),
|
||||
max_length=80,
|
||||
min_length=80,
|
||||
help_text=_('<a target="_blank" rel="noopener" href="{docs_url}">{text}</a>').format(
|
||||
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',
|
||||
forms.CharField(
|
||||
label=_('Secret'),
|
||||
max_length=80,
|
||||
min_length=80,
|
||||
)),
|
||||
('endpoint',
|
||||
forms.ChoiceField(
|
||||
label=_('Endpoint'),
|
||||
initial='live',
|
||||
choices=(
|
||||
('live', 'Live'),
|
||||
('sandbox', 'Sandbox'),
|
||||
),
|
||||
)),
|
||||
]
|
||||
|
||||
methods = [
|
||||
('method_wallet',
|
||||
forms.BooleanField(
|
||||
label=_('PayPal'),
|
||||
required=False,
|
||||
help_text=_(
|
||||
'Even if a customer chooses an Alternative Payment Method, they will always have the option to '
|
||||
'revert back to paying with their PayPal account. For this reason, this payment method is always '
|
||||
'active.'
|
||||
),
|
||||
disabled=True,
|
||||
)),
|
||||
('method_apm',
|
||||
forms.BooleanField(
|
||||
label=_('Alternative Payment Methods'),
|
||||
help_text=_(
|
||||
'In addition to payments through a PayPal account, you can also offer your customers the option '
|
||||
'to pay with credit cards and other, local payment methods such as SOFORT, giropay, iDEAL, and '
|
||||
'many more - even when they do not have a PayPal account. Eligible payment methods will be '
|
||||
'determined based on the shoppers location. For German merchants, this is the direct successor '
|
||||
'of PayPal Plus.'
|
||||
),
|
||||
required=False,
|
||||
widget=forms.CheckboxInput(
|
||||
attrs={
|
||||
'data-checkbox-dependency': '#id_payment_paypal_method_wallet',
|
||||
}
|
||||
)
|
||||
)),
|
||||
('disable_method_sepa',
|
||||
forms.BooleanField(
|
||||
label=_('Disable SEPA Direct Debit'),
|
||||
help_text=_(
|
||||
'While most payment methods cannot be recalled by a customer without outlining their exact grief '
|
||||
'with the merchants, SEPA Direct Debit can be recalled with the press of a button. For that '
|
||||
'reason - and depending on the nature of your event - you might want to disabled the option of '
|
||||
'SEPA Direct Debit payments in order to reduce the risk of costly chargebacks.'
|
||||
),
|
||||
required=False,
|
||||
widget=forms.CheckboxInput(
|
||||
attrs={
|
||||
'data-checkbox-dependency': '#id_payment_paypal_method_apm',
|
||||
}
|
||||
)
|
||||
)),
|
||||
('enable_method_paylater',
|
||||
forms.BooleanField(
|
||||
label=_('Enable Buy Now Pay Later'),
|
||||
help_text=_(
|
||||
'Offer your customers the possibility to buy now (up to a certain limit) and pay in multiple installments '
|
||||
'or within 30 days. You, as the merchant, are getting your money right away.'
|
||||
),
|
||||
required=False,
|
||||
widget=forms.CheckboxInput(
|
||||
attrs={
|
||||
'data-checkbox-dependency': '#id_payment_paypal_method_apm',
|
||||
}
|
||||
)
|
||||
)),
|
||||
|
||||
]
|
||||
|
||||
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,
|
||||
)),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
allcountries = list(countries)
|
||||
allcountries.insert(0, ('', _('-- Automatic --')))
|
||||
|
||||
extra_fields.append(
|
||||
('debug_buyer_country',
|
||||
forms.ChoiceField(
|
||||
choices=allcountries,
|
||||
label=mark_safe('<span class="label label-primary">DEBUG</span> {}'.format(_('Buyer country'))),
|
||||
initial=guess_country(self.event),
|
||||
)),
|
||||
)
|
||||
|
||||
d = OrderedDict(
|
||||
fields + methods + 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 settings_content_render(self, request):
|
||||
settings_content = ""
|
||||
if self.settings.connect_client_id and self.settings.connect_secret_key and not self.settings.secret:
|
||||
# Use ISU
|
||||
if not self.settings.isu_merchant_id:
|
||||
isu_referral_url = self.get_isu_referral_url(request)
|
||||
settings_content = (
|
||||
"<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 or connect pretix to an existing '
|
||||
'one.'),
|
||||
isu_referral_url,
|
||||
'disabled' if not isu_referral_url else '',
|
||||
_('Connect with {icon} PayPal').format(icon='<i class="fa fa-paypal"></i>')
|
||||
)
|
||||
else:
|
||||
settings_content = (
|
||||
"<button formaction='{}' class='btn btn-danger'>{}</button>"
|
||||
).format(
|
||||
reverse('plugins:paypal2:isu.disconnect', kwargs={
|
||||
'organizer': self.event.organizer.slug,
|
||||
'event': self.event.slug,
|
||||
}),
|
||||
_('Disconnect from PayPal')
|
||||
)
|
||||
else:
|
||||
settings_content = "<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:paypal2:webhook')
|
||||
)
|
||||
|
||||
if self.event.currency not in SUPPORTED_CURRENCIES:
|
||||
settings_content += (
|
||||
'<br><br><div class="alert alert-warning">%s '
|
||||
'<a href="https://developer.paypal.com/docs/api/reference/currency-codes/">%s</a>'
|
||||
'</div>'
|
||||
) % (
|
||||
_("PayPal does not process payments in your event's currency."),
|
||||
_("Please check this PayPal page for a complete list of supported currencies.")
|
||||
)
|
||||
|
||||
if self.event.currency in LOCAL_ONLY_CURRENCIES:
|
||||
settings_content += '<br><br><div class="alert alert-warning">%s''</div>' % (
|
||||
_("Your event's currency is supported by PayPal as a payment and balance currency for in-country "
|
||||
"accounts only. This means, that the receiving as well as the sending PayPal account must have been "
|
||||
"created in the same country and use the same currency. Out of country accounts will not be able to "
|
||||
"send any payments.")
|
||||
)
|
||||
|
||||
return settings_content
|
||||
|
||||
def get_isu_referral_url(self, request):
|
||||
pprov = PaypalMethod(request.event)
|
||||
pprov.init_api()
|
||||
|
||||
request.session['payment_paypal_isu_event'] = request.event.pk
|
||||
request.session['payment_paypal_isu_tracking_id'] = get_random_string(length=127)
|
||||
|
||||
try:
|
||||
req = PartnerReferralCreateRequest()
|
||||
|
||||
req.request_body({
|
||||
"operations": [
|
||||
{
|
||||
"operation": "API_INTEGRATION",
|
||||
"api_integration_preference": {
|
||||
"rest_api_integration": {
|
||||
"integration_method": "PAYPAL",
|
||||
"integration_type": "THIRD_PARTY",
|
||||
"third_party_details": {
|
||||
"features": [
|
||||
"PAYMENT",
|
||||
"REFUND",
|
||||
"ACCESS_MERCHANT_INFORMATION"
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"products": [
|
||||
"EXPRESS_CHECKOUT"
|
||||
],
|
||||
"partner_config_override": {
|
||||
"partner_logo_url": urllib.parse.urljoin(settings.SITE_URL, static('pretixbase/img/pretix-logo.svg')),
|
||||
"return_url": build_global_uri('plugins:paypal2:isu.return', kwargs={
|
||||
'organizer': self.event.organizer.slug,
|
||||
'event': self.event.slug,
|
||||
})
|
||||
},
|
||||
"tracking_id": request.session['payment_paypal_isu_tracking_id'],
|
||||
"preferred_language_code": request.user.locale.split('-')[0]
|
||||
})
|
||||
response = pprov.client.execute(req)
|
||||
except IOError as e:
|
||||
messages.error(request, _('An error occurred during connecting with PayPal, please try again.'))
|
||||
logger.exception('PayPal PartnerReferralCreateRequest: {}'.format(str(e)))
|
||||
else:
|
||||
return self.get_link(response.result.links, 'action_url').href
|
||||
|
||||
def get_link(self, links, rel):
|
||||
for link in links:
|
||||
if link.rel == rel:
|
||||
return link
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class PaypalMethod(BasePaymentProvider):
|
||||
identifier = ''
|
||||
method = ''
|
||||
BN = 'ramiioGmbH_Cart_PPCP'
|
||||
|
||||
def __init__(self, event: Event):
|
||||
super().__init__(event)
|
||||
self.settings = SettingsSandbox('payment', 'paypal', event)
|
||||
|
||||
@property
|
||||
def settings_form_fields(self):
|
||||
return {}
|
||||
|
||||
@property
|
||||
def is_enabled(self) -> bool:
|
||||
return self.settings.get('_enabled', as_type=bool) and self.settings.get('method_{}'.format(self.method),
|
||||
as_type=bool)
|
||||
|
||||
@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
|
||||
|
||||
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):
|
||||
# ISU
|
||||
if self.settings.connect_client_id and not self.settings.secret:
|
||||
if 'sandbox' in self.settings.connect_endpoint:
|
||||
env = SandboxEnvironment(
|
||||
client_id=self.settings.connect_client_id,
|
||||
client_secret=self.settings.connect_secret_key,
|
||||
merchant_id=self.settings.get('isu_merchant_id', None),
|
||||
partner_id=self.BN
|
||||
)
|
||||
else:
|
||||
env = LiveEnvironment(
|
||||
client_id=self.settings.connect_client_id,
|
||||
client_secret=self.settings.connect_secret_key,
|
||||
merchant_id=self.settings.get('isu_merchant_id', None),
|
||||
partner_id=self.BN
|
||||
)
|
||||
# Manual API integration
|
||||
else:
|
||||
if 'sandbox' in self.settings.get('endpoint'):
|
||||
env = SandboxEnvironment(
|
||||
client_id=self.settings.get('client_id'),
|
||||
client_secret=self.settings.get('secret'),
|
||||
merchant_id=None,
|
||||
partner_id=self.BN
|
||||
)
|
||||
else:
|
||||
env = LiveEnvironment(
|
||||
client_id=self.settings.get('client_id'),
|
||||
client_secret=self.settings.get('secret'),
|
||||
merchant_id=None,
|
||||
partner_id=self.BN
|
||||
)
|
||||
|
||||
self.client = PayPalHttpClient(env)
|
||||
|
||||
def payment_is_valid_session(self, request):
|
||||
return request.session.get('payment_paypal_oid', '') != ''
|
||||
|
||||
def payment_form_render(self, request) -> str:
|
||||
def build_kwargs():
|
||||
keys = ['organizer', 'event', 'order', 'secret', 'cart_namespace']
|
||||
kwargs = {}
|
||||
for key in keys:
|
||||
if key in request.resolver_match.kwargs:
|
||||
kwargs[key] = request.resolver_match.kwargs[key]
|
||||
return kwargs
|
||||
|
||||
template = get_template('pretixplugins/paypal2/checkout_payment_form.html')
|
||||
ctx = {
|
||||
'request': request,
|
||||
'event': self.event,
|
||||
'settings': self.settings,
|
||||
'method': self.method,
|
||||
'xhr': eventreverse(self.event, 'plugins:paypal2:xhr', kwargs=build_kwargs())
|
||||
}
|
||||
return template.render(ctx)
|
||||
|
||||
def checkout_prepare(self, request, cart):
|
||||
paypal_order_id = request.POST.get('payment_paypal_{}_oid'.format(self.method), None)
|
||||
|
||||
# PayPal OID has been previously generated through XHR and onApprove() has fired
|
||||
if paypal_order_id and paypal_order_id == request.session.get('payment_paypal_oid', None):
|
||||
self.init_api()
|
||||
|
||||
try:
|
||||
req = OrdersGetRequest(paypal_order_id)
|
||||
response = self.client.execute(req)
|
||||
except IOError as e:
|
||||
messages.warning(request, _('We had trouble communicating with PayPal'))
|
||||
logger.exception('PayPal OrdersGetRequest: {}'.format(str(e)))
|
||||
return False
|
||||
else:
|
||||
if response.result.status == 'APPROVED':
|
||||
return True
|
||||
messages.warning(request, _('Something went wrong when requesting the payment status. Please try again.'))
|
||||
return False
|
||||
# onApprove has fired, but we don't have a matching OID in the session - manipulation/something went wrong.
|
||||
elif paypal_order_id:
|
||||
messages.warning(request, _('We had trouble communicating with PayPal'))
|
||||
return False
|
||||
else:
|
||||
# We don't have an XHR-generated OID, nor a onApprove-fired OID.
|
||||
# Probably no active JavaScript; this won't work
|
||||
messages.warning(request, _('You may need to enable JavaScript for PayPal payments.'))
|
||||
return False
|
||||
|
||||
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_paypal_order(self, request, payment=None, cart=None):
|
||||
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']
|
||||
|
||||
# ISU
|
||||
if request.event.settings.payment_paypal_isu_merchant_id:
|
||||
payee = {
|
||||
"merchant_id": request.event.settings.payment_paypal_isu_merchant_id,
|
||||
}
|
||||
# Manual API integration
|
||||
else:
|
||||
payee = {}
|
||||
|
||||
if payment and not cart:
|
||||
value = self.format_price(payment.amount)
|
||||
currency = payment.order.event.currency
|
||||
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.order.code
|
||||
),
|
||||
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||
)
|
||||
custom_id = '{prefix}{slug}-{code}{postfix}'.format(
|
||||
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
|
||||
slug=self.event.slug.upper(),
|
||||
code=payment.order.code,
|
||||
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||
)
|
||||
request.session['payment_paypal_payment'] = payment.pk
|
||||
elif cart and not payment:
|
||||
value = self.format_price(cart['cart_total'] + cart['cart_fees'] + cart['payment_fee'])
|
||||
currency = request.event.currency
|
||||
description = __('Event tickets for {event}').format(event=request.event.name)
|
||||
custom_id = '{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
|
||||
else:
|
||||
pass
|
||||
|
||||
try:
|
||||
paymentreq = OrdersCreateRequest()
|
||||
paymentreq.request_body({
|
||||
'intent': 'CAPTURE',
|
||||
# 'payer': {}, # We could transmit PII (email, name, address, etc.)
|
||||
'purchase_units': [{
|
||||
'amount': {
|
||||
'currency_code': currency,
|
||||
'value': value,
|
||||
},
|
||||
'payee': payee,
|
||||
'description': description,
|
||||
'custom_id': custom_id,
|
||||
# 'shipping': {}, # Include Shipping information?
|
||||
}],
|
||||
'application_context': {
|
||||
'locale': request.LANGUAGE_CODE.split('-')[0],
|
||||
'shipping_preference': 'NO_SHIPPING', # 'SET_PROVIDED_ADDRESS', # Do not set on non-ship order?
|
||||
'user_action': 'CONTINUE',
|
||||
'return_url': build_absolute_uri(request.event, 'plugins:paypal2:return', kwargs=kwargs),
|
||||
'cancel_url': build_absolute_uri(request.event, 'plugins:paypal2:abort', kwargs=kwargs),
|
||||
},
|
||||
})
|
||||
response = self.client.execute(paymentreq)
|
||||
except IOError as e:
|
||||
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||
logger.exception('PayPal OrdersCreateRequest: {}'.format(str(e)))
|
||||
else:
|
||||
if response.result.status not in ('CREATED', 'PAYER_ACTION_REQUIRED'):
|
||||
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||
logger.error('Invalid payment state: ' + str(paymentreq))
|
||||
return
|
||||
|
||||
request.session['payment_paypal_oid'] = response.result.id
|
||||
return response.result
|
||||
|
||||
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/paypal2/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):
|
||||
try:
|
||||
if request.session.get('payment_paypal_oid', '') == '':
|
||||
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
|
||||
'proceed.'))
|
||||
|
||||
self.init_api()
|
||||
try:
|
||||
req = OrdersGetRequest(request.session.get('payment_paypal_oid'))
|
||||
response = self.client.execute(req)
|
||||
except IOError as e:
|
||||
logger.exception('PayPal OrdersGetRequest: {}'.format(str(e)))
|
||||
raise PaymentException(_('We had trouble communicating with PayPal'))
|
||||
else:
|
||||
pp_captured_order = response.result
|
||||
|
||||
try:
|
||||
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=pp_captured_order.id)
|
||||
except ReferencedPayPalObject.MultipleObjectsReturned:
|
||||
pass
|
||||
if str(pp_captured_order.purchase_units[0].amount.value) != str(payment.amount) or \
|
||||
pp_captured_order.purchase_units[0].amount.currency_code != self.event.currency:
|
||||
logger.error('Value mismatch: Payment %s vs paypal trans %s' % (payment.id, str(pp_captured_order)))
|
||||
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
|
||||
'proceed.'))
|
||||
|
||||
if pp_captured_order.status == 'APPROVED':
|
||||
try:
|
||||
patchreq = OrdersPatchRequest(pp_captured_order.id)
|
||||
patchreq.request_body([
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/purchase_units/@reference_id=='default'/custom_id",
|
||||
"value": '{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.order.code
|
||||
),
|
||||
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||
),
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/purchase_units/@reference_id=='default'/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.order.code
|
||||
),
|
||||
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
|
||||
),
|
||||
}
|
||||
])
|
||||
self.client.execute(patchreq)
|
||||
except IOError as e:
|
||||
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||
logger.exception('PayPal OrdersPatchRequest: {}'.format(str(e)))
|
||||
return
|
||||
|
||||
try:
|
||||
capturereq = OrdersCaptureRequest(pp_captured_order.id)
|
||||
response = self.client.execute(capturereq)
|
||||
except IOError as e:
|
||||
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||
logger.exception('PayPal OrdersCaptureRequest: {}'.format(str(e)))
|
||||
return
|
||||
else:
|
||||
pp_captured_order = response.result
|
||||
|
||||
for purchaseunit in pp_captured_order.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 == 'PENDING':
|
||||
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as '
|
||||
'soon as the payment completed.'))
|
||||
payment.info = json.dumps(pp_captured_order.dict())
|
||||
payment.state = OrderPayment.PAYMENT_STATE_PENDING
|
||||
payment.save()
|
||||
return
|
||||
|
||||
payment.refresh_from_db()
|
||||
|
||||
if pp_captured_order.status != 'COMPLETED':
|
||||
payment.fail(info=pp_captured_order.dict())
|
||||
logger.error('Invalid state: %s' % str(pp_captured_order))
|
||||
raise PaymentException(
|
||||
_('We were unable to process your payment. See below for details on how to proceed.')
|
||||
)
|
||||
|
||||
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
|
||||
logger.warning('PayPal success event even though order is already marked as paid')
|
||||
return
|
||||
|
||||
try:
|
||||
payment.info = json.dumps(pp_captured_order.dict())
|
||||
payment.save(update_fields=['info'])
|
||||
payment.confirm()
|
||||
except Quota.QuotaExceededException as e:
|
||||
raise PaymentException(str(e))
|
||||
|
||||
except SendMailException:
|
||||
messages.warning(request, _('There was an error sending the confirmation mail.'))
|
||||
finally:
|
||||
del request.session['payment_paypal_oid']
|
||||
|
||||
def payment_pending_render(self, request, payment) -> str:
|
||||
retry = True
|
||||
try:
|
||||
if payment.info and payment.info_data['state'] == 'pending':
|
||||
retry = False
|
||||
except KeyError:
|
||||
pass
|
||||
template = get_template('pretixplugins/paypal2/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
|
||||
|
||||
# Legacy PayPal info-data
|
||||
if 'purchase_units' not in payment.info_data:
|
||||
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']
|
||||
else:
|
||||
for trans in payment.info_data.get('purchase_units', []):
|
||||
for res in trans.get('payments', {}).get('captures', []):
|
||||
sale_id = res['id']
|
||||
|
||||
return sale_id or payment.info_data.get('id', None)
|
||||
|
||||
def api_payment_details(self, payment: OrderPayment):
|
||||
sale_id = None
|
||||
|
||||
# Legacy PayPal info-data
|
||||
if 'purchase_units' not in payment.info_data:
|
||||
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,
|
||||
}
|
||||
else:
|
||||
for trans in payment.info_data.get('purchase_units', []):
|
||||
for res in trans.get('payments', {}).get('captures', []):
|
||||
sale_id = res['id']
|
||||
|
||||
return {
|
||||
"payer_email": payment.info_data.get('payer', {}).get('email_address'),
|
||||
"payer_id": payment.info_data.get('payer', {}).get('payer_id'),
|
||||
"cart_id": payment.info_data.get('id', None),
|
||||
"payment_id": sale_id,
|
||||
"sale_id": sale_id,
|
||||
}
|
||||
|
||||
def payment_control_render(self, request: HttpRequest, payment: OrderPayment):
|
||||
# Legacy PayPal info-data
|
||||
if 'purchase_units' not in payment.info_data:
|
||||
template = get_template('pretixplugins/paypal2/control_legacy.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}
|
||||
else:
|
||||
template = get_template('pretixplugins/paypal2/control.html')
|
||||
ctx = {'request': request, 'event': self.event, 'settings': self.settings,
|
||||
'payment_info': payment.info_data, 'order': payment.order}
|
||||
|
||||
return template.render(ctx)
|
||||
|
||||
def payment_control_render_short(self, payment: OrderPayment) -> str:
|
||||
# Legacy PayPal info-data
|
||||
if 'purchase_units' not in payment.info_data:
|
||||
return payment.info_data.get('payer', {}).get('payer_info', {}).get('email', '')
|
||||
else:
|
||||
return '{} / {}'.format(
|
||||
payment.info_data.get('id', ''),
|
||||
payment.info_data.get('payer', {}).get('email_address', '')
|
||||
)
|
||||
|
||||
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:
|
||||
pp_payment = None
|
||||
payment_info_data = None
|
||||
# Legacy PayPal - get up to date info data first
|
||||
if "purchase_units" not in refund.payment.info_data:
|
||||
req = OrdersGetRequest(refund.payment.info_data['cart'])
|
||||
response = self.client.execute(req)
|
||||
payment_info_data = response.result.dict()
|
||||
else:
|
||||
payment_info_data = refund.payment.info_data
|
||||
|
||||
for res in payment_info_data['purchase_units'][0]['payments']['captures']:
|
||||
if res['status'] in ['COMPLETED', 'PARTIALLY_REFUNDED']:
|
||||
pp_payment = res['id']
|
||||
break
|
||||
|
||||
if not pp_payment:
|
||||
req = OrdersGetRequest(payment_info_data['id'])
|
||||
response = self.client.execute(req)
|
||||
for res in response.result.purchase_units[0].payments.captures:
|
||||
if res['status'] in ['COMPLETED', 'PARTIALLY_REFUNDED']:
|
||||
pp_payment = res.id
|
||||
break
|
||||
|
||||
req = CapturesRefundRequest(pp_payment)
|
||||
req.request_body({
|
||||
"amount": {
|
||||
"value": self.format_price(refund.amount),
|
||||
"currency_code": refund.order.event.currency
|
||||
}
|
||||
})
|
||||
response = self.client.execute(req)
|
||||
except IOError as e:
|
||||
refund.order.log_action('pretix.event.order.refund.failed', {
|
||||
'local_id': refund.local_id,
|
||||
'provider': refund.provider,
|
||||
'error': str(e)
|
||||
})
|
||||
logger.error('execute_refund: {}'.format(str(e)))
|
||||
raise PaymentException(_('Refunding the amount via PayPal failed: {}').format(str(e)))
|
||||
|
||||
refund.info = json.dumps(response.result.dict())
|
||||
refund.save(update_fields=['info'])
|
||||
|
||||
req = RefundsGetRequest(response.result.id)
|
||||
response = self.client.execute(req)
|
||||
refund.info = json.dumps(response.result.dict())
|
||||
refund.save(update_fields=['info'])
|
||||
|
||||
if response.result.status == 'COMPLETED':
|
||||
refund.done()
|
||||
elif response.result.status == 'PENDING':
|
||||
refund.state = OrderRefund.REFUND_STATE_TRANSIT
|
||||
refund.save(update_fields=['state'])
|
||||
else:
|
||||
refund.order.log_action('pretix.event.order.refund.failed', {
|
||||
'local_id': refund.local_id,
|
||||
'provider': refund.provider,
|
||||
'error': str(response.result.status_details.reason)
|
||||
})
|
||||
raise PaymentException(_('Refunding the amount via PayPal failed: {}').format(response.result.status_details.reason))
|
||||
|
||||
def payment_prepare(self, request, payment):
|
||||
paypal_order_id = request.POST.get('payment_paypal_{}_oid'.format(self.method), None)
|
||||
|
||||
# PayPal OID has been previously generated through XHR and onApprove() has fired
|
||||
if paypal_order_id and paypal_order_id == request.session.get('payment_paypal_oid', None):
|
||||
self.init_api()
|
||||
|
||||
try:
|
||||
req = OrdersGetRequest(paypal_order_id)
|
||||
response = self.client.execute(req)
|
||||
except IOError as e:
|
||||
messages.warning(request, _('We had trouble communicating with PayPal'))
|
||||
logger.exception('PayPal OrdersGetRequest: {}'.format(str(e)))
|
||||
return False
|
||||
else:
|
||||
if response.result.status == 'APPROVED':
|
||||
return True
|
||||
messages.warning(request, _('Something went wrong when requesting the payment status. Please try again.'))
|
||||
return False
|
||||
# onApprove has fired, but we don't have a matching OID in the session - manipulation/something went wrong.
|
||||
elif paypal_order_id:
|
||||
messages.warning(request, _('We had trouble communicating with PayPal'))
|
||||
return False
|
||||
else:
|
||||
# We don't have an XHR-generated OID, nor a onApprove-fired OID.
|
||||
# Probably no active JavaScript; this won't work
|
||||
messages.warning(request, _('You may need to enable JavaScript for PayPal payments.'))
|
||||
return False
|
||||
|
||||
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='')
|
||||
|
||||
|
||||
class PaypalWallet(PaypalMethod):
|
||||
identifier = 'paypal'
|
||||
verbose_name = _('PayPal')
|
||||
public_name = _('PayPal')
|
||||
method = 'wallet'
|
||||
|
||||
|
||||
class PaypalAPM(PaypalMethod):
|
||||
identifier = 'paypal_apm'
|
||||
verbose_name = _('PayPal APM')
|
||||
public_name = _('PayPal Alternative Payment Methods')
|
||||
method = 'apm'
|
||||
|
||||
def payment_is_valid_session(self, request):
|
||||
# Since APMs request the OID by XHR at a later point, no need to check anything here
|
||||
return True
|
||||
|
||||
def checkout_prepare(self, request, cart):
|
||||
return True
|
||||
|
||||
def payment_prepare(self, request, payment):
|
||||
return True
|
||||
|
||||
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
|
||||
# This is a workaround to not have APMs be written to the database with identifier paypal_apm.
|
||||
# Since all transactions - APM or not - look the same and are handled the same, we want to keep all PayPal
|
||||
# transactions under the "paypal"-identifier - no matter what the customer might have selected.
|
||||
payment.provider = "paypal"
|
||||
payment.save(update_fields=["provider"])
|
||||
|
||||
paypal_order = self._create_paypal_order(request, payment, None)
|
||||
payment.info = json.dumps(paypal_order.dict())
|
||||
payment.save(update_fields=['info'])
|
||||
|
||||
return eventreverse(self.event, 'plugins:paypal2:pay', kwargs={
|
||||
'order': payment.order.code,
|
||||
'payment': payment.pk,
|
||||
'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(),
|
||||
})
|
||||
171
src/pretix/plugins/paypal2/signals.py
Normal file
171
src/pretix/plugins/paypal2/signals.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#
|
||||
# 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 json
|
||||
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.paypal2.payment import PaypalMethod
|
||||
from pretix.presale.signals import html_head, process_response
|
||||
|
||||
|
||||
@receiver(register_payment_providers, dispatch_uid="payment_paypal2")
|
||||
def register_payment_provider(sender, **kwargs):
|
||||
from .payment import PaypalAPM, PaypalSettingsHolder, PaypalWallet
|
||||
return [PaypalSettingsHolder, PaypalWallet, PaypalAPM]
|
||||
|
||||
|
||||
@receiver(signal=logentry_display, dispatch_uid="paypal2_logentry_display")
|
||||
def pretixcontrol_logentry_display(sender, logentry, **kwargs):
|
||||
if logentry.action_type != 'pretix.plugins.paypal.event':
|
||||
return
|
||||
|
||||
data = json.loads(logentry.data)
|
||||
event_type = data.get('event_type')
|
||||
text = None
|
||||
plains = {
|
||||
'PAYMENT.SALE.COMPLETED': _('Payment completed.'),
|
||||
'PAYMENT.SALE.DENIED': _('Payment denied.'),
|
||||
'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:
|
||||
text = plains[event_type]
|
||||
else:
|
||||
text = event_type
|
||||
|
||||
if text:
|
||||
return _('PayPal reported an event: {}').format(text)
|
||||
|
||||
|
||||
@receiver(register_global_settings, dispatch_uid='paypal2_global_settings')
|
||||
def register_global_settings(sender, **kwargs):
|
||||
return OrderedDict([
|
||||
('payment_paypal_connect_client_id', forms.CharField(
|
||||
label=_('PayPal ISU/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.'),
|
||||
required=False,
|
||||
)),
|
||||
('payment_paypal_connect_endpoint', forms.ChoiceField(
|
||||
label=_('PayPal ISU/Connect Endpoint'),
|
||||
initial='live',
|
||||
choices=(
|
||||
('live', 'Live'),
|
||||
('sandbox', 'Sandbox'),
|
||||
),
|
||||
)),
|
||||
])
|
||||
|
||||
|
||||
@receiver(html_head, dispatch_uid="payment_paypal2_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:paypal2" and url.url_name == "pay")
|
||||
):
|
||||
provider.init_api()
|
||||
template = get_template('pretixplugins/paypal2/presale_head.html')
|
||||
|
||||
ctx = {
|
||||
'client_id': provider.client.environment.client_id,
|
||||
'merchant_id': provider.client.environment.merchant_id or '',
|
||||
'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_paypal2_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:paypal2" 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...
|
||||
'img-src': ['https://t.paypal.com'],
|
||||
'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
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="124px" height="33px" viewBox="0 0 124 33" enable-background="new 0 0 124 33" xml:space="preserve">
|
||||
<path fill="#253B80" d="M46.211,6.749h-6.839c-0.468,0-0.866,0.34-0.939,0.802l-2.766,17.537c-0.055,0.346,0.213,0.658,0.564,0.658 h3.265c0.468,0,0.866-0.34,0.939-0.803l0.746-4.73c0.072-0.463,0.471-0.803,0.938-0.803h2.165c4.505,0,7.105-2.18,7.784-6.5 c0.306-1.89,0.013-3.375-0.872-4.415C50.224,7.353,48.5,6.749,46.211,6.749z M47,13.154c-0.374,2.454-2.249,2.454-4.062,2.454 h-1.032l0.724-4.583c0.043-0.277,0.283-0.481,0.563-0.481h0.473c1.235,0,2.4,0,3.002,0.704C47.027,11.668,47.137,12.292,47,13.154z"/>
|
||||
<path fill="#253B80" d="M66.654,13.075h-3.275c-0.279,0-0.52,0.204-0.563,0.481l-0.145,0.916l-0.229-0.332 c-0.709-1.029-2.29-1.373-3.868-1.373c-3.619,0-6.71,2.741-7.312,6.586c-0.313,1.918,0.132,3.752,1.22,5.031 c0.998,1.176,2.426,1.666,4.125,1.666c2.916,0,4.533-1.875,4.533-1.875l-0.146,0.91c-0.055,0.348,0.213,0.66,0.562,0.66h2.95 c0.469,0,0.865-0.34,0.939-0.803l1.77-11.209C67.271,13.388,67.004,13.075,66.654,13.075z M62.089,19.449 c-0.316,1.871-1.801,3.127-3.695,3.127c-0.951,0-1.711-0.305-2.199-0.883c-0.484-0.574-0.668-1.391-0.514-2.301 c0.295-1.855,1.805-3.152,3.67-3.152c0.93,0,1.686,0.309,2.184,0.892C62.034,17.721,62.232,18.543,62.089,19.449z"/>
|
||||
<path fill="#253B80" d="M84.096,13.075h-3.291c-0.314,0-0.609,0.156-0.787,0.417l-4.539,6.686l-1.924-6.425 c-0.121-0.402-0.492-0.678-0.912-0.678h-3.234c-0.393,0-0.666,0.384-0.541,0.754l3.625,10.638l-3.408,4.811 c-0.268,0.379,0.002,0.9,0.465,0.9h3.287c0.312,0,0.604-0.152,0.781-0.408L84.564,13.97C84.826,13.592,84.557,13.075,84.096,13.075z "/>
|
||||
<path fill="#179BD7" d="M94.992,6.749h-6.84c-0.467,0-0.865,0.34-0.938,0.802l-2.766,17.537c-0.055,0.346,0.213,0.658,0.562,0.658 h3.51c0.326,0,0.605-0.238,0.656-0.562l0.785-4.971c0.072-0.463,0.471-0.803,0.938-0.803h2.164c4.506,0,7.105-2.18,7.785-6.5 c0.307-1.89,0.012-3.375-0.873-4.415C99.004,7.353,97.281,6.749,94.992,6.749z M95.781,13.154c-0.373,2.454-2.248,2.454-4.062,2.454 h-1.031l0.725-4.583c0.043-0.277,0.281-0.481,0.562-0.481h0.473c1.234,0,2.4,0,3.002,0.704 C95.809,11.668,95.918,12.292,95.781,13.154z"/>
|
||||
<path fill="#179BD7" d="M115.434,13.075h-3.273c-0.281,0-0.52,0.204-0.562,0.481l-0.145,0.916l-0.23-0.332 c-0.709-1.029-2.289-1.373-3.867-1.373c-3.619,0-6.709,2.741-7.311,6.586c-0.312,1.918,0.131,3.752,1.219,5.031 c1,1.176,2.426,1.666,4.125,1.666c2.916,0,4.533-1.875,4.533-1.875l-0.146,0.91c-0.055,0.348,0.213,0.66,0.564,0.66h2.949 c0.467,0,0.865-0.34,0.938-0.803l1.771-11.209C116.053,13.388,115.785,13.075,115.434,13.075z M110.869,19.449 c-0.314,1.871-1.801,3.127-3.695,3.127c-0.949,0-1.711-0.305-2.199-0.883c-0.484-0.574-0.666-1.391-0.514-2.301 c0.297-1.855,1.805-3.152,3.67-3.152c0.93,0,1.686,0.309,2.184,0.892C110.816,17.721,111.014,18.543,110.869,19.449z"/>
|
||||
<path fill="#179BD7" d="M119.295,7.23l-2.807,17.858c-0.055,0.346,0.213,0.658,0.562,0.658h2.822c0.469,0,0.867-0.34,0.939-0.803 l2.768-17.536c0.055-0.346-0.213-0.659-0.562-0.659h-3.16C119.578,6.749,119.338,6.953,119.295,7.23z"/>
|
||||
<path fill="#253B80" d="M7.266,29.154l0.523-3.322l-1.165-0.027H1.061L4.927,1.292C4.939,1.218,4.978,1.149,5.035,1.1 c0.057-0.049,0.13-0.076,0.206-0.076h9.38c3.114,0,5.263,0.648,6.385,1.927c0.526,0.6,0.861,1.227,1.023,1.917 c0.17,0.724,0.173,1.589,0.007,2.644l-0.012,0.077v0.676l0.526,0.298c0.443,0.235,0.795,0.504,1.065,0.812 c0.45,0.513,0.741,1.165,0.864,1.938c0.127,0.795,0.085,1.741-0.123,2.812c-0.24,1.232-0.628,2.305-1.152,3.183 c-0.482,0.809-1.096,1.48-1.825,2c-0.696,0.494-1.523,0.869-2.458,1.109c-0.906,0.236-1.939,0.355-3.072,0.355h-0.73 c-0.522,0-1.029,0.188-1.427,0.525c-0.399,0.344-0.663,0.814-0.744,1.328l-0.055,0.299l-0.924,5.855l-0.042,0.215 c-0.011,0.068-0.03,0.102-0.058,0.125c-0.025,0.021-0.061,0.035-0.096,0.035H7.266z"/>
|
||||
<path fill="#179BD7" d="M23.048,7.667L23.048,7.667L23.048,7.667c-0.028,0.179-0.06,0.362-0.096,0.55 c-1.237,6.351-5.469,8.545-10.874,8.545H9.326c-0.661,0-1.218,0.48-1.321,1.132l0,0l0,0L6.596,26.83l-0.399,2.533 c-0.067,0.428,0.263,0.814,0.695,0.814h4.881c0.578,0,1.069-0.42,1.16-0.99l0.048-0.248l0.919-5.832l0.059-0.32 c0.09-0.572,0.582-0.992,1.16-0.992h0.73c4.729,0,8.431-1.92,9.513-7.476c0.452-2.321,0.218-4.259-0.978-5.622 C24.022,8.286,23.573,7.945,23.048,7.667z"/>
|
||||
<path fill="#222D65" d="M21.754,7.151c-0.189-0.055-0.384-0.105-0.584-0.15c-0.201-0.044-0.407-0.083-0.619-0.117 c-0.742-0.12-1.555-0.177-2.426-0.177h-7.352c-0.181,0-0.353,0.041-0.507,0.115C9.927,6.985,9.675,7.306,9.614,7.699L8.05,17.605 l-0.045,0.289c0.103-0.652,0.66-1.132,1.321-1.132h2.752c5.405,0,9.637-2.195,10.874-8.545c0.037-0.188,0.068-0.371,0.096-0.55 c-0.313-0.166-0.652-0.308-1.017-0.429C21.941,7.208,21.848,7.179,21.754,7.151z"/>
|
||||
<path fill="#253B80" d="M9.614,7.699c0.061-0.393,0.313-0.714,0.652-0.876c0.155-0.074,0.326-0.115,0.507-0.115h7.352 c0.871,0,1.684,0.057,2.426,0.177c0.212,0.034,0.418,0.073,0.619,0.117c0.2,0.045,0.395,0.095,0.584,0.15 c0.094,0.028,0.187,0.057,0.278,0.086c0.365,0.121,0.704,0.264,1.017,0.429c0.368-2.347-0.003-3.945-1.272-5.392 C20.378,0.682,17.853,0,14.622,0h-9.38c-0.66,0-1.223,0.48-1.325,1.133L0.01,25.898c-0.077,0.49,0.301,0.932,0.795,0.932h5.791 l1.454-9.225L9.614,7.699z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,282 @@
|
||||
/*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);
|
||||
}
|
||||
|
||||
pretixpaypal.ready();
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
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) {
|
||||
if ('id' in data) {
|
||||
return data.id;
|
||||
} else {
|
||||
// Refreshing the page to surface the request-error message
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
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();
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
{% 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>
|
||||
@@ -0,0 +1,18 @@
|
||||
{% 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" />
|
||||
@@ -0,0 +1,18 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if payment_info %}
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Sale ID" %}</dt>
|
||||
<dd>{{ payment_info.purchase_units.0.payments.captures.0.id }}</dd>
|
||||
<dt>{% trans "Status" %}</dt>
|
||||
<dd>{{ payment_info.status }}</dd>
|
||||
<dt>{% trans "Payer" %}</dt>
|
||||
<dd>{{ payment_info.payer.email_address }}</dd>
|
||||
<dt>{% trans "Last update" %}</dt>
|
||||
<dd>{{ payment_info.purchase_units.0.payments.captures.0.update_time }}</dd>
|
||||
<dt>{% trans "Total value" %}</dt>
|
||||
<dd>{{ payment_info.purchase_units.0.payments.captures.0.amount.value }}</dd>
|
||||
<dt>{% trans "Currency" %}</dt>
|
||||
<dd>{{ payment_info.purchase_units.0.payments.captures.0.amount.currency_code }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,18 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,45 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if retry %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
Our attempt to execute your Payment via PayPal has failed. Please try again or contact us.
|
||||
{% endblocktrans %}</p>
|
||||
{% else %}
|
||||
<p>{% 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 %}</p>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
{% load i18n %}
|
||||
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "pretixplugins/paypal2/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 %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{% load compress %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
|
||||
{% endcompress %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
|
||||
{% endcompress %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>{% trans "The payment process has started in a new window." %}</h1>
|
||||
|
||||
<p>
|
||||
{% trans "The window to enter your payment data was not opened or was closed?" %}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url }}" target="_blank">
|
||||
{% trans "Click here in order to open the window." %}
|
||||
</a>
|
||||
</p>
|
||||
<script>
|
||||
window.open('{{ url|escapejs }}');
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
49
src/pretix/plugins/paypal2/urls.py
Normal file
49
src/pretix/plugins/paypal2/urls.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
from django.conf.urls import include, re_path
|
||||
|
||||
from .views import (
|
||||
PayView, XHRView, abort, isu_disconnect, isu_return, redirect_view,
|
||||
success, webhook,
|
||||
)
|
||||
|
||||
event_patterns = [
|
||||
re_path(r'^paypal2/', include([
|
||||
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'),
|
||||
])),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/paypal2/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'^_paypal/webhook/$', webhook, name='webhook'),
|
||||
]
|
||||
471
src/pretix/plugins/paypal2/views.py
Normal file
471
src/pretix/plugins/paypal2/views.py
Normal file
@@ -0,0 +1,471 @@
|
||||
#
|
||||
# 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 hashlib
|
||||
import json
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
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.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
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.services.cart import get_fees
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.control.permissions import event_permission_required
|
||||
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
|
||||
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||
from pretix.presale.views import get_cart, get_cart_total
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.paypal2')
|
||||
|
||||
|
||||
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')
|
||||
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 = {
|
||||
'positions': order.positions,
|
||||
'cart_total': order.pending_sum,
|
||||
'cart_fees': Decimal('0.00'),
|
||||
'payment_fee': fee,
|
||||
}
|
||||
else:
|
||||
cart_total = get_cart_total(request)
|
||||
cart_fees = Decimal('0.00')
|
||||
for fee in get_fees(request.event, request, cart_total, None, None, get_cart(request)):
|
||||
cart_fees += fee.value
|
||||
|
||||
cart = {
|
||||
'positions': get_cart(request),
|
||||
'cart_total': cart_total,
|
||||
'cart_fees': cart_fees,
|
||||
'payment_fee': prov.calculate_fee(cart_total + cart_fees),
|
||||
}
|
||||
|
||||
paypal_order = prov._create_paypal_order(request, None, cart)
|
||||
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):
|
||||
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):
|
||||
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'])
|
||||
|
||||
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('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.')
|
||||
)
|
||||
|
||||
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(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_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(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(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:
|
||||
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(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(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:
|
||||
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)
|
||||
|
||||
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) \
|
||||
and sale['status'] == 'COMPLETED':
|
||||
try:
|
||||
payment.confirm()
|
||||
except Quota.QuotaExceededException:
|
||||
pass
|
||||
|
||||
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.BB'))
|
||||
|
||||
return redirect(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
|
||||
@@ -128,7 +128,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, false, false, true)">
|
||||
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, false, false)">
|
||||
{{ $root.strings['modal.continue'] }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" @click="showUnpaidModal = false">
|
||||
@@ -188,7 +188,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, true, true)">
|
||||
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, true)">
|
||||
{{ $root.strings['modal.continue'] }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" @click="showQuestionsModal = false">
|
||||
@@ -296,7 +296,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
selectResult(res) {
|
||||
this.check(res.id, false, false, false, false)
|
||||
this.check(res.id, false, false, false)
|
||||
},
|
||||
answerSetM(qid, opid, checked) {
|
||||
let arr = this.answers[qid] ? this.answers[qid].split(',') : [];
|
||||
@@ -320,7 +320,7 @@ export default {
|
||||
this.showQuestionsModal = false
|
||||
this.answers = {}
|
||||
},
|
||||
check(id, ignoreUnpaid, keepAnswers, fallbackToSearch, untrusted) {
|
||||
check(id, ignoreUnpaid, keepAnswers, fallbackToSearch) {
|
||||
if (!keepAnswers) {
|
||||
this.answers = {}
|
||||
} else if (this.showQuestionsModal) {
|
||||
@@ -339,11 +339,7 @@ export default {
|
||||
this.$refs.input.blur()
|
||||
})
|
||||
|
||||
let url = this.$root.api.lists + this.checkinlist.id + '/positions/' + encodeURIComponent(id) + '/redeem/?expand=item&expand=subevent&expand=variation'
|
||||
if (untrusted) {
|
||||
url += '&untrusted_input=true'
|
||||
}
|
||||
fetch(url, {
|
||||
fetch(this.$root.api.lists + this.checkinlist.id + '/positions/' + encodeURIComponent(id) + '/redeem/?expand=item&expand=subevent&expand=variation', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector("input[name=csrfmiddlewaretoken]").value,
|
||||
@@ -443,7 +439,7 @@ export default {
|
||||
startSearch(fallbackToScan) {
|
||||
if (this.query.length >= 32 && fallbackToScan) {
|
||||
// likely a secret, not a search result
|
||||
this.check(this.query, false, false, true, true)
|
||||
this.check(this.query, false, false, true)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
</div>
|
||||
<p>
|
||||
{% elif not var.display_price.gross %}
|
||||
{% trans "FREE" context "price" %}
|
||||
<span class="text-uppercase">{% trans "free" context "price" %}</span>
|
||||
{% elif event.settings.display_net_prices %}
|
||||
{{ var.display_price.net|money:event.currency }}
|
||||
{% else %}
|
||||
|
||||
@@ -356,6 +356,7 @@ INSTALLED_APPS = [
|
||||
'pretix.plugins.banktransfer',
|
||||
'pretix.plugins.stripe',
|
||||
'pretix.plugins.paypal',
|
||||
'pretix.plugins.paypal2',
|
||||
'pretix.plugins.ticketoutputpdf',
|
||||
'pretix.plugins.sendmail',
|
||||
'pretix.plugins.statistics',
|
||||
|
||||
@@ -203,6 +203,8 @@ setup(
|
||||
'openpyxl==3.0.*',
|
||||
'packaging',
|
||||
'paypalrestsdk==1.13.*',
|
||||
'paypal-checkout-serversdk==1.0.*',
|
||||
'PyJWT==2.0.*',
|
||||
'phonenumberslite==8.12.*',
|
||||
'Pillow==9.1.*',
|
||||
'protobuf==3.19.*',
|
||||
|
||||
@@ -1199,6 +1199,7 @@ def test_redeem_unknown_legacy_device_bug(device, device_client, organizer, clis
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
print(resp.data)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "already_redeemed"
|
||||
@@ -1218,43 +1219,3 @@ def test_redeem_unknown_legacy_device_bug(device, device_client, organizer, clis
|
||||
assert resp.data["reason"] == "invalid"
|
||||
with scopes_disabled():
|
||||
assert not Checkin.objects.last()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_by_id_not_allowed_if_pretixscan(device, device_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
device.software_brand = "pretixSCAN"
|
||||
device.software_version = "1.14.2"
|
||||
device.save()
|
||||
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
print(resp.data)
|
||||
assert resp.status_code == 404
|
||||
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.secret
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
assert resp.status_code == 201
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_by_id_not_allowed_if_untrusted(device, device_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/?untrusted_input=true'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
assert resp.status_code == 404
|
||||
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/?untrusted_input=true'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.secret
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
assert resp.status_code == 201
|
||||
|
||||
@@ -299,16 +299,16 @@ class EventsTest(SoupTest):
|
||||
|
||||
def test_plugins(self):
|
||||
doc = self.get_doc('/control/event/%s/%s/settings/plugins' % (self.orga1.slug, self.event1.slug))
|
||||
self.assertIn("PayPal", doc.select(".form-plugins")[0].text)
|
||||
self.assertIn("Enable", doc.select("[name=\"plugin:pretix.plugins.paypal\"]")[0].text)
|
||||
self.assertIn("Stripe", doc.select(".form-plugins")[0].text)
|
||||
self.assertIn("Enable", doc.select("[name=\"plugin:pretix.plugins.stripe\"]")[0].text)
|
||||
|
||||
doc = self.post_doc('/control/event/%s/%s/settings/plugins' % (self.orga1.slug, self.event1.slug),
|
||||
{'plugin:pretix.plugins.paypal': 'enable'})
|
||||
self.assertIn("Disable", doc.select("[name=\"plugin:pretix.plugins.paypal\"]")[0].text)
|
||||
{'plugin:pretix.plugins.stripe': 'enable'})
|
||||
self.assertIn("Disable", doc.select("[name=\"plugin:pretix.plugins.stripe\"]")[0].text)
|
||||
|
||||
doc = self.post_doc('/control/event/%s/%s/settings/plugins' % (self.orga1.slug, self.event1.slug),
|
||||
{'plugin:pretix.plugins.paypal': 'disable'})
|
||||
self.assertIn("Enable", doc.select("[name=\"plugin:pretix.plugins.paypal\"]")[0].text)
|
||||
{'plugin:pretix.plugins.stripe': 'disable'})
|
||||
self.assertIn("Enable", doc.select("[name=\"plugin:pretix.plugins.stripe\"]")[0].text)
|
||||
|
||||
def test_testmode_enable(self):
|
||||
self.event1.testmode = False
|
||||
|
||||
@@ -57,12 +57,8 @@ def test_require_live(event, client):
|
||||
event.save()
|
||||
r = client.get('/mrmcd/2015/paypal/abort/', follow=False)
|
||||
assert r.status_code == 302
|
||||
r = client.get('/mrmcd/2015/paypal/webhook/', follow=False)
|
||||
assert r.status_code == 405
|
||||
|
||||
event.live = False
|
||||
event.save()
|
||||
r = client.get('/mrmcd/2015/paypal/abort/', follow=False)
|
||||
assert r.status_code == 403
|
||||
r = client.get('/mrmcd/2015/paypal/webhook/', follow=False)
|
||||
assert r.status_code == 405
|
||||
|
||||
33
src/tests/plugins/paypal/__init__.py
Normal file
33
src/tests/plugins/paypal/__init__.py
Normal file
@@ -0,0 +1,33 @@
|
||||
#
|
||||
# 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: 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.
|
||||
@@ -241,7 +241,7 @@ def test_webhook_all_good(env, client, monkeypatch):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_webhook_global(env, client, monkeypatch):
|
||||
def test_webhook_mark_paid(env, client, monkeypatch):
|
||||
order = env[1]
|
||||
order.status = Order.STATUS_PENDING
|
||||
order.save()
|
||||
@@ -282,49 +282,9 @@ def test_webhook_global(env, client, monkeypatch):
|
||||
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)
|
||||
|
||||
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.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])
|
||||
@@ -332,8 +292,9 @@ def test_webhook_refund1(env, client, monkeypatch):
|
||||
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('/dummy/dummy/paypal/webhook/', json.dumps(
|
||||
client.post('/_paypal/webhook/', json.dumps(
|
||||
{
|
||||
# Sample obtained in a sandbox webhook
|
||||
"id": "WH-9K829080KA1622327-31011919VC6498738",
|
||||
@@ -380,6 +341,7 @@ def test_webhook_refund1(env, client, monkeypatch):
|
||||
|
||||
@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])
|
||||
@@ -387,8 +349,9 @@ def test_webhook_refund2(env, client, monkeypatch):
|
||||
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('/dummy/dummy/paypal/webhook/', json.dumps(
|
||||
client.post('/_paypal/webhook/', json.dumps(
|
||||
{
|
||||
# Sample obtained in the webhook simulator
|
||||
"id": "WH-2N242548W9943490U-1JU23391CS4765624",
|
||||
|
||||
33
src/tests/plugins/paypal2/__init__.py
Normal file
33
src/tests/plugins/paypal2/__init__.py
Normal file
@@ -0,0 +1,33 @@
|
||||
#
|
||||
# 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: 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.
|
||||
141
src/tests/plugins/paypal2/test_checkout.py
Normal file
141
src/tests/plugins/paypal2/test_checkout.py
Normal file
@@ -0,0 +1,141 @@
|
||||
#
|
||||
# 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: 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.paypal2',
|
||||
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
|
||||
|
||||
|
||||
class Object():
|
||||
pass
|
||||
|
||||
|
||||
def get_test_order():
|
||||
return {'id': '04F89033701558004',
|
||||
'intent': 'CAPTURE',
|
||||
'status': 'APPROVED',
|
||||
'purchase_units': [{'reference_id': 'default',
|
||||
'amount': {'currency_code': 'EUR', 'value': '43.59'},
|
||||
'payee': {'merchant_id': 'G6R2B9YXADKWW'},
|
||||
'description': 'Event tickets for PayPal v2',
|
||||
'custom_id': 'PAYPALV2',
|
||||
'soft_descriptor': 'MARTINFACIL'}],
|
||||
'payer': {'name': {'given_name': 'test', 'surname': 'buyer'},
|
||||
'email_address': 'dummy@dummy.dummy',
|
||||
'payer_id': 'Q739JNKWH67HE',
|
||||
'address': {'country_code': 'DE'}},
|
||||
'create_time': '2022-04-28T13:10:58Z',
|
||||
'links': [{'href': 'https://api.sandbox.paypal.com/v2/checkout/orders/04F89033701558004',
|
||||
'rel': 'self',
|
||||
'method': 'GET'},
|
||||
{'href': 'https://api.sandbox.paypal.com/v2/checkout/orders/04F89033701558004',
|
||||
'rel': 'update',
|
||||
'method': 'PATCH'},
|
||||
{'href': 'https://api.sandbox.paypal.com/v2/checkout/orders/04F89033701558004/capture',
|
||||
'rel': 'capture',
|
||||
'method': 'POST'}]}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_payment(env, monkeypatch):
|
||||
def init_api(self):
|
||||
class Client():
|
||||
environment = Object()
|
||||
environment.client_id = '12345'
|
||||
environment.merchant_id = 'G6R2B9YXADKWW'
|
||||
|
||||
def execute(self, request):
|
||||
response = Object()
|
||||
response.result = Object()
|
||||
response.result.status = 'APPROVED'
|
||||
return response
|
||||
|
||||
self.client = Client()
|
||||
|
||||
order = get_test_order()
|
||||
monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", lambda *args: order)
|
||||
monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api)
|
||||
|
||||
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)
|
||||
|
||||
session = client.session
|
||||
session['payment_paypal_oid'] = '04F89033701558004'
|
||||
session.save()
|
||||
|
||||
response = client.post('/%s/%s/checkout/payment/' % (ticket.event.organizer.slug, ticket.event.slug), {
|
||||
'payment': 'paypal',
|
||||
'payment_paypal_wallet_oid': '04F89033701558004',
|
||||
'payment_paypal_wallet_payer': 'Q739JNKWH67HE',
|
||||
})
|
||||
print(response.content.decode())
|
||||
assert response['Location'] == '/ccc/30c3/checkout/confirm/'
|
||||
67
src/tests/plugins/paypal2/test_settings.py
Normal file
67
src/tests/plugins/paypal2/test_settings.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#
|
||||
# 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: 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.paypal2',
|
||||
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, can_change_event_settings=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_settings' % (event.organizer.slug, event.slug),
|
||||
follow=True)
|
||||
assert response.status_code == 200
|
||||
assert 'paypal__enabled' in response.rendered_content
|
||||
688
src/tests/plugins/paypal2/test_webhook.py
Normal file
688
src/tests/plugins/paypal2/test_webhook.py
Normal file
@@ -0,0 +1,688 @@
|
||||
#
|
||||
# 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 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.paypal2',
|
||||
date_from=now(), live=True
|
||||
)
|
||||
t = Team.objects.create(organizer=event.organizer, can_view_orders=True, can_change_orders=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('43.59'),
|
||||
)
|
||||
o1.payments.create(
|
||||
amount=o1.total,
|
||||
provider='paypal',
|
||||
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||
info=json.dumps({
|
||||
"id": "806440346Y391300T",
|
||||
"status": "COMPLETED",
|
||||
"purchase_units": [
|
||||
{
|
||||
"reference_id": "default",
|
||||
"shipping": {
|
||||
"name": {
|
||||
"full_name": "test buyer"
|
||||
}
|
||||
},
|
||||
"payments": {
|
||||
"captures": [
|
||||
{
|
||||
"id": "22A4162004478570J",
|
||||
"status": "COMPLETED",
|
||||
"amount": {
|
||||
"currency_code": "EUR",
|
||||
"value": "43.59"
|
||||
},
|
||||
"final_capture": True,
|
||||
"disbursement_mode": "INSTANT",
|
||||
"seller_protection": {
|
||||
"status": "ELIGIBLE",
|
||||
"dispute_categories": [
|
||||
"ITEM_NOT_RECEIVED",
|
||||
"UNAUTHORIZED_TRANSACTION"
|
||||
]
|
||||
},
|
||||
"seller_receivable_breakdown": {
|
||||
"gross_amount": {
|
||||
"currency_code": "EUR",
|
||||
"value": "43.59"
|
||||
},
|
||||
"paypal_fee": {
|
||||
"currency_code": "EUR",
|
||||
"value": "1.18"
|
||||
},
|
||||
"net_amount": {
|
||||
"currency_code": "EUR",
|
||||
"value": "42.41"
|
||||
}
|
||||
},
|
||||
"custom_id": "Order PAYPALV2-JWJGC",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v2/payments/captures/22A4162004478570J",
|
||||
"rel": "self",
|
||||
"method": "GET"
|
||||
},
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v2/payments/captures/22A4162004478570J/refund",
|
||||
"rel": "refund",
|
||||
"method": "POST"
|
||||
},
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T",
|
||||
"rel": "up",
|
||||
"method": "GET"
|
||||
}
|
||||
],
|
||||
"create_time": "2022-04-28T12:00:22Z",
|
||||
"update_time": "2022-04-28T12:00:22Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"payer": {
|
||||
"name": {
|
||||
"given_name": "test",
|
||||
"surname": "buyer"
|
||||
},
|
||||
"email_address": "dummy@dummy.dummy",
|
||||
"payer_id": "Q739JNKWH67HE",
|
||||
"address": {
|
||||
"country_code": "DE"
|
||||
}
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T",
|
||||
"rel": "self",
|
||||
"method": "GET"
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
return event, o1
|
||||
|
||||
|
||||
def get_test_order():
|
||||
return {'id': '806440346Y391300T',
|
||||
'intent': 'CAPTURE',
|
||||
'status': 'COMPLETED',
|
||||
'purchase_units': [{'reference_id': 'default',
|
||||
'amount': {'currency_code': 'EUR', 'value': '43.59'},
|
||||
'payee': {'email_address': 'dummy-facilitator@dummy.dummy',
|
||||
'merchant_id': 'G6R2B9YXADKWW'},
|
||||
'description': 'Order JWJGC for PayPal v2',
|
||||
'custom_id': 'Order PAYPALV2-JWJGC',
|
||||
'soft_descriptor': 'MARTINFACIL',
|
||||
'payments': {'captures': [{'id': '22A4162004478570J',
|
||||
'status': 'COMPLETED',
|
||||
'amount': {'currency_code': 'EUR', 'value': '43.59'},
|
||||
'final_capture': True,
|
||||
'disbursement_mode': 'INSTANT',
|
||||
'seller_protection': {'status': 'ELIGIBLE',
|
||||
'dispute_categories': [
|
||||
'ITEM_NOT_RECEIVED',
|
||||
'UNAUTHORIZED_TRANSACTION']},
|
||||
'seller_receivable_breakdown': {
|
||||
'gross_amount': {'currency_code': 'EUR',
|
||||
'value': '43.59'},
|
||||
'paypal_fee': {'currency_code': 'EUR', 'value': '1.18'},
|
||||
'net_amount': {'currency_code': 'EUR',
|
||||
'value': '42.41'}},
|
||||
'custom_id': 'Order PAYPALV2-JWJGC',
|
||||
'links': [{
|
||||
'href': 'https://api.sandbox.paypal.com/v2/payments/captures/22A4162004478570J',
|
||||
'rel': 'self',
|
||||
'method': 'GET'},
|
||||
{
|
||||
'href': 'https://api.sandbox.paypal.com/v2/payments/captures/22A4162004478570J/refund',
|
||||
'rel': 'refund',
|
||||
'method': 'POST'},
|
||||
{
|
||||
'href': 'https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T',
|
||||
'rel': 'up',
|
||||
'method': 'GET'}],
|
||||
'create_time': '2022-04-28T12:00:22Z',
|
||||
'update_time': '2022-04-28T12:00:22Z'}]}}],
|
||||
'payer': {'name': {'given_name': 'test', 'surname': 'buyer'},
|
||||
'email_address': 'dummy@dummy.dummy',
|
||||
'payer_id': 'Q739JNKWH67HE',
|
||||
'address': {'country_code': 'DE'}},
|
||||
'create_time': '2022-04-28T11:59:59Z',
|
||||
'update_time': '2022-04-28T12:00:22Z',
|
||||
'links': [{'href': 'https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T',
|
||||
'rel': 'self',
|
||||
'method': 'GET'}]}
|
||||
|
||||
|
||||
def get_test_refund():
|
||||
return {
|
||||
"id": "1YK122615V244890X",
|
||||
"amount": {
|
||||
"currency_code": "EUR",
|
||||
"value": "43.59"
|
||||
},
|
||||
"seller_payable_breakdown": {
|
||||
"gross_amount": {
|
||||
"currency_code": "EUR",
|
||||
"value": "43.59"
|
||||
},
|
||||
"paypal_fee": {
|
||||
"currency_code": "EUR",
|
||||
"value": "1.18"
|
||||
},
|
||||
"net_amount": {
|
||||
"currency_code": "EUR",
|
||||
"value": "42.41"
|
||||
},
|
||||
"total_refunded_amount": {
|
||||
"currency_code": "EUR",
|
||||
"value": "43.59"
|
||||
}
|
||||
},
|
||||
"custom_id": "Order PAYPALV2-JWJGC",
|
||||
"status": "COMPLETED",
|
||||
"create_time": "2022-04-28T07:50:56-07:00",
|
||||
"update_time": "2022-04-28T07:50:56-07:00",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v2/payments/refunds/1YK122615V244890X",
|
||||
"rel": "self",
|
||||
"method": "GET"
|
||||
},
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v2/payments/captures/22A4162004478570J",
|
||||
"rel": "up",
|
||||
"method": "GET"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class Object():
|
||||
pass
|
||||
|
||||
|
||||
def init_api(self):
|
||||
class Client():
|
||||
environment = Object()
|
||||
environment.client_id = '12345'
|
||||
environment.merchant_id = 'G6R2B9YXADKWW'
|
||||
|
||||
def execute(self, request):
|
||||
response = Object()
|
||||
response.result = request
|
||||
return response
|
||||
|
||||
self.client = Client()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_webhook_all_good(env, client, monkeypatch):
|
||||
order = env[1]
|
||||
pp_order = get_test_order()
|
||||
monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", lambda *args: pp_order)
|
||||
monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api)
|
||||
|
||||
with scopes_disabled():
|
||||
ReferencedPayPalObject.objects.create(order=order, payment=order.payments.first(),
|
||||
reference="806440346Y391300T")
|
||||
|
||||
client.post('/_paypal/webhook/', json.dumps(
|
||||
{
|
||||
"id": "WH-4T867178D0574904F-7TT11736YU643990P",
|
||||
"create_time": "2022-04-28T12:00:37.077Z",
|
||||
"resource_type": "checkout-order",
|
||||
"event_type": "CHECKOUT.ORDER.COMPLETED",
|
||||
"summary": "Checkout Order Completed",
|
||||
"resource": {
|
||||
"update_time": "2022-04-28T12:00:22Z",
|
||||
"create_time": "2022-04-28T11:59:59Z",
|
||||
"purchase_units": [
|
||||
{
|
||||
"reference_id": "default",
|
||||
"amount": {
|
||||
"currency_code": "EUR",
|
||||
"value": "43.59"
|
||||
},
|
||||
"payee": {
|
||||
"email_address": "dummy-facilitator@dummy.dummy",
|
||||
"merchant_id": "G6R2B9YXADKWW"
|
||||
},
|
||||
"description": "Order JWJGC for PayPal v2",
|
||||
"custom_id": "Order PAYPALV2-JWJGC",
|
||||
"soft_descriptor": "MARTINFACIL",
|
||||
"payments": {
|
||||
"captures": [
|
||||
{
|
||||
"id": "22A4162004478570J",
|
||||
"status": "COMPLETED",
|
||||
"amount": {
|
||||
"currency_code": "EUR",
|
||||
"value": "43.59"
|
||||
},
|
||||
"final_capture": True,
|
||||
"disbursement_mode": "INSTANT",
|
||||
"seller_protection": {
|
||||
"status": "ELIGIBLE",
|
||||
"dispute_categories": [
|
||||
"ITEM_NOT_RECEIVED",
|
||||
"UNAUTHORIZED_TRANSACTION"
|
||||
]
|
||||
},
|
||||
"seller_receivable_breakdown": {
|
||||
"gross_amount": {
|
||||
"currency_code": "EUR",
|
||||
"value": "43.59"
|
||||
},
|
||||
"paypal_fee": {
|
||||
"currency_code": "EUR",
|
||||
"value": "1.18"
|
||||
},
|
||||
"net_amount": {
|
||||
"currency_code": "EUR",
|
||||
"value": "42.41"
|
||||
}
|
||||
},
|
||||
"custom_id": "Order PAYPALV2-JWJGC",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v2/payments/captures/22A4162004478570J",
|
||||
"rel": "self",
|
||||
"method": "GET"
|
||||
},
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v2/payments/captures/22A4162004478570J/refund",
|
||||
"rel": "refund",
|
||||
"method": "POST"
|
||||
},
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T",
|
||||
"rel": "up",
|
||||
"method": "GET"
|
||||
}
|
||||
],
|
||||
"create_time": "2022-04-28T12:00:22Z",
|
||||
"update_time": "2022-04-28T12:00:22Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T",
|
||||
"rel": "self",
|
||||
"method": "GET"
|
||||
}
|
||||
],
|
||||
"id": "806440346Y391300T",
|
||||
"intent": "CAPTURE",
|
||||
"payer": {
|
||||
"name": {
|
||||
"given_name": "test",
|
||||
"surname": "buyer"
|
||||
},
|
||||
"email_address": "dummy@dummy.dummy",
|
||||
"payer_id": "Q739JNKWH67HE",
|
||||
"address": {
|
||||
"country_code": "DE"
|
||||
}
|
||||
},
|
||||
"status": "COMPLETED"
|
||||
},
|
||||
"status": "SUCCESS",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4T867178D0574904F-7TT11736YU643990P",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"encType": "application/json"
|
||||
},
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-4T867178D0574904F-7TT11736YU643990P/resend",
|
||||
"rel": "resend",
|
||||
"method": "POST",
|
||||
"encType": "application/json"
|
||||
}
|
||||
],
|
||||
"event_version": "1.0",
|
||||
"resource_version": "2.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)
|
||||
|
||||
pp_order = get_test_order()
|
||||
monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", lambda *args: pp_order)
|
||||
monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api)
|
||||
with scopes_disabled():
|
||||
ReferencedPayPalObject.objects.create(order=order, payment=order.payments.first(),
|
||||
reference="806440346Y391300T")
|
||||
|
||||
client.post('/_paypal/webhook/', json.dumps(
|
||||
{
|
||||
"id": "WH-88L014580L300952M-4BX97184625330932",
|
||||
"create_time": "2022-04-28T12:00:26.840Z",
|
||||
"resource_type": "capture",
|
||||
"event_type": "PAYMENT.CAPTURE.COMPLETED",
|
||||
"summary": "Payment completed for EUR 43.59 EUR",
|
||||
"resource": {
|
||||
"disbursement_mode": "INSTANT",
|
||||
"amount": {
|
||||
"value": "43.59",
|
||||
"currency_code": "EUR"
|
||||
},
|
||||
"seller_protection": {
|
||||
"dispute_categories": [
|
||||
"ITEM_NOT_RECEIVED",
|
||||
"UNAUTHORIZED_TRANSACTION"
|
||||
],
|
||||
"status": "ELIGIBLE"
|
||||
},
|
||||
"supplementary_data": {
|
||||
"related_ids": {
|
||||
"order_id": "806440346Y391300T"
|
||||
}
|
||||
},
|
||||
"update_time": "2022-04-28T12:00:22Z",
|
||||
"create_time": "2022-04-28T12:00:22Z",
|
||||
"final_capture": True,
|
||||
"seller_receivable_breakdown": {
|
||||
"paypal_fee": {
|
||||
"value": "1.18",
|
||||
"currency_code": "EUR"
|
||||
},
|
||||
"gross_amount": {
|
||||
"value": "43.59",
|
||||
"currency_code": "EUR"
|
||||
},
|
||||
"net_amount": {
|
||||
"value": "42.41",
|
||||
"currency_code": "EUR"
|
||||
}
|
||||
},
|
||||
"custom_id": "Order PAYPALV2-JWJGC",
|
||||
"links": [
|
||||
{
|
||||
"method": "GET",
|
||||
"rel": "self",
|
||||
"href": "https://api.sandbox.paypal.com/v2/payments/captures/22A4162004478570J"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"rel": "refund",
|
||||
"href": "https://api.sandbox.paypal.com/v2/payments/captures/22A4162004478570J/refund"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"rel": "up",
|
||||
"href": "https://api.sandbox.paypal.com/v2/checkout/orders/806440346Y391300T"
|
||||
}
|
||||
],
|
||||
"id": "22A4162004478570J",
|
||||
"status": "COMPLETED"
|
||||
},
|
||||
"status": "SUCCESS",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-88L014580L300952M-4BX97184625330932",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"encType": "application/json"
|
||||
},
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-88L014580L300952M-4BX97184625330932/resend",
|
||||
"rel": "resend",
|
||||
"method": "POST",
|
||||
"encType": "application/json"
|
||||
}
|
||||
],
|
||||
"event_version": "1.0",
|
||||
"resource_version": "2.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]
|
||||
pp_order = get_test_order()
|
||||
pp_refund = get_test_refund()
|
||||
|
||||
monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", lambda *args: pp_order)
|
||||
monkeypatch.setattr("paypalcheckoutsdk.payments.RefundsGetRequest", lambda *args: pp_refund)
|
||||
monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api)
|
||||
with scopes_disabled():
|
||||
ReferencedPayPalObject.objects.create(order=order, payment=order.payments.first(),
|
||||
reference="22A4162004478570J")
|
||||
|
||||
client.post('/_paypal/webhook/', json.dumps(
|
||||
{
|
||||
"id": "WH-5LJ60612747357339-66248625WA926672S",
|
||||
"create_time": "2022-04-28T14:51:00.318Z",
|
||||
"resource_type": "refund",
|
||||
"event_type": "PAYMENT.CAPTURE.REFUNDED",
|
||||
"summary": "A EUR 43.59 EUR capture payment was refunded",
|
||||
"resource": {
|
||||
"seller_payable_breakdown": {
|
||||
"total_refunded_amount": {
|
||||
"value": "43.59",
|
||||
"currency_code": "EUR"
|
||||
},
|
||||
"paypal_fee": {
|
||||
"value": "1.18",
|
||||
"currency_code": "EUR"
|
||||
},
|
||||
"gross_amount": {
|
||||
"value": "42.41",
|
||||
"currency_code": "EUR"
|
||||
},
|
||||
"net_amount": {
|
||||
"value": "43.59",
|
||||
"currency_code": "EUR"
|
||||
}
|
||||
},
|
||||
"amount": {
|
||||
"value": "43.59",
|
||||
"currency_code": "EUR"
|
||||
},
|
||||
"update_time": "2022-04-28T07:50:56-07:00",
|
||||
"create_time": "2022-04-28T07:50:56-07:00",
|
||||
"custom_id": "Order PAYPALV2-JWJGC",
|
||||
"links": [
|
||||
{
|
||||
"method": "GET",
|
||||
"rel": "self",
|
||||
"href": "https://api.sandbox.paypal.com/v2/payments/refunds/1YK122615V244890X"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"rel": "up",
|
||||
"href": "https://api.sandbox.paypal.com/v2/payments/captures/22A4162004478570J"
|
||||
}
|
||||
],
|
||||
"id": "1YK122615V244890X",
|
||||
"status": "COMPLETED"
|
||||
},
|
||||
"status": "SUCCESS",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-5LJ60612747357339-66248625WA926672S",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"encType": "application/json"
|
||||
},
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-5LJ60612747357339-66248625WA926672S/resend",
|
||||
"rel": "resend",
|
||||
"method": "POST",
|
||||
"encType": "application/json"
|
||||
}
|
||||
],
|
||||
"event_version": "1.0",
|
||||
"resource_version": "2.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]
|
||||
pp_order = get_test_order()
|
||||
pp_refund = get_test_refund()
|
||||
|
||||
monkeypatch.setattr("paypalcheckoutsdk.orders.OrdersGetRequest", lambda *args: pp_order)
|
||||
monkeypatch.setattr("paypalcheckoutsdk.payments.RefundsGetRequest", lambda *args: pp_refund)
|
||||
monkeypatch.setattr("pretix.plugins.paypal2.payment.PaypalMethod.init_api", init_api)
|
||||
with scopes_disabled():
|
||||
ReferencedPayPalObject.objects.create(order=order, payment=order.payments.first(),
|
||||
reference="22A4162004478570J")
|
||||
|
||||
client.post('/_paypal/webhook/', json.dumps(
|
||||
{
|
||||
"id": "WH-7FL378472F5218625-6WC87835CR8751809",
|
||||
"create_time": "2022-04-28T14:56:08.160Z",
|
||||
"resource_type": "refund",
|
||||
"event_type": "PAYMENT.CAPTURE.REFUNDED",
|
||||
"summary": "A EUR 43.59 EUR capture payment was refunded",
|
||||
"resource": {
|
||||
"seller_payable_breakdown": {
|
||||
"total_refunded_amount": {
|
||||
"value": "43.59",
|
||||
"currency_code": "EUR"
|
||||
},
|
||||
"paypal_fee": {
|
||||
"value": "01.18",
|
||||
"currency_code": "EUR"
|
||||
},
|
||||
"gross_amount": {
|
||||
"value": "43.59",
|
||||
"currency_code": "EUR"
|
||||
},
|
||||
"net_amount": {
|
||||
"value": "42.41",
|
||||
"currency_code": "EUR"
|
||||
}
|
||||
},
|
||||
"amount": {
|
||||
"value": "43.59",
|
||||
"currency_code": "EUR"
|
||||
},
|
||||
"update_time": "2022-04-28T07:56:04-07:00",
|
||||
"create_time": "2022-04-28T07:56:04-07:00",
|
||||
"custom_id": "Order PAYPALV2-JWJGC",
|
||||
"links": [
|
||||
{
|
||||
"method": "GET",
|
||||
"rel": "self",
|
||||
"href": "https://api.sandbox.paypal.com/v2/payments/refunds/3K87087190824201K"
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"rel": "up",
|
||||
"href": "https://api.sandbox.paypal.com/v2/payments/captures/22A4162004478570J"
|
||||
}
|
||||
],
|
||||
"id": "3K87087190824201K",
|
||||
"status": "COMPLETED"
|
||||
},
|
||||
"status": "SUCCESS",
|
||||
"links": [
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-7FL378472F5218625-6WC87835CR8751809",
|
||||
"rel": "self",
|
||||
"method": "GET",
|
||||
"encType": "application/json"
|
||||
},
|
||||
{
|
||||
"href": "https://api.sandbox.paypal.com/v1/notifications/webhooks-events/WH-7FL378472F5218625-6WC87835CR8751809/resend",
|
||||
"rel": "resend",
|
||||
"method": "POST",
|
||||
"encType": "application/json"
|
||||
}
|
||||
],
|
||||
"event_version": "1.0",
|
||||
"resource_version": "2.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
|
||||
Reference in New Issue
Block a user