pretix-thepay 9.0.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pretix_thepay/__init__.py +15 -0
- pretix_thepay/apps.py +40 -0
- pretix_thepay/locale/en/LC_MESSAGES/django.po +5 -0
- pretix_thepay/payment.py +763 -0
- pretix_thepay/pretix_plugin_urls.py +10 -0
- pretix_thepay/signals.py +18 -0
- pretix_thepay/urls.py +15 -0
- pretix_thepay/views.py +345 -0
- pretix_thepay-9.0.18.dist-info/METADATA +139 -0
- pretix_thepay-9.0.18.dist-info/RECORD +13 -0
- pretix_thepay-9.0.18.dist-info/WHEEL +5 -0
- pretix_thepay-9.0.18.dist-info/entry_points.txt +2 -0
- pretix_thepay-9.0.18.dist-info/top_level.txt +1 -0
pretix_thepay/signals.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Signal handlers for The Pay payment provider registration.
|
|
3
|
+
"""
|
|
4
|
+
from django.dispatch import receiver
|
|
5
|
+
from pretix.base.signals import register_payment_providers
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@receiver(register_payment_providers, dispatch_uid="payment_thepay")
|
|
9
|
+
def register_payment_provider(sender, **kwargs):
|
|
10
|
+
"""
|
|
11
|
+
Register The Pay payment provider with Pretix.
|
|
12
|
+
|
|
13
|
+
This signal handler is called by Pretix to discover available
|
|
14
|
+
payment providers and registers the The Pay provider.
|
|
15
|
+
"""
|
|
16
|
+
from .payment import ThePay
|
|
17
|
+
return ThePay
|
|
18
|
+
|
pretix_thepay/urls.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
URL configuration for The Pay payment provider.
|
|
3
|
+
|
|
4
|
+
Defines routes for handling payment gateway callbacks:
|
|
5
|
+
- return: User redirect after payment completion
|
|
6
|
+
- notify: Server-to-server notification (IPN)
|
|
7
|
+
"""
|
|
8
|
+
from django.urls import path
|
|
9
|
+
from . import views
|
|
10
|
+
|
|
11
|
+
urlpatterns = [
|
|
12
|
+
path('return/<str:order>/<int:payment>/<str:hash>/', views.ReturnView.as_view(), name='return'),
|
|
13
|
+
path('notify/<str:order>/<int:payment>/<str:hash>/', views.NotifyView.as_view(), name='notify'),
|
|
14
|
+
]
|
|
15
|
+
|
pretix_thepay/views.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Views for handling The Pay payment gateway callbacks.
|
|
3
|
+
|
|
4
|
+
This module provides views for processing user redirects and server-to-server
|
|
5
|
+
notifications from the The Pay payment gateway, including signature verification
|
|
6
|
+
and payment status updates.
|
|
7
|
+
"""
|
|
8
|
+
import logging
|
|
9
|
+
from django.contrib import messages
|
|
10
|
+
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
|
|
11
|
+
from django.shortcuts import redirect, get_object_or_404
|
|
12
|
+
from django.urls import reverse, NoReverseMatch
|
|
13
|
+
from django.utils.decorators import method_decorator
|
|
14
|
+
from django.utils.translation import gettext_lazy as _
|
|
15
|
+
from django.views import View
|
|
16
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
17
|
+
from django_scopes import scopes_disabled
|
|
18
|
+
from pretix.base.models import OrderPayment
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _order_redirect_url(request, order) -> str:
|
|
24
|
+
if hasattr(order, 'get_absolute_url'):
|
|
25
|
+
return order.get_absolute_url()
|
|
26
|
+
if hasattr(order, 'get_detail_url'):
|
|
27
|
+
return order.get_detail_url()
|
|
28
|
+
if hasattr(order, 'get_url'):
|
|
29
|
+
return order.get_url()
|
|
30
|
+
if hasattr(order, 'get_presale_url'):
|
|
31
|
+
return order.get_presale_url()
|
|
32
|
+
try:
|
|
33
|
+
patterns = [
|
|
34
|
+
('presale:event.order', {
|
|
35
|
+
'organizer': order.event.organizer.slug,
|
|
36
|
+
'event': order.event.slug,
|
|
37
|
+
'code': order.code,
|
|
38
|
+
'secret': order.secret,
|
|
39
|
+
}),
|
|
40
|
+
('presale:event.order', {
|
|
41
|
+
'event': order.event.slug,
|
|
42
|
+
'code': order.code,
|
|
43
|
+
'secret': order.secret,
|
|
44
|
+
}),
|
|
45
|
+
('presale:event.order.by_code', {
|
|
46
|
+
'organizer': order.event.organizer.slug,
|
|
47
|
+
'event': order.event.slug,
|
|
48
|
+
'code': order.code,
|
|
49
|
+
'secret': order.secret,
|
|
50
|
+
}),
|
|
51
|
+
('presale:event.order.by_code', {
|
|
52
|
+
'event': order.event.slug,
|
|
53
|
+
'code': order.code,
|
|
54
|
+
'secret': order.secret,
|
|
55
|
+
}),
|
|
56
|
+
]
|
|
57
|
+
for name, kwargs in patterns:
|
|
58
|
+
try:
|
|
59
|
+
return reverse(name, kwargs=kwargs)
|
|
60
|
+
except NoReverseMatch:
|
|
61
|
+
continue
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
try:
|
|
65
|
+
organizer = order.event.organizer.slug
|
|
66
|
+
event = order.event.slug
|
|
67
|
+
code = order.code
|
|
68
|
+
secret = order.secret
|
|
69
|
+
path = f'/{organizer}/{event}/order/{code}/{secret}/'
|
|
70
|
+
return request.build_absolute_uri(path)
|
|
71
|
+
except Exception:
|
|
72
|
+
return '/'
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _store_thepay_state(payment_obj: OrderPayment, state: str) -> None:
|
|
76
|
+
info = getattr(payment_obj, 'info_data', None)
|
|
77
|
+
if info is None:
|
|
78
|
+
info = getattr(payment_obj, 'info', None) or {}
|
|
79
|
+
info['thepay_state'] = state
|
|
80
|
+
if hasattr(payment_obj, 'info_data'):
|
|
81
|
+
payment_obj.info_data = info
|
|
82
|
+
else:
|
|
83
|
+
payment_obj.info = info
|
|
84
|
+
payment_obj.save(update_fields=['info'])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@method_decorator(csrf_exempt, name='dispatch')
|
|
88
|
+
class ReturnView(View):
|
|
89
|
+
"""
|
|
90
|
+
Handle user redirect return from The Pay payment gateway.
|
|
91
|
+
|
|
92
|
+
Processes the payment response when the customer is redirected back
|
|
93
|
+
from the The Pay gateway after completing or canceling payment.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def get(self, request: HttpRequest, order: str, payment: int, hash: str):
|
|
97
|
+
return self._handle_response(request, order, payment, hash)
|
|
98
|
+
|
|
99
|
+
def post(self, request: HttpRequest, order: str, payment: int, hash: str):
|
|
100
|
+
return self._handle_response(request, order, payment, hash)
|
|
101
|
+
|
|
102
|
+
def _handle_response(self, request: HttpRequest, order: str, payment: int, hash: str):
|
|
103
|
+
"""
|
|
104
|
+
Process The Pay return response.
|
|
105
|
+
|
|
106
|
+
Verifies the payment signature, checks payment status,
|
|
107
|
+
and updates the payment state accordingly.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
request: HTTP request containing The Pay response parameters
|
|
111
|
+
order: Order code
|
|
112
|
+
payment: Payment ID
|
|
113
|
+
hash: Order secret hash for validation
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
with scopes_disabled():
|
|
117
|
+
payment_obj = get_object_or_404(
|
|
118
|
+
OrderPayment,
|
|
119
|
+
id=payment,
|
|
120
|
+
order__code=order,
|
|
121
|
+
order__secret=hash
|
|
122
|
+
)
|
|
123
|
+
order_obj = payment_obj.order
|
|
124
|
+
event = order_obj.event
|
|
125
|
+
|
|
126
|
+
# Get payment provider
|
|
127
|
+
from pretix.base.models import Event
|
|
128
|
+
provider = event.get_payment_providers().get('thepay')
|
|
129
|
+
if not provider:
|
|
130
|
+
logger.error('The Pay provider not found')
|
|
131
|
+
messages.error(request, _('Payment provider not configured.'))
|
|
132
|
+
return redirect(order_obj.get_abandon_url())
|
|
133
|
+
|
|
134
|
+
# Get settings
|
|
135
|
+
settings_dict = provider.settings
|
|
136
|
+
api_password = settings_dict.get('api_password', '')
|
|
137
|
+
|
|
138
|
+
# Get response parameters
|
|
139
|
+
# The Pay return URL includes payment_uid and project_id
|
|
140
|
+
payment_uid = (
|
|
141
|
+
request.GET.get('payment_uid', '') or request.POST.get('payment_uid', '') or
|
|
142
|
+
request.GET.get('uid', '') or request.POST.get('uid', '')
|
|
143
|
+
)
|
|
144
|
+
signature = request.GET.get('signature', '') or request.POST.get('signature', '')
|
|
145
|
+
|
|
146
|
+
# If no payment_uid from callback, extract from payment.info (stored during creation)
|
|
147
|
+
if not payment_uid:
|
|
148
|
+
payment_info = getattr(payment_obj, 'info_data', None)
|
|
149
|
+
if payment_info is None:
|
|
150
|
+
payment_info = getattr(payment_obj, 'info', None) or {}
|
|
151
|
+
payment_uid = payment_info.get('payment_uid') or payment_info.get('uid')
|
|
152
|
+
# Fallback: reconstruct from stored pattern
|
|
153
|
+
if not payment_uid:
|
|
154
|
+
payment_uid = f'pretix-{payment_obj.id}-{order_obj.code}'
|
|
155
|
+
|
|
156
|
+
# Verify signature if provided
|
|
157
|
+
if signature and api_password:
|
|
158
|
+
params = dict(request.GET.items()) if request.method == 'GET' else dict(request.POST.items())
|
|
159
|
+
params.pop('signature', None) # Remove signature from params for verification
|
|
160
|
+
|
|
161
|
+
if not provider._verify_signature(params, signature, api_password):
|
|
162
|
+
logger.error('The Pay signature verification failed')
|
|
163
|
+
messages.error(request, _('Payment verification failed.'))
|
|
164
|
+
return redirect(order_obj.get_abandon_url())
|
|
165
|
+
|
|
166
|
+
# Always query payment status via API (per The Pay spec)
|
|
167
|
+
payment_state = None
|
|
168
|
+
if payment_uid:
|
|
169
|
+
merchant_id = settings_dict.get('merchant_id', '')
|
|
170
|
+
project_id = settings_dict.get('project_id', '')
|
|
171
|
+
test_mode = settings_dict.get('test_mode', False)
|
|
172
|
+
api_url = provider._get_api_url(test_mode).rstrip('/')
|
|
173
|
+
payment_state = provider._get_payment_status(
|
|
174
|
+
payment_uid, merchant_id, project_id, api_password, api_url
|
|
175
|
+
)
|
|
176
|
+
logger.info('The Pay return status for payment %s (uid=%s): %s', payment, payment_uid, payment_state)
|
|
177
|
+
|
|
178
|
+
# Check payment status
|
|
179
|
+
# The Pay payment states: 'paid', 'waiting_for_payment', 'expired', 'error', etc.
|
|
180
|
+
# According to OpenAPI spec: Enum_PaymentState
|
|
181
|
+
if payment_state:
|
|
182
|
+
_store_thepay_state(payment_obj, payment_state)
|
|
183
|
+
if payment_state in ('paid', 'refunded', 'partially_refunded'):
|
|
184
|
+
allowed_states = {OrderPayment.PAYMENT_STATE_PENDING}
|
|
185
|
+
created_state = getattr(OrderPayment, 'PAYMENT_STATE_CREATED', None)
|
|
186
|
+
if created_state:
|
|
187
|
+
allowed_states.add(created_state)
|
|
188
|
+
if payment_obj.state in allowed_states:
|
|
189
|
+
logger.info('The Pay payment %s state before confirm: %s', payment, payment_obj.state)
|
|
190
|
+
with scopes_disabled():
|
|
191
|
+
payment_obj.confirm()
|
|
192
|
+
logger.info('The Pay payment %s confirmed for order %s', payment, order)
|
|
193
|
+
if payment_state == 'paid':
|
|
194
|
+
messages.success(request, _('Payment successful!'))
|
|
195
|
+
else:
|
|
196
|
+
messages.warning(request, _('Payment was refunded on The Pay side.'))
|
|
197
|
+
else:
|
|
198
|
+
logger.info('The Pay payment %s already in state %s; skipping confirm', payment, payment_obj.state)
|
|
199
|
+
return redirect(_order_redirect_url(request, order_obj))
|
|
200
|
+
elif payment_state in ('expired', 'error', 'preauth_cancelled', 'preauth_expired'):
|
|
201
|
+
error_msg = _('Payment failed or expired.')
|
|
202
|
+
if payment_obj.state == OrderPayment.PAYMENT_STATE_PENDING:
|
|
203
|
+
with scopes_disabled():
|
|
204
|
+
payment_obj.fail(info={'error': f'Payment state: {payment_state}'})
|
|
205
|
+
logger.warning(f'The Pay payment {payment} failed for order {order}: {payment_state}')
|
|
206
|
+
messages.error(request, error_msg)
|
|
207
|
+
return redirect(order_obj.get_abandon_url())
|
|
208
|
+
elif payment_state in ('waiting_for_payment', 'waiting_for_confirmation', 'preauthorized'):
|
|
209
|
+
# Payment is still processing
|
|
210
|
+
logger.info(f'The Pay payment {payment} is still processing: {payment_state}')
|
|
211
|
+
return redirect(_order_redirect_url(request, order_obj))
|
|
212
|
+
else:
|
|
213
|
+
# Unknown status or no status - keep pending
|
|
214
|
+
if payment_state:
|
|
215
|
+
logger.warning(f'The Pay payment {payment} returned unknown status: {payment_state}')
|
|
216
|
+
else:
|
|
217
|
+
logger.warning(f'The Pay payment {payment} - no status in callback and API query failed')
|
|
218
|
+
return redirect(_order_redirect_url(request, order_obj))
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.error(f'Error processing The Pay return: {e}', exc_info=True)
|
|
222
|
+
messages.error(request, _('An error occurred while processing your payment.'))
|
|
223
|
+
try:
|
|
224
|
+
return redirect(order_obj.get_abandon_url())
|
|
225
|
+
except:
|
|
226
|
+
return HttpResponseBadRequest('Error processing payment')
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@method_decorator(csrf_exempt, name='dispatch')
|
|
230
|
+
class NotifyView(View):
|
|
231
|
+
"""
|
|
232
|
+
Handle server-to-server notification (IPN) from The Pay payment gateway.
|
|
233
|
+
|
|
234
|
+
Processes asynchronous payment notifications sent by The Pay to confirm
|
|
235
|
+
payment status independently of user redirect.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
def get(self, request: HttpRequest, order: str, payment: int, hash: str):
|
|
239
|
+
return self._handle_notification(request, order, payment, hash)
|
|
240
|
+
|
|
241
|
+
def post(self, request: HttpRequest, order: str, payment: int, hash: str):
|
|
242
|
+
return self._handle_notification(request, order, payment, hash)
|
|
243
|
+
|
|
244
|
+
def _handle_notification(self, request: HttpRequest, order: str, payment: int, hash: str):
|
|
245
|
+
"""
|
|
246
|
+
Process The Pay server notification.
|
|
247
|
+
|
|
248
|
+
Verifies the notification signature, checks payment status,
|
|
249
|
+
and updates the payment state. Returns HTTP 200 OK on success.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
request: HTTP request containing The Pay notification parameters
|
|
253
|
+
order: Order code
|
|
254
|
+
payment: Payment ID
|
|
255
|
+
hash: Order secret hash for validation
|
|
256
|
+
"""
|
|
257
|
+
try:
|
|
258
|
+
with scopes_disabled():
|
|
259
|
+
payment_obj = get_object_or_404(
|
|
260
|
+
OrderPayment,
|
|
261
|
+
id=payment,
|
|
262
|
+
order__code=order,
|
|
263
|
+
order__secret=hash
|
|
264
|
+
)
|
|
265
|
+
order_obj = payment_obj.order
|
|
266
|
+
event = order_obj.event
|
|
267
|
+
|
|
268
|
+
# Get payment provider
|
|
269
|
+
from pretix.base.models import Event
|
|
270
|
+
provider = event.get_payment_providers().get('thepay')
|
|
271
|
+
if not provider:
|
|
272
|
+
logger.error('The Pay provider not found')
|
|
273
|
+
return HttpResponseBadRequest('Provider not configured')
|
|
274
|
+
|
|
275
|
+
# Get settings
|
|
276
|
+
settings_dict = provider.settings
|
|
277
|
+
api_password = settings_dict.get('api_password', '')
|
|
278
|
+
|
|
279
|
+
# Get notification parameters
|
|
280
|
+
# The Pay notifications include: payment_uid, project_id, type
|
|
281
|
+
payment_uid = (
|
|
282
|
+
request.GET.get('payment_uid', '') or request.POST.get('payment_uid', '') or
|
|
283
|
+
request.GET.get('uid', '') or request.POST.get('uid', '')
|
|
284
|
+
)
|
|
285
|
+
signature = request.GET.get('signature', '') or request.POST.get('signature', '')
|
|
286
|
+
|
|
287
|
+
# If no payment_uid from notification, extract from payment.info (stored during creation)
|
|
288
|
+
if not payment_uid:
|
|
289
|
+
payment_info = getattr(payment_obj, 'info_data', None)
|
|
290
|
+
if payment_info is None:
|
|
291
|
+
payment_info = getattr(payment_obj, 'info', None) or {}
|
|
292
|
+
payment_uid = payment_info.get('payment_uid') or payment_info.get('uid')
|
|
293
|
+
# Fallback: reconstruct from stored pattern
|
|
294
|
+
if not payment_uid:
|
|
295
|
+
payment_uid = f'pretix-{payment_obj.id}-{order_obj.code}'
|
|
296
|
+
|
|
297
|
+
# Verify signature if provided
|
|
298
|
+
if signature and api_password:
|
|
299
|
+
params = dict(request.GET.items()) if request.method == 'GET' else dict(request.POST.items())
|
|
300
|
+
params.pop('signature', None)
|
|
301
|
+
|
|
302
|
+
if not provider._verify_signature(params, signature, api_password):
|
|
303
|
+
logger.error('The Pay notification signature verification failed')
|
|
304
|
+
return HttpResponseBadRequest('Invalid signature')
|
|
305
|
+
|
|
306
|
+
# Always query payment status via API (per The Pay spec)
|
|
307
|
+
payment_state = None
|
|
308
|
+
if payment_uid:
|
|
309
|
+
merchant_id = settings_dict.get('merchant_id', '')
|
|
310
|
+
project_id = settings_dict.get('project_id', '')
|
|
311
|
+
test_mode = settings_dict.get('test_mode', False)
|
|
312
|
+
api_url = provider._get_api_url(test_mode).rstrip('/')
|
|
313
|
+
payment_state = provider._get_payment_status(
|
|
314
|
+
payment_uid, merchant_id, project_id, api_password, api_url
|
|
315
|
+
)
|
|
316
|
+
logger.info('The Pay notify status for payment %s (uid=%s): %s', payment, payment_uid, payment_state)
|
|
317
|
+
|
|
318
|
+
# Process payment status
|
|
319
|
+
# The Pay payment states: 'paid', 'waiting_for_payment', 'expired', 'error', etc.
|
|
320
|
+
# Notification type 'state_changed' indicates payment state has changed
|
|
321
|
+
if payment_state:
|
|
322
|
+
_store_thepay_state(payment_obj, payment_state)
|
|
323
|
+
if payment_state in ('paid', 'refunded', 'partially_refunded'):
|
|
324
|
+
allowed_states = {OrderPayment.PAYMENT_STATE_PENDING}
|
|
325
|
+
created_state = getattr(OrderPayment, 'PAYMENT_STATE_CREATED', None)
|
|
326
|
+
if created_state:
|
|
327
|
+
allowed_states.add(created_state)
|
|
328
|
+
if payment_obj.state in allowed_states:
|
|
329
|
+
logger.info('The Pay payment %s state before confirm (notify): %s', payment, payment_obj.state)
|
|
330
|
+
with scopes_disabled():
|
|
331
|
+
payment_obj.confirm()
|
|
332
|
+
logger.info('The Pay payment %s confirmed via notification for order %s', payment, order)
|
|
333
|
+
else:
|
|
334
|
+
logger.info('The Pay payment %s already in state %s; skipping confirm', payment, payment_obj.state)
|
|
335
|
+
elif payment_state in ('expired', 'error', 'preauth_cancelled', 'preauth_expired'):
|
|
336
|
+
if payment_obj.state == OrderPayment.PAYMENT_STATE_PENDING:
|
|
337
|
+
with scopes_disabled():
|
|
338
|
+
payment_obj.fail(info={'error': f'Payment state: {payment_state}'})
|
|
339
|
+
logger.warning(f'The Pay payment {payment} failed via notification for order {order}: {payment_state}')
|
|
340
|
+
|
|
341
|
+
return HttpResponse('OK')
|
|
342
|
+
|
|
343
|
+
except Exception as e:
|
|
344
|
+
logger.error(f'Error processing The Pay notification: {e}', exc_info=True)
|
|
345
|
+
return HttpResponseBadRequest('Error processing notification')
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pretix-thepay
|
|
3
|
+
Version: 9.0.18
|
|
4
|
+
Summary: The Pay payment provider for Pretix
|
|
5
|
+
Author-email: KrisIsNew <thepay.pretix@krisisnew.xyz>
|
|
6
|
+
Maintainer-email: KrisIsNew <thepay.pretix@krisisnew.xyz>
|
|
7
|
+
License: Apache-2.0
|
|
8
|
+
Project-URL: homepage, https://github.com/krisisnewxyz/pretix-thepay
|
|
9
|
+
Project-URL: repository, https://github.com/krisisnewxyz/pretix-thepay
|
|
10
|
+
Project-URL: documentation, https://github.com/krisisnewxyz/pretix-thepay
|
|
11
|
+
Project-URL: issues, https://github.com/krisisnewxyz/pretix-thepay/issues
|
|
12
|
+
Keywords: pretix,payment,thepay
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/x-rst
|
|
24
|
+
Requires-Dist: pretix >=2.7.0
|
|
25
|
+
Requires-Dist: requests >=2.25.0
|
|
26
|
+
|
|
27
|
+
pretix-thepay
|
|
28
|
+
=============
|
|
29
|
+
|
|
30
|
+
The Pay payment provider plugin for Pretix. It creates payments via The Pay API,
|
|
31
|
+
redirects customers to The Pay, and confirms payment status using the return and
|
|
32
|
+
notification callbacks.
|
|
33
|
+
|
|
34
|
+
Features
|
|
35
|
+
--------
|
|
36
|
+
|
|
37
|
+
- The Pay REST API integration (payment creation, status lookup)
|
|
38
|
+
- Redirect-based checkout flow
|
|
39
|
+
- Server-to-server notifications
|
|
40
|
+
- Refunds (full and partial)
|
|
41
|
+
- ISO 4217 currency support with correct minor units
|
|
42
|
+
- Demo mode via Test Mode setting
|
|
43
|
+
- Multi-language gateway support (Czech, Slovak, English)
|
|
44
|
+
|
|
45
|
+
Requirements
|
|
46
|
+
------------
|
|
47
|
+
|
|
48
|
+
- Pretix >= 2.7.0
|
|
49
|
+
- Python >= 3.8
|
|
50
|
+
- requests >= 2.25.0
|
|
51
|
+
|
|
52
|
+
Installation
|
|
53
|
+
------------
|
|
54
|
+
|
|
55
|
+
Install from PyPI::
|
|
56
|
+
|
|
57
|
+
pip install pretix-thepay
|
|
58
|
+
|
|
59
|
+
Or install from source::
|
|
60
|
+
|
|
61
|
+
pip install -e /path/to/pretix-thepay
|
|
62
|
+
|
|
63
|
+
Enable the plugin in ``pretix.cfg``::
|
|
64
|
+
|
|
65
|
+
[pretix]
|
|
66
|
+
plugins = pretix_thepay
|
|
67
|
+
|
|
68
|
+
Restart Pretix after enabling the plugin.
|
|
69
|
+
|
|
70
|
+
Configuration
|
|
71
|
+
-------------
|
|
72
|
+
|
|
73
|
+
In the Pretix control panel, enable The Pay and configure:
|
|
74
|
+
|
|
75
|
+
- **Merchant ID**: Your The Pay merchant ID
|
|
76
|
+
- **Project ID**: Your The Pay project ID
|
|
77
|
+
- **API Password**: Your The Pay API password
|
|
78
|
+
- **Language**: Default language for the payment gateway
|
|
79
|
+
- **Test Mode**: Enable to use the demo environment
|
|
80
|
+
|
|
81
|
+
Behavior
|
|
82
|
+
--------
|
|
83
|
+
|
|
84
|
+
1. The customer selects The Pay.
|
|
85
|
+
2. Pretix creates a payment at The Pay and redirects the customer.
|
|
86
|
+
3. The customer completes payment on The Pay.
|
|
87
|
+
4. Pretix confirms the payment by querying The Pay from the return URL and from
|
|
88
|
+
notifications.
|
|
89
|
+
|
|
90
|
+
Notes
|
|
91
|
+
-----
|
|
92
|
+
|
|
93
|
+
- Test Mode switches the API base URL to the demo environment.
|
|
94
|
+
- The Pay requires a customer name plus email or phone; Pretix order data is used.
|
|
95
|
+
|
|
96
|
+
Refunds
|
|
97
|
+
-------
|
|
98
|
+
|
|
99
|
+
Refunds are initiated from Pretix and sent to The Pay. The plugin supports full
|
|
100
|
+
and partial refunds for payments that support automatic refunds in The Pay.
|
|
101
|
+
|
|
102
|
+
Troubleshooting
|
|
103
|
+
---------------
|
|
104
|
+
|
|
105
|
+
Payment not created
|
|
106
|
+
^^^^^^^^^^^^^^^^^^^
|
|
107
|
+
|
|
108
|
+
- Verify Merchant ID, Project ID, and API Password
|
|
109
|
+
- Check Pretix logs for The Pay API response details
|
|
110
|
+
- Ensure the customer has a name and email or phone
|
|
111
|
+
|
|
112
|
+
Notifications not arriving
|
|
113
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
114
|
+
|
|
115
|
+
- Make sure Pretix is reachable from The Pay (public URL)
|
|
116
|
+
- Confirm your server IP is allowed in The Pay administration
|
|
117
|
+
|
|
118
|
+
Docker
|
|
119
|
+
------
|
|
120
|
+
|
|
121
|
+
For Pretix in Docker::
|
|
122
|
+
|
|
123
|
+
FROM pretix/standalone:stable
|
|
124
|
+
USER root
|
|
125
|
+
RUN pip3 install pretix-thepay
|
|
126
|
+
USER pretixuser
|
|
127
|
+
RUN cd /pretix/src && make production
|
|
128
|
+
|
|
129
|
+
License
|
|
130
|
+
-------
|
|
131
|
+
|
|
132
|
+
Apache Software License 2.0
|
|
133
|
+
|
|
134
|
+
References
|
|
135
|
+
----------
|
|
136
|
+
|
|
137
|
+
- `Pretix Plugin Development Guide <https://docs.pretix.eu/dev/development/api/index.html>`_
|
|
138
|
+
- `The Pay API Documentation <https://docs.thepay.eu/>`_
|
|
139
|
+
- `The Pay OpenAPI Spec <https://gate.thepay.cz/openapi.yaml>`_
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
pretix_thepay/__init__.py,sha256=WM1ftBLz5HhVzbLgstZ_85ohCNYu5BlUZCwSETxLWhc,449
|
|
2
|
+
pretix_thepay/apps.py,sha256=NuyUCL3dccqPU0DbGu4yS7JIey6kZ5-BXyYLvzDoNA0,1114
|
|
3
|
+
pretix_thepay/payment.py,sha256=nmQyk5-ZB6_mHAAzcgnfoAUpCVMbg5IrudSXgtP8gtQ,31717
|
|
4
|
+
pretix_thepay/pretix_plugin_urls.py,sha256=RDrQfsuBA326V7WBBKL6Q27aLCgXnGMINXaRYp6dO6U,211
|
|
5
|
+
pretix_thepay/signals.py,sha256=t0w6D5DPv7cZPbewgol7IMl7DKtMGF30q4i-0wwqlB0,530
|
|
6
|
+
pretix_thepay/urls.py,sha256=tG2vjSBIXtb4J0GuWrmjuC2NvXpvu3_5xf6kwZ9bHYI,478
|
|
7
|
+
pretix_thepay/views.py,sha256=lgeC5FPjDOtdqm8UtxqHmKbx7e3mu2kRlpUXMMa1rvQ,16159
|
|
8
|
+
pretix_thepay/locale/en/LC_MESSAGES/django.po,sha256=c2IiguXpHotpr3uBlDJR2GiVauyTpHMCFNHOqNMIn_M,104
|
|
9
|
+
pretix_thepay-9.0.18.dist-info/METADATA,sha256=vNqYJdlNc4vjv5a-QOSJz36OZ4KAeAQ77LthvaCd-OY,3828
|
|
10
|
+
pretix_thepay-9.0.18.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
11
|
+
pretix_thepay-9.0.18.dist-info/entry_points.txt,sha256=dlsZ6P-h88TN5VvfOmOGHQ-98-KCFjAdCi146Br5M-E,63
|
|
12
|
+
pretix_thepay-9.0.18.dist-info/top_level.txt,sha256=n1Ysl4WizfC1AMjn6xkNQ9vh7T7OR-1z8ZEnNc_79mg,14
|
|
13
|
+
pretix_thepay-9.0.18.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pretix_thepay
|