paytechuz 0.2.20__py3-none-any.whl → 0.2.22__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.
Potentially problematic release.
This version of paytechuz might be problematic. Click here for more details.
- paytechuz/integrations/fastapi/routes.py +32 -27
- {paytechuz-0.2.20.dist-info → paytechuz-0.2.22.dist-info}/METADATA +6 -33
- paytechuz-0.2.22.dist-info/RECORD +36 -0
- {paytechuz-0.2.20.dist-info → paytechuz-0.2.22.dist-info}/WHEEL +1 -1
- paytechuz-0.2.22.dist-info/top_level.txt +1 -0
- core/__init__.py +0 -0
- core/base.py +0 -97
- core/constants.py +0 -68
- core/exceptions.py +0 -190
- core/http.py +0 -268
- core/payme/errors.py +0 -25
- core/utils.py +0 -192
- gateways/__init__.py +0 -0
- gateways/click/__init__.py +0 -0
- gateways/click/client.py +0 -199
- gateways/click/merchant.py +0 -265
- gateways/click/webhook.py +0 -227
- gateways/payme/__init__.py +0 -0
- gateways/payme/cards.py +0 -222
- gateways/payme/client.py +0 -262
- gateways/payme/receipts.py +0 -336
- gateways/payme/webhook.py +0 -379
- integrations/__init__.py +0 -0
- integrations/django/__init__.py +0 -4
- integrations/django/admin.py +0 -78
- integrations/django/apps.py +0 -21
- integrations/django/migrations/0001_initial.py +0 -51
- integrations/django/migrations/__init__.py +0 -3
- integrations/django/models.py +0 -174
- integrations/django/signals.py +0 -46
- integrations/django/views.py +0 -102
- integrations/django/webhooks.py +0 -884
- integrations/fastapi/__init__.py +0 -21
- integrations/fastapi/models.py +0 -183
- integrations/fastapi/routes.py +0 -1038
- integrations/fastapi/schemas.py +0 -116
- paytechuz-0.2.20.dist-info/RECORD +0 -67
- paytechuz-0.2.20.dist-info/top_level.txt +0 -4
integrations/django/webhooks.py
DELETED
|
@@ -1,884 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Django webhook handlers for PayTechUZ.
|
|
3
|
-
"""
|
|
4
|
-
import base64
|
|
5
|
-
import hashlib
|
|
6
|
-
import json
|
|
7
|
-
import logging
|
|
8
|
-
from decimal import Decimal
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
|
|
11
|
-
from django.conf import settings
|
|
12
|
-
from django.http import JsonResponse
|
|
13
|
-
from django.utils.module_loading import import_string
|
|
14
|
-
from django.views import View
|
|
15
|
-
|
|
16
|
-
from paytechuz.core.exceptions import (
|
|
17
|
-
PermissionDenied,
|
|
18
|
-
InvalidAmount,
|
|
19
|
-
TransactionNotFound,
|
|
20
|
-
AccountNotFound,
|
|
21
|
-
MethodNotFound,
|
|
22
|
-
UnsupportedMethod
|
|
23
|
-
)
|
|
24
|
-
from .models import PaymentTransaction
|
|
25
|
-
|
|
26
|
-
logger = logging.getLogger(__name__)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class PaymeWebhook(View):
|
|
30
|
-
"""
|
|
31
|
-
Base Payme webhook handler for Django.
|
|
32
|
-
|
|
33
|
-
This class handles webhook requests from the Payme payment system.
|
|
34
|
-
You can extend this class and override the event methods to customize
|
|
35
|
-
the behavior.
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
def __init__(self, **kwargs):
|
|
39
|
-
super().__init__(**kwargs)
|
|
40
|
-
|
|
41
|
-
# Try to get the account model from settings
|
|
42
|
-
# Get the account model from settings
|
|
43
|
-
account_model_path = getattr(
|
|
44
|
-
settings, 'PAYME_ACCOUNT_MODEL', 'django.contrib.auth.models.User'
|
|
45
|
-
)
|
|
46
|
-
try:
|
|
47
|
-
self.account_model = import_string(account_model_path)
|
|
48
|
-
except ImportError:
|
|
49
|
-
# If the model is not found, log an error and raise an exception
|
|
50
|
-
logger.error(
|
|
51
|
-
"Could not import %s. Check PAYME_ACCOUNT_MODEL setting.",
|
|
52
|
-
account_model_path
|
|
53
|
-
)
|
|
54
|
-
raise ImportError(
|
|
55
|
-
f"Import error: {account_model_path}"
|
|
56
|
-
) from None
|
|
57
|
-
|
|
58
|
-
self.account_field = getattr(settings, 'PAYME_ACCOUNT_FIELD', 'id')
|
|
59
|
-
self.amount_field = getattr(settings, 'PAYME_AMOUNT_FIELD', 'amount')
|
|
60
|
-
self.one_time_payment = getattr(
|
|
61
|
-
settings, 'PAYME_ONE_TIME_PAYMENT', True
|
|
62
|
-
)
|
|
63
|
-
self.payme_id = getattr(settings, 'PAYME_ID', '')
|
|
64
|
-
self.payme_key = getattr(settings, 'PAYME_KEY', '')
|
|
65
|
-
|
|
66
|
-
def post(self, request, **_):
|
|
67
|
-
"""
|
|
68
|
-
Handle POST requests from Payme.
|
|
69
|
-
"""
|
|
70
|
-
try:
|
|
71
|
-
# Check authorization
|
|
72
|
-
self._check_auth(request)
|
|
73
|
-
|
|
74
|
-
# Parse request data
|
|
75
|
-
data = json.loads(request.body.decode('utf-8'))
|
|
76
|
-
method = data.get('method')
|
|
77
|
-
params = data.get('params', {})
|
|
78
|
-
request_id = data.get('id', 0)
|
|
79
|
-
|
|
80
|
-
# Process the request based on the method
|
|
81
|
-
if method == 'CheckPerformTransaction':
|
|
82
|
-
result = self._check_perform_transaction(params)
|
|
83
|
-
elif method == 'CreateTransaction':
|
|
84
|
-
result = self._create_transaction(params)
|
|
85
|
-
elif method == 'PerformTransaction':
|
|
86
|
-
result = self._perform_transaction(params)
|
|
87
|
-
elif method == 'CheckTransaction':
|
|
88
|
-
result = self._check_transaction(params)
|
|
89
|
-
elif method == 'CancelTransaction':
|
|
90
|
-
result = self._cancel_transaction(params)
|
|
91
|
-
elif method == 'GetStatement':
|
|
92
|
-
result = self._get_statement(params)
|
|
93
|
-
else:
|
|
94
|
-
raise MethodNotFound(f"Method not supported: {method}")
|
|
95
|
-
|
|
96
|
-
# Return the result
|
|
97
|
-
return JsonResponse({
|
|
98
|
-
'jsonrpc': '2.0',
|
|
99
|
-
'id': request_id,
|
|
100
|
-
'result': result
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
except PermissionDenied as e:
|
|
104
|
-
return JsonResponse({
|
|
105
|
-
'jsonrpc': '2.0',
|
|
106
|
-
'id': request_id if 'request_id' in locals() else 0,
|
|
107
|
-
'error': {
|
|
108
|
-
'code': -32504,
|
|
109
|
-
'message': str(e)
|
|
110
|
-
}
|
|
111
|
-
}, status=200) # Return 200 status code for all errors
|
|
112
|
-
|
|
113
|
-
except (MethodNotFound, UnsupportedMethod) as e:
|
|
114
|
-
return JsonResponse({
|
|
115
|
-
'jsonrpc': '2.0',
|
|
116
|
-
'id': request_id if 'request_id' in locals() else 0,
|
|
117
|
-
'error': {
|
|
118
|
-
'code': -32601,
|
|
119
|
-
'message': str(e)
|
|
120
|
-
}
|
|
121
|
-
}, status=200) # Return 200 status code for all errors
|
|
122
|
-
|
|
123
|
-
except AccountNotFound as e:
|
|
124
|
-
return JsonResponse({
|
|
125
|
-
'jsonrpc': '2.0',
|
|
126
|
-
'id': request_id if 'request_id' in locals() else 0,
|
|
127
|
-
'error': {
|
|
128
|
-
# Code for account not found, in the range -31099 to -31050
|
|
129
|
-
'code': -31050,
|
|
130
|
-
'message': str(e)
|
|
131
|
-
}
|
|
132
|
-
}, status=200) # Return 200 status code for all errors
|
|
133
|
-
|
|
134
|
-
except (InvalidAmount, TransactionNotFound) as e:
|
|
135
|
-
return JsonResponse({
|
|
136
|
-
'jsonrpc': '2.0',
|
|
137
|
-
'id': request_id if 'request_id' in locals() else 0,
|
|
138
|
-
'error': {
|
|
139
|
-
'code': -31001,
|
|
140
|
-
'message': str(e)
|
|
141
|
-
}
|
|
142
|
-
}, status=200) # Return 200 status code for all errors
|
|
143
|
-
|
|
144
|
-
except Exception as e: # pylint: disable=broad-except
|
|
145
|
-
logger.exception("Unexpected error in Payme webhook: %s", e)
|
|
146
|
-
return JsonResponse({
|
|
147
|
-
'jsonrpc': '2.0',
|
|
148
|
-
'id': request_id if 'request_id' in locals() else 0,
|
|
149
|
-
'error': {
|
|
150
|
-
'code': -32400,
|
|
151
|
-
'message': 'Internal error'
|
|
152
|
-
}
|
|
153
|
-
}, status=200) # Return 200 status code for all errors
|
|
154
|
-
|
|
155
|
-
def _check_auth(self, request):
|
|
156
|
-
"""
|
|
157
|
-
Check authorization header.
|
|
158
|
-
"""
|
|
159
|
-
auth_header = request.META.get('HTTP_AUTHORIZATION')
|
|
160
|
-
if not auth_header:
|
|
161
|
-
raise PermissionDenied("Missing authentication credentials")
|
|
162
|
-
|
|
163
|
-
try:
|
|
164
|
-
auth_parts = auth_header.split()
|
|
165
|
-
if len(auth_parts) != 2 or auth_parts[0].lower() != 'basic':
|
|
166
|
-
raise PermissionDenied("Invalid authentication format")
|
|
167
|
-
|
|
168
|
-
auth_decoded = base64.b64decode(auth_parts[1]).decode('utf-8')
|
|
169
|
-
_, password = auth_decoded.split(':') # We only need the password
|
|
170
|
-
|
|
171
|
-
if password != self.payme_key:
|
|
172
|
-
raise PermissionDenied("Invalid merchant key")
|
|
173
|
-
except Exception as e:
|
|
174
|
-
logger.error("Authentication error: %s", e)
|
|
175
|
-
raise PermissionDenied("Authentication error") from e
|
|
176
|
-
|
|
177
|
-
def _find_account(self, params):
|
|
178
|
-
"""
|
|
179
|
-
Find account by parameters.
|
|
180
|
-
"""
|
|
181
|
-
account_value = params.get('account', {}).get(self.account_field)
|
|
182
|
-
if not account_value:
|
|
183
|
-
raise AccountNotFound("Account not found in parameters")
|
|
184
|
-
|
|
185
|
-
try:
|
|
186
|
-
# Handle special case for 'order_id' field
|
|
187
|
-
# Handle special case for 'order_id' field
|
|
188
|
-
lookup_field = 'id' if self.account_field == 'order_id' else (
|
|
189
|
-
self.account_field
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
# Convert account_value to int if needed
|
|
193
|
-
if (lookup_field == 'id' and isinstance(account_value, str)
|
|
194
|
-
and account_value.isdigit()):
|
|
195
|
-
account_value = int(account_value)
|
|
196
|
-
|
|
197
|
-
# Use model manager to find account
|
|
198
|
-
lookup_kwargs = {lookup_field: account_value}
|
|
199
|
-
account = self.account_model._default_manager.get(**lookup_kwargs)
|
|
200
|
-
return account
|
|
201
|
-
except self.account_model.DoesNotExist:
|
|
202
|
-
raise AccountNotFound(
|
|
203
|
-
f"Account with {self.account_field}={account_value} not found"
|
|
204
|
-
) from None
|
|
205
|
-
|
|
206
|
-
def _validate_amount(self, account, amount):
|
|
207
|
-
"""
|
|
208
|
-
Validate payment amount.
|
|
209
|
-
"""
|
|
210
|
-
# If one_time_payment is disabled, we still validate the amount
|
|
211
|
-
# but we don't require it to match exactly
|
|
212
|
-
|
|
213
|
-
expected_amount = Decimal(getattr(account, self.amount_field)) * 100
|
|
214
|
-
received_amount = Decimal(amount)
|
|
215
|
-
|
|
216
|
-
# If one_time_payment is enabled, amount must match exactly
|
|
217
|
-
if self.one_time_payment and expected_amount != received_amount:
|
|
218
|
-
raise InvalidAmount(
|
|
219
|
-
f"Invalid amount. Expected: {expected_amount}, "
|
|
220
|
-
f"received: {received_amount}"
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
# If one_time_payment is disabled, amount must be positive
|
|
224
|
-
if not self.one_time_payment and received_amount <= 0:
|
|
225
|
-
raise InvalidAmount(
|
|
226
|
-
f"Invalid amount. Amount must be positive, "
|
|
227
|
-
f"received: {received_amount}"
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
return True
|
|
231
|
-
|
|
232
|
-
def _check_perform_transaction(self, params):
|
|
233
|
-
"""
|
|
234
|
-
Handle CheckPerformTransaction method.
|
|
235
|
-
"""
|
|
236
|
-
account = self._find_account(params)
|
|
237
|
-
self._validate_amount(account, params.get('amount'))
|
|
238
|
-
|
|
239
|
-
# Call the event method
|
|
240
|
-
self.before_check_perform_transaction(params, account)
|
|
241
|
-
|
|
242
|
-
return {'allow': True}
|
|
243
|
-
|
|
244
|
-
def _create_transaction(self, params):
|
|
245
|
-
"""
|
|
246
|
-
Handle CreateTransaction method.
|
|
247
|
-
"""
|
|
248
|
-
transaction_id = params.get('id')
|
|
249
|
-
account = self._find_account(params)
|
|
250
|
-
amount = params.get('amount')
|
|
251
|
-
|
|
252
|
-
self._validate_amount(account, amount)
|
|
253
|
-
|
|
254
|
-
# Check if there's already a transaction for this account
|
|
255
|
-
# with a different transaction_id
|
|
256
|
-
# Only check if one_time_payment is enabled
|
|
257
|
-
if self.one_time_payment:
|
|
258
|
-
# Check for existing transactions in non-final states
|
|
259
|
-
existing_transactions = PaymentTransaction._default_manager.filter(
|
|
260
|
-
gateway=PaymentTransaction.PAYME,
|
|
261
|
-
account_id=account.id
|
|
262
|
-
).exclude(transaction_id=transaction_id)
|
|
263
|
-
|
|
264
|
-
# Filter out transactions in final states
|
|
265
|
-
non_final_transactions = existing_transactions.exclude(
|
|
266
|
-
state__in=[
|
|
267
|
-
PaymentTransaction.SUCCESSFULLY,
|
|
268
|
-
PaymentTransaction.CANCELLED
|
|
269
|
-
]
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
if non_final_transactions.exists():
|
|
273
|
-
# If there's already a transaction for this account with a different
|
|
274
|
-
# transaction ID in a non-final state, raise an error
|
|
275
|
-
msg = (
|
|
276
|
-
f"Account with {self.account_field}={account.id} "
|
|
277
|
-
"already has a pending transaction"
|
|
278
|
-
)
|
|
279
|
-
raise AccountNotFound(msg)
|
|
280
|
-
|
|
281
|
-
# Check for existing transaction with the same transaction_id
|
|
282
|
-
try:
|
|
283
|
-
transaction = PaymentTransaction._default_manager.get(
|
|
284
|
-
gateway=PaymentTransaction.PAYME,
|
|
285
|
-
transaction_id=transaction_id
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
# Call the event method
|
|
289
|
-
self.transaction_already_exists(params, transaction)
|
|
290
|
-
|
|
291
|
-
return {
|
|
292
|
-
'transaction': transaction.transaction_id,
|
|
293
|
-
'state': transaction.state,
|
|
294
|
-
'create_time': int(transaction.created_at.timestamp() * 1000),
|
|
295
|
-
}
|
|
296
|
-
except PaymentTransaction.DoesNotExist:
|
|
297
|
-
# No existing transaction found, continue with creation
|
|
298
|
-
pass
|
|
299
|
-
|
|
300
|
-
# Create new transaction
|
|
301
|
-
transaction = PaymentTransaction.create_transaction(
|
|
302
|
-
gateway=PaymentTransaction.PAYME,
|
|
303
|
-
transaction_id=transaction_id,
|
|
304
|
-
account_id=account.id,
|
|
305
|
-
amount=Decimal(amount) / 100, # Convert from tiyin to som
|
|
306
|
-
extra_data={
|
|
307
|
-
'account_field': self.account_field,
|
|
308
|
-
'account_value': (params.get('account', {}).get(
|
|
309
|
-
self.account_field
|
|
310
|
-
)),
|
|
311
|
-
'create_time': params.get('time'),
|
|
312
|
-
'raw_params': params
|
|
313
|
-
}
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
# Update state to INITIATING
|
|
317
|
-
transaction.state = PaymentTransaction.INITIATING
|
|
318
|
-
transaction.save()
|
|
319
|
-
|
|
320
|
-
# Call the event method
|
|
321
|
-
self.transaction_created(params, transaction, account)
|
|
322
|
-
|
|
323
|
-
return {
|
|
324
|
-
'transaction': transaction.transaction_id,
|
|
325
|
-
'state': transaction.state,
|
|
326
|
-
'create_time': int(transaction.created_at.timestamp() * 1000),
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
def _perform_transaction(self, params):
|
|
330
|
-
"""
|
|
331
|
-
Handle PerformTransaction method.
|
|
332
|
-
"""
|
|
333
|
-
transaction_id = params.get('id')
|
|
334
|
-
|
|
335
|
-
try:
|
|
336
|
-
transaction = PaymentTransaction._default_manager.get(
|
|
337
|
-
gateway=PaymentTransaction.PAYME,
|
|
338
|
-
transaction_id=transaction_id
|
|
339
|
-
)
|
|
340
|
-
except PaymentTransaction.DoesNotExist:
|
|
341
|
-
raise TransactionNotFound(
|
|
342
|
-
f"Transaction {transaction_id} not found"
|
|
343
|
-
) from None
|
|
344
|
-
|
|
345
|
-
# Mark transaction as paid
|
|
346
|
-
transaction.mark_as_paid()
|
|
347
|
-
|
|
348
|
-
# Call the event method
|
|
349
|
-
self.successfully_payment(params, transaction)
|
|
350
|
-
|
|
351
|
-
return {
|
|
352
|
-
'transaction': transaction.transaction_id,
|
|
353
|
-
'state': transaction.state,
|
|
354
|
-
'perform_time': (
|
|
355
|
-
int(transaction.performed_at.timestamp() * 1000)
|
|
356
|
-
if transaction.performed_at else 0
|
|
357
|
-
),
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
def _check_transaction(self, params):
|
|
361
|
-
"""
|
|
362
|
-
Handle CheckTransaction method.
|
|
363
|
-
"""
|
|
364
|
-
transaction_id = params.get('id')
|
|
365
|
-
|
|
366
|
-
try:
|
|
367
|
-
transaction = PaymentTransaction._default_manager.get(
|
|
368
|
-
gateway=PaymentTransaction.PAYME,
|
|
369
|
-
transaction_id=transaction_id
|
|
370
|
-
)
|
|
371
|
-
except PaymentTransaction.DoesNotExist:
|
|
372
|
-
raise TransactionNotFound(
|
|
373
|
-
f"Transaction {transaction_id} not found"
|
|
374
|
-
) from None
|
|
375
|
-
|
|
376
|
-
# Call the event method
|
|
377
|
-
self.check_transaction(params, transaction)
|
|
378
|
-
|
|
379
|
-
return {
|
|
380
|
-
'transaction': transaction.transaction_id,
|
|
381
|
-
'state': transaction.state,
|
|
382
|
-
'create_time': int(transaction.created_at.timestamp() * 1000),
|
|
383
|
-
'perform_time': (
|
|
384
|
-
int(transaction.performed_at.timestamp() * 1000)
|
|
385
|
-
if transaction.performed_at else 0
|
|
386
|
-
),
|
|
387
|
-
'cancel_time': (
|
|
388
|
-
int(transaction.cancelled_at.timestamp() * 1000)
|
|
389
|
-
if transaction.cancelled_at else 0
|
|
390
|
-
),
|
|
391
|
-
'reason': transaction.reason,
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
def _cancel_response(self, transaction):
|
|
395
|
-
"""
|
|
396
|
-
Helper method to generate cancel transaction response.
|
|
397
|
-
|
|
398
|
-
Args:
|
|
399
|
-
transaction: Transaction object
|
|
400
|
-
|
|
401
|
-
Returns:
|
|
402
|
-
Dict containing the response
|
|
403
|
-
"""
|
|
404
|
-
return {
|
|
405
|
-
'transaction': transaction.transaction_id,
|
|
406
|
-
'state': transaction.state,
|
|
407
|
-
'cancel_time': (
|
|
408
|
-
int(transaction.cancelled_at.timestamp() * 1000)
|
|
409
|
-
if transaction.cancelled_at else 0
|
|
410
|
-
),
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
def _cancel_transaction(self, params):
|
|
414
|
-
"""
|
|
415
|
-
Handle CancelTransaction method.
|
|
416
|
-
"""
|
|
417
|
-
transaction_id = params.get('id')
|
|
418
|
-
reason = params.get('reason')
|
|
419
|
-
|
|
420
|
-
try:
|
|
421
|
-
transaction = PaymentTransaction._default_manager.get(
|
|
422
|
-
gateway=PaymentTransaction.PAYME,
|
|
423
|
-
transaction_id=transaction_id
|
|
424
|
-
)
|
|
425
|
-
except PaymentTransaction.DoesNotExist:
|
|
426
|
-
raise TransactionNotFound(
|
|
427
|
-
f"Transaction {transaction_id} not found"
|
|
428
|
-
) from None
|
|
429
|
-
|
|
430
|
-
# Check if transaction is already cancelled
|
|
431
|
-
if transaction.state == PaymentTransaction.CANCELLED:
|
|
432
|
-
# If transaction is already cancelled, return the existing data
|
|
433
|
-
return self._cancel_response(transaction)
|
|
434
|
-
|
|
435
|
-
if reason == 3:
|
|
436
|
-
transaction.state = PaymentTransaction.CANCELLED_DURING_INIT
|
|
437
|
-
transaction.save()
|
|
438
|
-
|
|
439
|
-
# Use the mark_as_cancelled method to properly store the reason
|
|
440
|
-
transaction.mark_as_cancelled(reason=reason)
|
|
441
|
-
|
|
442
|
-
# Call the event method
|
|
443
|
-
self.cancelled_payment(params, transaction)
|
|
444
|
-
|
|
445
|
-
# Return cancel response
|
|
446
|
-
return self._cancel_response(transaction)
|
|
447
|
-
|
|
448
|
-
def _get_statement(self, params):
|
|
449
|
-
"""
|
|
450
|
-
Handle GetStatement method.
|
|
451
|
-
"""
|
|
452
|
-
from_date = params.get('from')
|
|
453
|
-
to_date = params.get('to')
|
|
454
|
-
|
|
455
|
-
# Convert milliseconds to datetime objects
|
|
456
|
-
if from_date:
|
|
457
|
-
from_datetime = datetime.fromtimestamp(from_date / 1000)
|
|
458
|
-
else:
|
|
459
|
-
from_datetime = datetime.fromtimestamp(0) # Unix epoch start
|
|
460
|
-
|
|
461
|
-
if to_date:
|
|
462
|
-
to_datetime = datetime.fromtimestamp(to_date / 1000)
|
|
463
|
-
else:
|
|
464
|
-
to_datetime = datetime.now() # Current time
|
|
465
|
-
|
|
466
|
-
# Get transactions in the date range
|
|
467
|
-
transactions = PaymentTransaction._default_manager.filter(
|
|
468
|
-
gateway=PaymentTransaction.PAYME,
|
|
469
|
-
created_at__gte=from_datetime,
|
|
470
|
-
created_at__lte=to_datetime
|
|
471
|
-
)
|
|
472
|
-
|
|
473
|
-
# Format transactions for response
|
|
474
|
-
result = []
|
|
475
|
-
for transaction in transactions:
|
|
476
|
-
result.append({
|
|
477
|
-
'id': transaction.transaction_id,
|
|
478
|
-
'time': int(transaction.created_at.timestamp() * 1000),
|
|
479
|
-
'amount': int(transaction.amount * 100), # Convert to tiyin
|
|
480
|
-
'account': {
|
|
481
|
-
self.account_field: transaction.account_id
|
|
482
|
-
},
|
|
483
|
-
'state': transaction.state,
|
|
484
|
-
'create_time': int(transaction.created_at.timestamp() * 1000),
|
|
485
|
-
'perform_time': (
|
|
486
|
-
int(transaction.performed_at.timestamp() * 1000)
|
|
487
|
-
if transaction.performed_at else 0
|
|
488
|
-
),
|
|
489
|
-
'cancel_time': (
|
|
490
|
-
int(transaction.cancelled_at.timestamp() * 1000)
|
|
491
|
-
if transaction.cancelled_at else 0
|
|
492
|
-
),
|
|
493
|
-
'reason': transaction.reason,
|
|
494
|
-
})
|
|
495
|
-
|
|
496
|
-
# Call the event method
|
|
497
|
-
self.get_statement(params, result)
|
|
498
|
-
|
|
499
|
-
return {'transactions': result}
|
|
500
|
-
|
|
501
|
-
# Event methods that can be overridden by subclasses
|
|
502
|
-
|
|
503
|
-
def before_check_perform_transaction(self, params, account):
|
|
504
|
-
"""
|
|
505
|
-
Called before checking if a transaction can be performed.
|
|
506
|
-
|
|
507
|
-
Args:
|
|
508
|
-
params: Request parameters
|
|
509
|
-
account: Account object
|
|
510
|
-
"""
|
|
511
|
-
# This method is meant to be overridden by subclasses
|
|
512
|
-
|
|
513
|
-
def transaction_already_exists(self, params, transaction):
|
|
514
|
-
"""
|
|
515
|
-
Called when a transaction already exists.
|
|
516
|
-
|
|
517
|
-
Args:
|
|
518
|
-
params: Request parameters
|
|
519
|
-
transaction: Transaction object
|
|
520
|
-
"""
|
|
521
|
-
# This method is meant to be overridden by subclasses
|
|
522
|
-
|
|
523
|
-
def transaction_created(self, params, transaction, account):
|
|
524
|
-
"""
|
|
525
|
-
Called when a transaction is created.
|
|
526
|
-
|
|
527
|
-
Args:
|
|
528
|
-
params: Request parameters
|
|
529
|
-
transaction: Transaction object
|
|
530
|
-
account: Account object
|
|
531
|
-
"""
|
|
532
|
-
# This method is meant to be overridden by subclasses
|
|
533
|
-
|
|
534
|
-
def successfully_payment(self, params, transaction):
|
|
535
|
-
"""
|
|
536
|
-
Called when a payment is successful.
|
|
537
|
-
|
|
538
|
-
Args:
|
|
539
|
-
params: Request parameters
|
|
540
|
-
transaction: Transaction object
|
|
541
|
-
"""
|
|
542
|
-
# This method is meant to be overridden by subclasses
|
|
543
|
-
|
|
544
|
-
def check_transaction(self, params, transaction):
|
|
545
|
-
"""
|
|
546
|
-
Called when checking a transaction.
|
|
547
|
-
|
|
548
|
-
Args:
|
|
549
|
-
params: Request parameters
|
|
550
|
-
transaction: Transaction object
|
|
551
|
-
"""
|
|
552
|
-
# This method is meant to be overridden by subclasses
|
|
553
|
-
|
|
554
|
-
def cancelled_payment(self, params, transaction):
|
|
555
|
-
"""
|
|
556
|
-
Called when a payment is cancelled.
|
|
557
|
-
|
|
558
|
-
Args:
|
|
559
|
-
params: Request parameters
|
|
560
|
-
transaction: Transaction object
|
|
561
|
-
"""
|
|
562
|
-
# This method is meant to be overridden by subclasses
|
|
563
|
-
|
|
564
|
-
def get_statement(self, params, transactions):
|
|
565
|
-
"""
|
|
566
|
-
Called when getting a statement.
|
|
567
|
-
|
|
568
|
-
Args:
|
|
569
|
-
params: Request parameters
|
|
570
|
-
transactions: List of transactions
|
|
571
|
-
"""
|
|
572
|
-
# This method is meant to be overridden by subclasses
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
class ClickWebhook(View):
|
|
576
|
-
"""
|
|
577
|
-
Base Click webhook handler for Django.
|
|
578
|
-
|
|
579
|
-
This class handles webhook requests from the Click payment system.
|
|
580
|
-
You can extend this class and override the event methods to customize
|
|
581
|
-
the behavior.
|
|
582
|
-
"""
|
|
583
|
-
|
|
584
|
-
def __init__(self, **kwargs):
|
|
585
|
-
super().__init__(**kwargs)
|
|
586
|
-
|
|
587
|
-
# Try to get the account model from settings
|
|
588
|
-
# Get the account model from settings
|
|
589
|
-
account_model_path = getattr(
|
|
590
|
-
settings, 'CLICK_ACCOUNT_MODEL', 'django.contrib.auth.models.User'
|
|
591
|
-
)
|
|
592
|
-
try:
|
|
593
|
-
self.account_model = import_string(account_model_path)
|
|
594
|
-
except ImportError:
|
|
595
|
-
# If the model is not found, log an error and raise an exception
|
|
596
|
-
logger.error(
|
|
597
|
-
"Could not import %s. Check CLICK_ACCOUNT_MODEL setting.",
|
|
598
|
-
account_model_path
|
|
599
|
-
)
|
|
600
|
-
raise ImportError(
|
|
601
|
-
f"Import error: {account_model_path}"
|
|
602
|
-
) from None
|
|
603
|
-
|
|
604
|
-
self.service_id = getattr(settings, 'CLICK_SERVICE_ID', '')
|
|
605
|
-
self.secret_key = getattr(settings, 'CLICK_SECRET_KEY', '')
|
|
606
|
-
self.commission_percent = getattr(
|
|
607
|
-
settings, 'CLICK_COMMISSION_PERCENT', 0.0
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
def post(self, request, **_):
|
|
611
|
-
"""
|
|
612
|
-
Handle POST requests from Click.
|
|
613
|
-
"""
|
|
614
|
-
try:
|
|
615
|
-
# Get parameters from request
|
|
616
|
-
params = request.POST.dict()
|
|
617
|
-
|
|
618
|
-
# Check authorization
|
|
619
|
-
self._check_auth(params)
|
|
620
|
-
|
|
621
|
-
# Extract parameters
|
|
622
|
-
click_trans_id = params.get('click_trans_id')
|
|
623
|
-
merchant_trans_id = params.get('merchant_trans_id')
|
|
624
|
-
amount = float(params.get('amount', 0))
|
|
625
|
-
action = int(params.get('action', -1))
|
|
626
|
-
error = int(params.get('error', 0))
|
|
627
|
-
|
|
628
|
-
# Find account
|
|
629
|
-
try:
|
|
630
|
-
account = self._find_account(merchant_trans_id)
|
|
631
|
-
except AccountNotFound:
|
|
632
|
-
logger.error("Account not found: %s", merchant_trans_id)
|
|
633
|
-
return JsonResponse({
|
|
634
|
-
'click_trans_id': click_trans_id,
|
|
635
|
-
'merchant_trans_id': merchant_trans_id,
|
|
636
|
-
'error': -5,
|
|
637
|
-
'error_note': "User not found"
|
|
638
|
-
}, status=200) # Return 200 status code for all errors
|
|
639
|
-
|
|
640
|
-
# Validate amount
|
|
641
|
-
try:
|
|
642
|
-
# Get amount from account and validate
|
|
643
|
-
account_amount = float(getattr(account, 'amount', 0))
|
|
644
|
-
self._validate_amount(amount, account_amount)
|
|
645
|
-
except InvalidAmount as e:
|
|
646
|
-
logger.error("Invalid amount: %s", e)
|
|
647
|
-
return JsonResponse({
|
|
648
|
-
'click_trans_id': click_trans_id,
|
|
649
|
-
'merchant_trans_id': merchant_trans_id,
|
|
650
|
-
'error': -2,
|
|
651
|
-
'error_note': str(e)
|
|
652
|
-
}, status=200) # Return 200 status code for all errors
|
|
653
|
-
|
|
654
|
-
# Check if transaction already exists
|
|
655
|
-
try:
|
|
656
|
-
transaction = PaymentTransaction._default_manager.get(
|
|
657
|
-
gateway=PaymentTransaction.CLICK,
|
|
658
|
-
transaction_id=click_trans_id
|
|
659
|
-
)
|
|
660
|
-
|
|
661
|
-
# If transaction is already completed, return success
|
|
662
|
-
if transaction.state == PaymentTransaction.SUCCESSFULLY:
|
|
663
|
-
# Call the event method
|
|
664
|
-
self.transaction_already_exists(params, transaction)
|
|
665
|
-
|
|
666
|
-
return JsonResponse({
|
|
667
|
-
'click_trans_id': click_trans_id,
|
|
668
|
-
'merchant_trans_id': merchant_trans_id,
|
|
669
|
-
'merchant_prepare_id': transaction.id,
|
|
670
|
-
'error': 0,
|
|
671
|
-
'error_note': "Success"
|
|
672
|
-
})
|
|
673
|
-
|
|
674
|
-
# If transaction is cancelled, return error
|
|
675
|
-
if transaction.state == PaymentTransaction.CANCELLED:
|
|
676
|
-
return JsonResponse({
|
|
677
|
-
'click_trans_id': click_trans_id,
|
|
678
|
-
'merchant_trans_id': merchant_trans_id,
|
|
679
|
-
'merchant_prepare_id': transaction.id,
|
|
680
|
-
'error': -9,
|
|
681
|
-
'error_note': "Transaction cancelled"
|
|
682
|
-
})
|
|
683
|
-
except PaymentTransaction.DoesNotExist:
|
|
684
|
-
# Transaction doesn't exist, continue with the flow
|
|
685
|
-
pass
|
|
686
|
-
|
|
687
|
-
# Handle different actions
|
|
688
|
-
if action == 0: # Prepare
|
|
689
|
-
# Create transaction
|
|
690
|
-
transaction = PaymentTransaction.create_transaction(
|
|
691
|
-
gateway=PaymentTransaction.CLICK,
|
|
692
|
-
transaction_id=click_trans_id,
|
|
693
|
-
account_id=merchant_trans_id,
|
|
694
|
-
amount=amount,
|
|
695
|
-
extra_data={
|
|
696
|
-
'raw_params': params
|
|
697
|
-
}
|
|
698
|
-
)
|
|
699
|
-
|
|
700
|
-
# Update state to INITIATING
|
|
701
|
-
transaction.state = PaymentTransaction.INITIATING
|
|
702
|
-
transaction.save()
|
|
703
|
-
|
|
704
|
-
# Call the event method
|
|
705
|
-
self.transaction_created(params, transaction, account)
|
|
706
|
-
|
|
707
|
-
return JsonResponse({
|
|
708
|
-
'click_trans_id': click_trans_id,
|
|
709
|
-
'merchant_trans_id': merchant_trans_id,
|
|
710
|
-
'merchant_prepare_id': transaction.id,
|
|
711
|
-
'error': 0,
|
|
712
|
-
'error_note': "Success"
|
|
713
|
-
})
|
|
714
|
-
|
|
715
|
-
# Complete action
|
|
716
|
-
if action == 1:
|
|
717
|
-
# Check if error is negative (payment failed)
|
|
718
|
-
is_successful = error >= 0
|
|
719
|
-
|
|
720
|
-
try:
|
|
721
|
-
transaction = PaymentTransaction._default_manager.get(
|
|
722
|
-
gateway=PaymentTransaction.CLICK,
|
|
723
|
-
transaction_id=click_trans_id
|
|
724
|
-
)
|
|
725
|
-
except PaymentTransaction.DoesNotExist:
|
|
726
|
-
# Create transaction if it doesn't exist
|
|
727
|
-
transaction = PaymentTransaction.create_transaction(
|
|
728
|
-
gateway=PaymentTransaction.CLICK,
|
|
729
|
-
transaction_id=click_trans_id,
|
|
730
|
-
account_id=merchant_trans_id,
|
|
731
|
-
amount=amount,
|
|
732
|
-
extra_data={
|
|
733
|
-
'raw_params': params
|
|
734
|
-
}
|
|
735
|
-
)
|
|
736
|
-
|
|
737
|
-
if is_successful:
|
|
738
|
-
# Mark transaction as paid
|
|
739
|
-
transaction.mark_as_paid()
|
|
740
|
-
|
|
741
|
-
# Call the event method
|
|
742
|
-
self.successfully_payment(params, transaction)
|
|
743
|
-
else:
|
|
744
|
-
# Mark transaction as cancelled
|
|
745
|
-
transaction.mark_as_cancelled(
|
|
746
|
-
reason=f"Error code: {error}"
|
|
747
|
-
)
|
|
748
|
-
|
|
749
|
-
# Call the event method
|
|
750
|
-
self.cancelled_payment(params, transaction)
|
|
751
|
-
|
|
752
|
-
return JsonResponse({
|
|
753
|
-
'click_trans_id': click_trans_id,
|
|
754
|
-
'merchant_trans_id': merchant_trans_id,
|
|
755
|
-
'merchant_prepare_id': transaction.id,
|
|
756
|
-
'error': 0,
|
|
757
|
-
'error_note': "Success"
|
|
758
|
-
})
|
|
759
|
-
|
|
760
|
-
# Handle unsupported action
|
|
761
|
-
logger.error("Unsupported action: %s", action)
|
|
762
|
-
return JsonResponse({
|
|
763
|
-
'click_trans_id': click_trans_id,
|
|
764
|
-
'merchant_trans_id': merchant_trans_id,
|
|
765
|
-
'error': -3,
|
|
766
|
-
'error_note': "Action not found"
|
|
767
|
-
}, status=200) # Return 200 status code for all errors
|
|
768
|
-
|
|
769
|
-
except Exception as e: # pylint: disable=broad-except
|
|
770
|
-
logger.exception("Unexpected error in Click webhook: %s", e)
|
|
771
|
-
return JsonResponse({
|
|
772
|
-
'error': -7,
|
|
773
|
-
'error_note': "Internal error"
|
|
774
|
-
}, status=200) # Return 200 status code for all errors
|
|
775
|
-
|
|
776
|
-
def _check_auth(self, params):
|
|
777
|
-
"""
|
|
778
|
-
Check authentication using signature.
|
|
779
|
-
"""
|
|
780
|
-
if str(params.get('service_id')) != self.service_id:
|
|
781
|
-
raise PermissionDenied("Invalid service ID")
|
|
782
|
-
|
|
783
|
-
# Check signature if secret key is provided
|
|
784
|
-
if self.secret_key:
|
|
785
|
-
sign_string = params.get('sign_string')
|
|
786
|
-
sign_time = params.get('sign_time')
|
|
787
|
-
|
|
788
|
-
if not sign_string or not sign_time:
|
|
789
|
-
raise PermissionDenied("Missing signature parameters")
|
|
790
|
-
|
|
791
|
-
# Create string to sign
|
|
792
|
-
to_sign = (
|
|
793
|
-
f"{params.get('click_trans_id')}{params.get('service_id')}"
|
|
794
|
-
)
|
|
795
|
-
to_sign += f"{self.secret_key}{params.get('merchant_trans_id')}"
|
|
796
|
-
to_sign += f"{params.get('amount')}{params.get('action')}"
|
|
797
|
-
to_sign += f"{sign_time}"
|
|
798
|
-
|
|
799
|
-
# Generate signature
|
|
800
|
-
signature = hashlib.md5(to_sign.encode('utf-8')).hexdigest()
|
|
801
|
-
|
|
802
|
-
if signature != sign_string:
|
|
803
|
-
raise PermissionDenied("Invalid signature")
|
|
804
|
-
|
|
805
|
-
def _find_account(self, merchant_trans_id):
|
|
806
|
-
"""
|
|
807
|
-
Find account by merchant_trans_id.
|
|
808
|
-
"""
|
|
809
|
-
try:
|
|
810
|
-
# Convert merchant_trans_id to int if needed
|
|
811
|
-
if (isinstance(merchant_trans_id, str)
|
|
812
|
-
and merchant_trans_id.isdigit()):
|
|
813
|
-
merchant_trans_id = int(merchant_trans_id)
|
|
814
|
-
|
|
815
|
-
# Use model manager to find account
|
|
816
|
-
account = self.account_model._default_manager.get(
|
|
817
|
-
id=merchant_trans_id
|
|
818
|
-
)
|
|
819
|
-
return account
|
|
820
|
-
except self.account_model.DoesNotExist:
|
|
821
|
-
raise AccountNotFound(
|
|
822
|
-
f"Account with id={merchant_trans_id} not found"
|
|
823
|
-
) from None
|
|
824
|
-
|
|
825
|
-
def _validate_amount(self, received_amount, expected_amount):
|
|
826
|
-
"""
|
|
827
|
-
Validate payment amount.
|
|
828
|
-
"""
|
|
829
|
-
# Add commission if needed
|
|
830
|
-
if self.commission_percent > 0:
|
|
831
|
-
expected_amount = expected_amount * (
|
|
832
|
-
1 + self.commission_percent / 100
|
|
833
|
-
)
|
|
834
|
-
expected_amount = round(expected_amount, 2)
|
|
835
|
-
|
|
836
|
-
# Allow small difference due to floating point precision
|
|
837
|
-
if abs(received_amount - expected_amount) > 0.01:
|
|
838
|
-
raise InvalidAmount(
|
|
839
|
-
f"Incorrect amount. Expected: {expected_amount}, "
|
|
840
|
-
f"received: {received_amount}"
|
|
841
|
-
)
|
|
842
|
-
|
|
843
|
-
# Event methods that can be overridden by subclasses
|
|
844
|
-
|
|
845
|
-
def transaction_already_exists(self, params, transaction):
|
|
846
|
-
"""
|
|
847
|
-
Called when a transaction already exists.
|
|
848
|
-
|
|
849
|
-
Args:
|
|
850
|
-
params: Request parameters
|
|
851
|
-
transaction: Transaction object
|
|
852
|
-
"""
|
|
853
|
-
# This method is meant to be overridden by subclasses
|
|
854
|
-
|
|
855
|
-
def transaction_created(self, params, transaction, account):
|
|
856
|
-
"""
|
|
857
|
-
Called when a transaction is created.
|
|
858
|
-
|
|
859
|
-
Args:
|
|
860
|
-
params: Request parameters
|
|
861
|
-
transaction: Transaction object
|
|
862
|
-
account: Account object
|
|
863
|
-
"""
|
|
864
|
-
# This method is meant to be overridden by subclasses
|
|
865
|
-
|
|
866
|
-
def successfully_payment(self, params, transaction):
|
|
867
|
-
"""
|
|
868
|
-
Called when a payment is successful.
|
|
869
|
-
|
|
870
|
-
Args:
|
|
871
|
-
params: Request parameters
|
|
872
|
-
transaction: Transaction object
|
|
873
|
-
"""
|
|
874
|
-
# This method is meant to be overridden by subclasses
|
|
875
|
-
|
|
876
|
-
def cancelled_payment(self, params, transaction):
|
|
877
|
-
"""
|
|
878
|
-
Called when a payment is cancelled.
|
|
879
|
-
|
|
880
|
-
Args:
|
|
881
|
-
params: Request parameters
|
|
882
|
-
transaction: Transaction object
|
|
883
|
-
"""
|
|
884
|
-
# This method is meant to be overridden by subclasses
|