paytechuz 0.2.21__py3-none-any.whl → 0.2.23__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.

@@ -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