paytechuz 0.2.6__py3-none-any.whl → 0.2.8__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/models.py +16 -11
- paytechuz/integrations/fastapi/routes.py +163 -61
- paytechuz/integrations/fastapi/schemas.py +26 -7
- paytechuz-0.2.8.dist-info/METADATA +312 -0
- {paytechuz-0.2.6.dist-info → paytechuz-0.2.8.dist-info}/RECORD +7 -7
- paytechuz-0.2.6.dist-info/METADATA +0 -170
- {paytechuz-0.2.6.dist-info → paytechuz-0.2.8.dist-info}/WHEEL +0 -0
- {paytechuz-0.2.6.dist-info → paytechuz-0.2.8.dist-info}/top_level.txt +0 -0
|
@@ -9,6 +9,7 @@ from sqlalchemy.ext.declarative import declarative_base
|
|
|
9
9
|
|
|
10
10
|
Base = declarative_base()
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
class PaymentTransaction(Base):
|
|
13
14
|
"""
|
|
14
15
|
Payment transaction model for storing payment information.
|
|
@@ -35,7 +36,11 @@ class PaymentTransaction(Base):
|
|
|
35
36
|
reason = Column(Integer, nullable=True) # Reason for cancellation
|
|
36
37
|
extra_data = Column(JSON, default={})
|
|
37
38
|
created_at = Column(DateTime, default=datetime.utcnow, index=True)
|
|
38
|
-
updated_at = Column(
|
|
39
|
+
updated_at = Column(
|
|
40
|
+
DateTime,
|
|
41
|
+
default=datetime.utcnow,
|
|
42
|
+
onupdate=datetime.utcnow, index=True
|
|
43
|
+
)
|
|
39
44
|
performed_at = Column(DateTime, nullable=True, index=True)
|
|
40
45
|
cancelled_at = Column(DateTime, nullable=True, index=True)
|
|
41
46
|
|
|
@@ -107,7 +112,9 @@ class PaymentTransaction(Base):
|
|
|
107
112
|
|
|
108
113
|
return self
|
|
109
114
|
|
|
110
|
-
def mark_as_cancelled(
|
|
115
|
+
def mark_as_cancelled(
|
|
116
|
+
self, db, reason: Optional[str] = None
|
|
117
|
+
) -> "PaymentTransaction":
|
|
111
118
|
"""
|
|
112
119
|
Mark the transaction as cancelled.
|
|
113
120
|
|
|
@@ -118,23 +125,21 @@ class PaymentTransaction(Base):
|
|
|
118
125
|
Returns:
|
|
119
126
|
PaymentTransaction instance
|
|
120
127
|
"""
|
|
121
|
-
# If reason is not provided, use default reason from PaymeCancelReason
|
|
122
128
|
if reason is None:
|
|
123
|
-
|
|
124
|
-
from paytechuz.core.constants import PaymeCancelReason
|
|
125
|
-
reason_code = PaymeCancelReason.REASON_FUND_RETURNED # Default reason 5
|
|
129
|
+
reason_code = 5 # REASON_FUND_RETURNED
|
|
126
130
|
else:
|
|
127
|
-
# Convert reason to int if it's a string
|
|
128
131
|
if isinstance(reason, str) and reason.isdigit():
|
|
129
132
|
reason_code = int(reason)
|
|
130
133
|
else:
|
|
131
134
|
reason_code = reason
|
|
132
135
|
|
|
133
|
-
# Only update state if not already cancelled
|
|
134
136
|
if self.state not in [self.CANCELLED, self.CANCELLED_DURING_INIT]:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
if self.state == self.INITIATING or reason_code == 3:
|
|
138
|
+
self.state = self.CANCELLED_DURING_INIT
|
|
139
|
+
else:
|
|
140
|
+
# Otherwise, set state to CANCELLED (-2)
|
|
141
|
+
self.state = self.CANCELLED
|
|
142
|
+
|
|
138
143
|
self.cancelled_at = datetime.utcnow()
|
|
139
144
|
|
|
140
145
|
# Store the reason directly in the reason column
|
|
@@ -7,11 +7,10 @@ import json
|
|
|
7
7
|
import logging
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
from decimal import Decimal
|
|
10
|
-
from typing import Dict, Any, Optional
|
|
10
|
+
from typing import Dict, Any, Optional
|
|
11
11
|
|
|
12
12
|
from fastapi import (
|
|
13
13
|
APIRouter,
|
|
14
|
-
Depends,
|
|
15
14
|
HTTPException,
|
|
16
15
|
Request,
|
|
17
16
|
Response,
|
|
@@ -19,6 +18,7 @@ from fastapi import (
|
|
|
19
18
|
)
|
|
20
19
|
from sqlalchemy.orm import Session
|
|
21
20
|
|
|
21
|
+
# pylint: disable=E0401,E0611
|
|
22
22
|
from paytechuz.core.exceptions import (
|
|
23
23
|
PermissionDenied,
|
|
24
24
|
InvalidAmount,
|
|
@@ -185,13 +185,12 @@ class PaymeWebhookHandler:
|
|
|
185
185
|
'jsonrpc': '2.0',
|
|
186
186
|
'id': request_id if 'request_id' in locals() else 0,
|
|
187
187
|
'error': {
|
|
188
|
-
# Code for account not found, in the range -31099 to -31050
|
|
189
188
|
'code': -31050,
|
|
190
189
|
'message': str(e)
|
|
191
190
|
}
|
|
192
191
|
}),
|
|
193
192
|
media_type="application/json",
|
|
194
|
-
status_code=200
|
|
193
|
+
status_code=200
|
|
195
194
|
)
|
|
196
195
|
|
|
197
196
|
except InvalidAmount as e:
|
|
@@ -200,12 +199,12 @@ class PaymeWebhookHandler:
|
|
|
200
199
|
'jsonrpc': '2.0',
|
|
201
200
|
'id': request_id if 'request_id' in locals() else 0,
|
|
202
201
|
'error': {
|
|
203
|
-
'code': -31001,
|
|
202
|
+
'code': -31001,
|
|
204
203
|
'message': str(e)
|
|
205
204
|
}
|
|
206
205
|
}),
|
|
207
206
|
media_type="application/json",
|
|
208
|
-
status_code=200
|
|
207
|
+
status_code=200
|
|
209
208
|
)
|
|
210
209
|
|
|
211
210
|
except InvalidAccount as e:
|
|
@@ -214,13 +213,12 @@ class PaymeWebhookHandler:
|
|
|
214
213
|
'jsonrpc': '2.0',
|
|
215
214
|
'id': request_id if 'request_id' in locals() else 0,
|
|
216
215
|
'error': {
|
|
217
|
-
# Code for invalid account, in the range -31099 to -31050
|
|
218
216
|
'code': -31050,
|
|
219
217
|
'message': str(e)
|
|
220
218
|
}
|
|
221
219
|
}),
|
|
222
220
|
media_type="application/json",
|
|
223
|
-
status_code=200
|
|
221
|
+
status_code=200
|
|
224
222
|
)
|
|
225
223
|
|
|
226
224
|
except TransactionNotFound as e:
|
|
@@ -234,7 +232,7 @@ class PaymeWebhookHandler:
|
|
|
234
232
|
}
|
|
235
233
|
}),
|
|
236
234
|
media_type="application/json",
|
|
237
|
-
status_code=200
|
|
235
|
+
status_code=200
|
|
238
236
|
)
|
|
239
237
|
|
|
240
238
|
except Exception as e:
|
|
@@ -249,7 +247,7 @@ class PaymeWebhookHandler:
|
|
|
249
247
|
}
|
|
250
248
|
}),
|
|
251
249
|
media_type="application/json",
|
|
252
|
-
status_code=200
|
|
250
|
+
status_code=200
|
|
253
251
|
)
|
|
254
252
|
|
|
255
253
|
def _check_auth(self, auth_header: Optional[str]) -> None:
|
|
@@ -270,7 +268,6 @@ class PaymeWebhookHandler:
|
|
|
270
268
|
if password != self.payme_key:
|
|
271
269
|
raise PermissionDenied("Invalid merchant key")
|
|
272
270
|
except PermissionDenied:
|
|
273
|
-
# Re-raise permission denied exceptions
|
|
274
271
|
raise
|
|
275
272
|
except Exception as e:
|
|
276
273
|
logger.error(f"Authentication error: {e}")
|
|
@@ -284,13 +281,12 @@ class PaymeWebhookHandler:
|
|
|
284
281
|
if not account_value:
|
|
285
282
|
raise AccountNotFound("Account not found in parameters")
|
|
286
283
|
|
|
287
|
-
# Handle special case for 'order_id' field
|
|
288
284
|
lookup_field = 'id' if self.account_field == 'order_id' else (
|
|
289
285
|
self.account_field
|
|
290
286
|
)
|
|
291
287
|
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
if (lookup_field == 'id' and
|
|
289
|
+
isinstance(account_value, str) and
|
|
294
290
|
account_value.isdigit()):
|
|
295
291
|
account_value = int(account_value)
|
|
296
292
|
|
|
@@ -324,12 +320,15 @@ class PaymeWebhookHandler:
|
|
|
324
320
|
# If one_time_payment is disabled, amount must be positive
|
|
325
321
|
if not self.one_time_payment and received_amount <= 0:
|
|
326
322
|
raise InvalidAmount(
|
|
327
|
-
f"Invalid amount. Amount must be positive,
|
|
323
|
+
(f"Invalid amount. Amount must be positive, "
|
|
324
|
+
f"received: {received_amount}")
|
|
328
325
|
)
|
|
329
326
|
|
|
330
327
|
return True
|
|
331
328
|
|
|
332
|
-
def _check_perform_transaction(
|
|
329
|
+
def _check_perform_transaction(
|
|
330
|
+
self, params: Dict[str, Any]
|
|
331
|
+
) -> Dict[str, Any]:
|
|
333
332
|
"""
|
|
334
333
|
Handle CheckPerformTransaction method.
|
|
335
334
|
"""
|
|
@@ -351,8 +350,8 @@ class PaymeWebhookHandler:
|
|
|
351
350
|
|
|
352
351
|
self._validate_amount(account, amount)
|
|
353
352
|
|
|
354
|
-
# Check if there's already a transaction for this account with a
|
|
355
|
-
# Only check if one_time_payment is enabled
|
|
353
|
+
# Check if there's already a transaction for this account with a
|
|
354
|
+
# different transaction_id. Only check if one_time_payment is enabled
|
|
356
355
|
if self.one_time_payment:
|
|
357
356
|
# Check for existing transactions in non-final states
|
|
358
357
|
existing_transactions = self.db.query(PaymentTransaction).filter(
|
|
@@ -362,16 +361,23 @@ class PaymeWebhookHandler:
|
|
|
362
361
|
PaymentTransaction.transaction_id != transaction_id
|
|
363
362
|
).all()
|
|
364
363
|
|
|
365
|
-
# Filter out transactions in final states
|
|
364
|
+
# Filter out transactions in final states
|
|
365
|
+
# (SUCCESSFULLY, CANCELLED, CANCELLED_DURING_INIT)
|
|
366
366
|
non_final_transactions = [
|
|
367
367
|
t for t in existing_transactions
|
|
368
|
-
if t.state not in [
|
|
368
|
+
if t.state not in [
|
|
369
|
+
PaymentTransaction.SUCCESSFULLY,
|
|
370
|
+
PaymentTransaction.CANCELLED,
|
|
371
|
+
PaymentTransaction.CANCELLED_DURING_INIT
|
|
372
|
+
]
|
|
369
373
|
]
|
|
370
374
|
|
|
371
375
|
if non_final_transactions:
|
|
372
|
-
# If there's already a transaction for this account with a
|
|
376
|
+
# If there's already a transaction for this account with a
|
|
377
|
+
# different transaction_id in a non-final state, raise an error
|
|
373
378
|
raise InvalidAccount(
|
|
374
|
-
f"Account with {self.account_field}={account.id}
|
|
379
|
+
(f"Account with {self.account_field}={account.id} "
|
|
380
|
+
f"already has a pending transaction")
|
|
375
381
|
)
|
|
376
382
|
|
|
377
383
|
# Check for existing transaction with the same transaction_id
|
|
@@ -386,7 +392,9 @@ class PaymeWebhookHandler:
|
|
|
386
392
|
|
|
387
393
|
# For existing transactions, use the original time from extra_data
|
|
388
394
|
# This ensures the same response is returned for repeated calls
|
|
389
|
-
create_time = transaction.extra_data.get(
|
|
395
|
+
create_time = transaction.extra_data.get(
|
|
396
|
+
'create_time', params.get('time')
|
|
397
|
+
)
|
|
390
398
|
|
|
391
399
|
return {
|
|
392
400
|
'transaction': transaction.transaction_id,
|
|
@@ -403,7 +411,9 @@ class PaymeWebhookHandler:
|
|
|
403
411
|
state=PaymentTransaction.INITIATING,
|
|
404
412
|
extra_data={
|
|
405
413
|
'account_field': self.account_field,
|
|
406
|
-
'account_value':
|
|
414
|
+
'account_value': (
|
|
415
|
+
params.get('account', {}).get(self.account_field)
|
|
416
|
+
),
|
|
407
417
|
'create_time': params.get('time'),
|
|
408
418
|
'raw_params': params
|
|
409
419
|
}
|
|
@@ -416,7 +426,8 @@ class PaymeWebhookHandler:
|
|
|
416
426
|
# Call the event method
|
|
417
427
|
self.transaction_created(params, transaction, account)
|
|
418
428
|
|
|
419
|
-
# Use the time from the request params
|
|
429
|
+
# Use the time from the request params
|
|
430
|
+
# instead of transaction.created_at
|
|
420
431
|
create_time = params.get('time')
|
|
421
432
|
|
|
422
433
|
return {
|
|
@@ -451,7 +462,9 @@ class PaymeWebhookHandler:
|
|
|
451
462
|
return {
|
|
452
463
|
'transaction': transaction.transaction_id,
|
|
453
464
|
'state': transaction.state,
|
|
454
|
-
'perform_time': int(
|
|
465
|
+
'perform_time': int(
|
|
466
|
+
transaction.performed_at.timestamp() * 1000
|
|
467
|
+
) if transaction.performed_at else 0,
|
|
455
468
|
}
|
|
456
469
|
|
|
457
470
|
def _check_transaction(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -475,18 +488,28 @@ class PaymeWebhookHandler:
|
|
|
475
488
|
self.check_transaction(params, transaction)
|
|
476
489
|
|
|
477
490
|
# Use the original time from extra_data for consistency
|
|
478
|
-
create_time = transaction.extra_data.get(
|
|
491
|
+
create_time = transaction.extra_data.get(
|
|
492
|
+
'create_time', int(transaction.created_at.timestamp() * 1000)
|
|
493
|
+
)
|
|
479
494
|
|
|
480
495
|
return {
|
|
481
496
|
'transaction': transaction.transaction_id,
|
|
482
497
|
'state': transaction.state,
|
|
483
498
|
'create_time': create_time,
|
|
484
|
-
'perform_time':
|
|
485
|
-
|
|
499
|
+
'perform_time': (
|
|
500
|
+
int(transaction.performed_at.timestamp() * 1000)
|
|
501
|
+
if transaction.performed_at else 0
|
|
502
|
+
),
|
|
503
|
+
'cancel_time': (
|
|
504
|
+
int(transaction.cancelled_at.timestamp() * 1000)
|
|
505
|
+
if transaction.cancelled_at else 0
|
|
506
|
+
),
|
|
486
507
|
'reason': transaction.reason,
|
|
487
508
|
}
|
|
488
509
|
|
|
489
|
-
def _cancel_response(
|
|
510
|
+
def _cancel_response(
|
|
511
|
+
self, transaction: PaymentTransaction
|
|
512
|
+
) -> Dict[str, Any]:
|
|
490
513
|
"""
|
|
491
514
|
Helper method to generate cancel transaction response.
|
|
492
515
|
|
|
@@ -511,7 +534,8 @@ class PaymeWebhookHandler:
|
|
|
511
534
|
return {
|
|
512
535
|
'transaction': transaction.transaction_id,
|
|
513
536
|
'state': transaction.state,
|
|
514
|
-
'cancel_time': int(transaction.cancelled_at.timestamp() * 1000)
|
|
537
|
+
'cancel_time': (int(transaction.cancelled_at.timestamp() * 1000)
|
|
538
|
+
if transaction.cancelled_at else 0),
|
|
515
539
|
'reason': reason,
|
|
516
540
|
}
|
|
517
541
|
|
|
@@ -534,15 +558,22 @@ class PaymeWebhookHandler:
|
|
|
534
558
|
)
|
|
535
559
|
|
|
536
560
|
# Check if transaction is already cancelled
|
|
537
|
-
|
|
538
|
-
|
|
561
|
+
cancelled_states = [
|
|
562
|
+
PaymentTransaction.CANCELLED,
|
|
563
|
+
PaymentTransaction.CANCELLED_DURING_INIT
|
|
564
|
+
]
|
|
565
|
+
if transaction.state in cancelled_states:
|
|
566
|
+
# If transaction is already cancelled, update the reason
|
|
567
|
+
# if provided
|
|
539
568
|
if 'reason' in params:
|
|
540
569
|
reason = params.get('reason')
|
|
541
570
|
|
|
542
|
-
# If reason is not provided, use default reason
|
|
571
|
+
# If reason is not provided, use default reason
|
|
572
|
+
# from PaymeCancelReason
|
|
543
573
|
if reason is None:
|
|
544
574
|
from paytechuz.core.constants import PaymeCancelReason
|
|
545
|
-
|
|
575
|
+
# Default reason 5
|
|
576
|
+
reason = PaymeCancelReason.REASON_FUND_RETURNED
|
|
546
577
|
|
|
547
578
|
# Convert reason to int if it's a string
|
|
548
579
|
if isinstance(reason, str) and reason.isdigit():
|
|
@@ -569,7 +600,8 @@ class PaymeWebhookHandler:
|
|
|
569
600
|
# Ensure the reason is stored in extra_data
|
|
570
601
|
extra_data = transaction.extra_data or {}
|
|
571
602
|
if 'cancel_reason' not in extra_data:
|
|
572
|
-
|
|
603
|
+
# Default reason 5 if none provided
|
|
604
|
+
extra_data['cancel_reason'] = reason if reason is not None else 5
|
|
573
605
|
transaction.extra_data = extra_data
|
|
574
606
|
self.db.commit()
|
|
575
607
|
self.db.refresh(transaction)
|
|
@@ -616,9 +648,18 @@ class PaymeWebhookHandler:
|
|
|
616
648
|
self.account_field: transaction.account_id
|
|
617
649
|
},
|
|
618
650
|
'state': transaction.state,
|
|
619
|
-
'create_time': transaction.extra_data.get(
|
|
620
|
-
|
|
621
|
-
|
|
651
|
+
'create_time': transaction.extra_data.get(
|
|
652
|
+
'create_time',
|
|
653
|
+
int(transaction.created_at.timestamp() * 1000)
|
|
654
|
+
),
|
|
655
|
+
'perform_time': (
|
|
656
|
+
int(transaction.performed_at.timestamp() * 1000)
|
|
657
|
+
if transaction.performed_at else 0
|
|
658
|
+
),
|
|
659
|
+
'cancel_time': (
|
|
660
|
+
int(transaction.cancelled_at.timestamp() * 1000)
|
|
661
|
+
if transaction.cancelled_at else 0
|
|
662
|
+
),
|
|
622
663
|
'reason': transaction.reason,
|
|
623
664
|
})
|
|
624
665
|
|
|
@@ -629,7 +670,9 @@ class PaymeWebhookHandler:
|
|
|
629
670
|
|
|
630
671
|
# Event methods that can be overridden by subclasses
|
|
631
672
|
|
|
632
|
-
def before_check_perform_transaction(
|
|
673
|
+
def before_check_perform_transaction(
|
|
674
|
+
self, params: Dict[str, Any], account: Any
|
|
675
|
+
) -> None:
|
|
633
676
|
"""
|
|
634
677
|
Called before checking if a transaction can be performed.
|
|
635
678
|
|
|
@@ -639,7 +682,9 @@ class PaymeWebhookHandler:
|
|
|
639
682
|
"""
|
|
640
683
|
pass
|
|
641
684
|
|
|
642
|
-
def transaction_already_exists(
|
|
685
|
+
def transaction_already_exists(
|
|
686
|
+
self, params: Dict[str, Any], transaction: PaymentTransaction
|
|
687
|
+
) -> None:
|
|
643
688
|
"""
|
|
644
689
|
Called when a transaction already exists.
|
|
645
690
|
|
|
@@ -649,7 +694,12 @@ class PaymeWebhookHandler:
|
|
|
649
694
|
"""
|
|
650
695
|
pass
|
|
651
696
|
|
|
652
|
-
def transaction_created(
|
|
697
|
+
def transaction_created(
|
|
698
|
+
self,
|
|
699
|
+
params: Dict[str, Any],
|
|
700
|
+
transaction: PaymentTransaction,
|
|
701
|
+
account: Any
|
|
702
|
+
) -> None:
|
|
653
703
|
"""
|
|
654
704
|
Called when a transaction is created.
|
|
655
705
|
|
|
@@ -660,7 +710,11 @@ class PaymeWebhookHandler:
|
|
|
660
710
|
"""
|
|
661
711
|
pass
|
|
662
712
|
|
|
663
|
-
def successfully_payment(
|
|
713
|
+
def successfully_payment(
|
|
714
|
+
self,
|
|
715
|
+
params: Dict[str, Any],
|
|
716
|
+
transaction: PaymentTransaction
|
|
717
|
+
) -> None:
|
|
664
718
|
"""
|
|
665
719
|
Called when a payment is successful.
|
|
666
720
|
|
|
@@ -670,7 +724,11 @@ class PaymeWebhookHandler:
|
|
|
670
724
|
"""
|
|
671
725
|
pass
|
|
672
726
|
|
|
673
|
-
def check_transaction(
|
|
727
|
+
def check_transaction(
|
|
728
|
+
self,
|
|
729
|
+
params: Dict[str, Any],
|
|
730
|
+
transaction: PaymentTransaction
|
|
731
|
+
) -> None:
|
|
674
732
|
"""
|
|
675
733
|
Called when checking a transaction.
|
|
676
734
|
|
|
@@ -680,7 +738,11 @@ class PaymeWebhookHandler:
|
|
|
680
738
|
"""
|
|
681
739
|
pass
|
|
682
740
|
|
|
683
|
-
def cancelled_payment(
|
|
741
|
+
def cancelled_payment(
|
|
742
|
+
self,
|
|
743
|
+
params: Dict[str, Any],
|
|
744
|
+
transaction: PaymentTransaction
|
|
745
|
+
) -> None:
|
|
684
746
|
"""
|
|
685
747
|
Called when a payment is cancelled.
|
|
686
748
|
|
|
@@ -690,7 +752,11 @@ class PaymeWebhookHandler:
|
|
|
690
752
|
"""
|
|
691
753
|
pass
|
|
692
754
|
|
|
693
|
-
def get_statement(
|
|
755
|
+
def get_statement(
|
|
756
|
+
self,
|
|
757
|
+
params: Dict[str, Any],
|
|
758
|
+
transactions: list
|
|
759
|
+
) -> None:
|
|
694
760
|
"""
|
|
695
761
|
Called when getting a statement.
|
|
696
762
|
|
|
@@ -719,7 +785,9 @@ class ClickWebhookHandler:
|
|
|
719
785
|
print(f"Payment successful: {transaction.transaction_id}")
|
|
720
786
|
|
|
721
787
|
# Update your order status
|
|
722
|
-
order = db.query(Order)
|
|
788
|
+
order = (db.query(Order)
|
|
789
|
+
.filter(Order.id == transaction.account_id)
|
|
790
|
+
.first())
|
|
723
791
|
order.status = 'paid'
|
|
724
792
|
db.commit()
|
|
725
793
|
```
|
|
@@ -788,7 +856,8 @@ class ClickWebhookHandler:
|
|
|
788
856
|
|
|
789
857
|
# Validate amount
|
|
790
858
|
try:
|
|
791
|
-
|
|
859
|
+
expected = float(getattr(account, 'amount', 0))
|
|
860
|
+
self._validate_amount(amount, expected)
|
|
792
861
|
except Exception as e:
|
|
793
862
|
logger.error(f"Invalid amount: {e}")
|
|
794
863
|
return {
|
|
@@ -886,7 +955,8 @@ class ClickWebhookHandler:
|
|
|
886
955
|
self.successfully_payment(params, transaction)
|
|
887
956
|
else:
|
|
888
957
|
# Mark transaction as cancelled
|
|
889
|
-
|
|
958
|
+
error_reason = f"Error code: {error}"
|
|
959
|
+
transaction.mark_as_cancelled(self.db, reason=error_reason)
|
|
890
960
|
|
|
891
961
|
# Call the event method
|
|
892
962
|
self.cancelled_payment(params, transaction)
|
|
@@ -937,10 +1007,15 @@ class ClickWebhookHandler:
|
|
|
937
1007
|
)
|
|
938
1008
|
|
|
939
1009
|
# Create string to sign
|
|
940
|
-
to_sign =
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1010
|
+
to_sign = (
|
|
1011
|
+
f"{params.get('click_trans_id')}"
|
|
1012
|
+
f"{params.get('service_id')}"
|
|
1013
|
+
f"{self.secret_key}"
|
|
1014
|
+
f"{params.get('merchant_trans_id')}"
|
|
1015
|
+
f"{params.get('amount')}"
|
|
1016
|
+
f"{params.get('action')}"
|
|
1017
|
+
f"{sign_time}"
|
|
1018
|
+
)
|
|
944
1019
|
|
|
945
1020
|
# Generate signature
|
|
946
1021
|
signature = hashlib.md5(to_sign.encode('utf-8')).hexdigest()
|
|
@@ -959,7 +1034,11 @@ class ClickWebhookHandler:
|
|
|
959
1034
|
if isinstance(merchant_trans_id, str) and merchant_trans_id.isdigit():
|
|
960
1035
|
merchant_trans_id = int(merchant_trans_id)
|
|
961
1036
|
|
|
962
|
-
account =
|
|
1037
|
+
account = (
|
|
1038
|
+
self.db.query(self.account_model)
|
|
1039
|
+
.filter_by(id=merchant_trans_id)
|
|
1040
|
+
.first()
|
|
1041
|
+
)
|
|
963
1042
|
if not account:
|
|
964
1043
|
raise HTTPException(
|
|
965
1044
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
@@ -968,25 +1047,35 @@ class ClickWebhookHandler:
|
|
|
968
1047
|
|
|
969
1048
|
return account
|
|
970
1049
|
|
|
971
|
-
def _validate_amount(
|
|
1050
|
+
def _validate_amount(
|
|
1051
|
+
self, received_amount: float, expected_amount: float
|
|
1052
|
+
) -> None:
|
|
972
1053
|
"""
|
|
973
1054
|
Validate payment amount.
|
|
974
1055
|
"""
|
|
975
1056
|
# Add commission if needed
|
|
976
1057
|
if self.commission_percent > 0:
|
|
977
|
-
|
|
1058
|
+
commission_factor = 1 + (self.commission_percent / 100)
|
|
1059
|
+
expected_amount = expected_amount * commission_factor
|
|
978
1060
|
expected_amount = round(expected_amount, 2)
|
|
979
1061
|
|
|
980
1062
|
# Allow small difference due to floating point precision
|
|
981
1063
|
if abs(received_amount - expected_amount) > 0.01:
|
|
982
1064
|
raise HTTPException(
|
|
983
1065
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
984
|
-
detail=
|
|
1066
|
+
detail=(
|
|
1067
|
+
f"Incorrect amount. Expected: {expected_amount}, "
|
|
1068
|
+
f"received: {received_amount}"
|
|
1069
|
+
)
|
|
985
1070
|
)
|
|
986
1071
|
|
|
987
1072
|
# Event methods that can be overridden by subclasses
|
|
988
1073
|
|
|
989
|
-
def transaction_already_exists(
|
|
1074
|
+
def transaction_already_exists(
|
|
1075
|
+
self,
|
|
1076
|
+
params: Dict[str, Any],
|
|
1077
|
+
transaction: PaymentTransaction
|
|
1078
|
+
) -> None:
|
|
990
1079
|
"""
|
|
991
1080
|
Called when a transaction already exists.
|
|
992
1081
|
|
|
@@ -996,7 +1085,12 @@ class ClickWebhookHandler:
|
|
|
996
1085
|
"""
|
|
997
1086
|
pass
|
|
998
1087
|
|
|
999
|
-
def transaction_created(
|
|
1088
|
+
def transaction_created(
|
|
1089
|
+
self,
|
|
1090
|
+
params: Dict[str, Any],
|
|
1091
|
+
transaction: PaymentTransaction,
|
|
1092
|
+
account: Any
|
|
1093
|
+
) -> None:
|
|
1000
1094
|
"""
|
|
1001
1095
|
Called when a transaction is created.
|
|
1002
1096
|
|
|
@@ -1007,7 +1101,11 @@ class ClickWebhookHandler:
|
|
|
1007
1101
|
"""
|
|
1008
1102
|
pass
|
|
1009
1103
|
|
|
1010
|
-
def successfully_payment(
|
|
1104
|
+
def successfully_payment(
|
|
1105
|
+
self,
|
|
1106
|
+
params: Dict[str, Any],
|
|
1107
|
+
transaction: PaymentTransaction
|
|
1108
|
+
) -> None:
|
|
1011
1109
|
"""
|
|
1012
1110
|
Called when a payment is successful.
|
|
1013
1111
|
|
|
@@ -1017,7 +1115,11 @@ class ClickWebhookHandler:
|
|
|
1017
1115
|
"""
|
|
1018
1116
|
pass
|
|
1019
1117
|
|
|
1020
|
-
def cancelled_payment(
|
|
1118
|
+
def cancelled_payment(
|
|
1119
|
+
self,
|
|
1120
|
+
params: Dict[str, Any],
|
|
1121
|
+
transaction: PaymentTransaction
|
|
1122
|
+
) -> None:
|
|
1021
1123
|
"""
|
|
1022
1124
|
Called when a payment is cancelled.
|
|
1023
1125
|
|
|
@@ -6,12 +6,15 @@ from typing import Dict, Any, Optional, List
|
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
class PaymentTransactionBase(BaseModel):
|
|
10
11
|
"""
|
|
11
12
|
Base schema for payment transaction.
|
|
12
13
|
"""
|
|
13
14
|
gateway: str = Field(..., description="Payment gateway (payme or click)")
|
|
14
|
-
transaction_id: str = Field(
|
|
15
|
+
transaction_id: str = Field(
|
|
16
|
+
..., description="Transaction ID from the payment system"
|
|
17
|
+
)
|
|
15
18
|
account_id: str = Field(..., description="Account or order ID")
|
|
16
19
|
amount: float = Field(..., description="Payment amount")
|
|
17
20
|
state: int = Field(0, description="Transaction state")
|
|
@@ -21,18 +24,27 @@ class PaymentTransactionCreate(PaymentTransactionBase):
|
|
|
21
24
|
"""
|
|
22
25
|
Schema for creating a payment transaction.
|
|
23
26
|
"""
|
|
24
|
-
extra_data: Optional[Dict[str, Any]] = Field(
|
|
27
|
+
extra_data: Optional[Dict[str, Any]] = Field(
|
|
28
|
+
None, description="Additional data for the transaction"
|
|
29
|
+
)
|
|
30
|
+
|
|
25
31
|
|
|
26
32
|
class PaymentTransaction(PaymentTransactionBase):
|
|
27
33
|
"""
|
|
28
34
|
Schema for payment transaction.
|
|
29
35
|
"""
|
|
30
36
|
id: int = Field(..., description="Transaction ID")
|
|
31
|
-
extra_data: Dict[str, Any] = Field(
|
|
37
|
+
extra_data: Dict[str, Any] = Field(
|
|
38
|
+
{}, description="Additional data for the transaction"
|
|
39
|
+
)
|
|
32
40
|
created_at: datetime = Field(..., description="Creation timestamp")
|
|
33
41
|
updated_at: datetime = Field(..., description="Last update timestamp")
|
|
34
|
-
performed_at: Optional[datetime] = Field(
|
|
35
|
-
|
|
42
|
+
performed_at: Optional[datetime] = Field(
|
|
43
|
+
None, description="Payment timestamp"
|
|
44
|
+
)
|
|
45
|
+
cancelled_at: Optional[datetime] = Field(
|
|
46
|
+
None, description="Cancellation timestamp"
|
|
47
|
+
)
|
|
36
48
|
|
|
37
49
|
class Config:
|
|
38
50
|
"""
|
|
@@ -45,9 +57,12 @@ class PaymentTransactionList(BaseModel):
|
|
|
45
57
|
"""
|
|
46
58
|
Schema for a list of payment transactions.
|
|
47
59
|
"""
|
|
48
|
-
transactions: List[PaymentTransaction] = Field(
|
|
60
|
+
transactions: List[PaymentTransaction] = Field(
|
|
61
|
+
..., description="List of transactions"
|
|
62
|
+
)
|
|
49
63
|
total: int = Field(..., description="Total number of transactions")
|
|
50
64
|
|
|
65
|
+
|
|
51
66
|
class PaymeWebhookRequest(BaseModel):
|
|
52
67
|
"""
|
|
53
68
|
Schema for Payme webhook request.
|
|
@@ -65,6 +80,7 @@ class PaymeWebhookResponse(BaseModel):
|
|
|
65
80
|
id: int = Field(..., description="Request ID")
|
|
66
81
|
result: Dict[str, Any] = Field(..., description="Response result")
|
|
67
82
|
|
|
83
|
+
|
|
68
84
|
class PaymeWebhookErrorResponse(BaseModel):
|
|
69
85
|
"""
|
|
70
86
|
Schema for Payme webhook error response.
|
|
@@ -88,12 +104,15 @@ class ClickWebhookRequest(BaseModel):
|
|
|
88
104
|
error: Optional[str] = Field(None, description="Error code")
|
|
89
105
|
error_note: Optional[str] = Field(None, description="Error note")
|
|
90
106
|
|
|
107
|
+
|
|
91
108
|
class ClickWebhookResponse(BaseModel):
|
|
92
109
|
"""
|
|
93
110
|
Schema for Click webhook response.
|
|
94
111
|
"""
|
|
95
112
|
click_trans_id: str = Field(..., description="Click transaction ID")
|
|
96
113
|
merchant_trans_id: str = Field(..., description="Merchant transaction ID")
|
|
97
|
-
merchant_prepare_id: Optional[int] = Field(
|
|
114
|
+
merchant_prepare_id: Optional[int] = Field(
|
|
115
|
+
None, description="Merchant prepare ID"
|
|
116
|
+
)
|
|
98
117
|
error: int = Field(0, description="Error code")
|
|
99
118
|
error_note: str = Field("Success", description="Error note")
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: paytechuz
|
|
3
|
+
Version: 0.2.8
|
|
4
|
+
Summary: Unified Python package for Uzbekistan payment gateways
|
|
5
|
+
Home-page: https://github.com/Muhammadali-Akbarov/paytechuz
|
|
6
|
+
Author: Muhammadali Akbarov
|
|
7
|
+
Author-email: muhammadali17abc@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.6
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: author-email
|
|
13
|
+
Dynamic: home-page
|
|
14
|
+
Dynamic: requires-python
|
|
15
|
+
|
|
16
|
+
# PayTechUZ
|
|
17
|
+
|
|
18
|
+
[](https://badge.fury.io/py/paytechuz)
|
|
19
|
+
[](https://pypi.org/project/paytechuz/)
|
|
20
|
+
[](https://opensource.org/licenses/MIT)
|
|
21
|
+
|
|
22
|
+
PayTechUZ is a unified payment library for integrating with popular payment systems in Uzbekistan. It provides a simple and consistent interface for working with Payme and Click payment gateways.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- 🔄 **API**: Consistent interface for multiple payment providers
|
|
27
|
+
- 🛡️ **Secure**: Built-in security features for payment processing
|
|
28
|
+
- 🔌 **Framework Integration**: Native support for Django and FastAPI
|
|
29
|
+
- 🌐 **Webhook Handling**: Easy-to-use webhook handlers for payment notifications
|
|
30
|
+
- 📊 **Transaction Management**: Automatic transaction tracking and management
|
|
31
|
+
- 🧩 **Extensible**: Easy to add new payment providers
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
### Basic Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install paytechuz
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Framework-Specific Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# For Django
|
|
44
|
+
pip install paytechuz[django]
|
|
45
|
+
|
|
46
|
+
# For FastAPI
|
|
47
|
+
pip install paytechuz[fastapi]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
### Generate Payment Links
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from paytechuz.gateways.payme import PaymeGateway
|
|
56
|
+
from paytechuz.gateways.click import ClickGateway
|
|
57
|
+
|
|
58
|
+
# Initialize Payme gateway
|
|
59
|
+
payme = PaymeGateway(
|
|
60
|
+
payme_id="your_payme_id",
|
|
61
|
+
payme_key="your_payme_key",
|
|
62
|
+
is_test_mode=True # Set to False in production environment
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Initialize Click gateway
|
|
66
|
+
click = ClickGateway(
|
|
67
|
+
service_id="your_service_id",
|
|
68
|
+
merchant_id="your_merchant_id",
|
|
69
|
+
merchant_user_id="your_merchant_user_id",
|
|
70
|
+
secret_key="your_secret_key",
|
|
71
|
+
is_test_mode=True # Set to False in production environment
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Generate payment links
|
|
75
|
+
payme_link = payme.create_payment(
|
|
76
|
+
id="order_123",
|
|
77
|
+
amount=150000, # amount in UZS
|
|
78
|
+
return_url="https://example.com/return"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
click_link = click.create_payment(
|
|
82
|
+
id="order_123",
|
|
83
|
+
amount=150000, # amount in UZS
|
|
84
|
+
description="Test payment",
|
|
85
|
+
return_url="https://example.com/return"
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Django Integration
|
|
90
|
+
|
|
91
|
+
1. Create Order model:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# models.py
|
|
95
|
+
from django.db import models
|
|
96
|
+
from django.utils import timezone
|
|
97
|
+
|
|
98
|
+
class Order(models.Model):
|
|
99
|
+
STATUS_CHOICES = (
|
|
100
|
+
('pending', 'Pending'),
|
|
101
|
+
('paid', 'Paid'),
|
|
102
|
+
('cancelled', 'Cancelled'),
|
|
103
|
+
('delivered', 'Delivered'),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
product_name = models.CharField(max_length=255)
|
|
107
|
+
amount = models.DecimalField(max_digits=12, decimal_places=2)
|
|
108
|
+
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
|
109
|
+
created_at = models.DateTimeField(default=timezone.now)
|
|
110
|
+
|
|
111
|
+
def __str__(self):
|
|
112
|
+
return f"{self.id} - {self.product_name} ({self.amount})"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
2. Add to `INSTALLED_APPS` and configure settings:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
# settings.py
|
|
119
|
+
INSTALLED_APPS = [
|
|
120
|
+
# ...
|
|
121
|
+
'paytechuz.integrations.django',
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
PAYME_ID = 'your_payme_merchant_id'
|
|
125
|
+
PAYME_KEY = 'your_payme_merchant_key'
|
|
126
|
+
PAYME_ACCOUNT_MODEL = 'your_app.models.Order' # For example: 'orders.models.Order'
|
|
127
|
+
PAYME_ACCOUNT_FIELD = 'id'
|
|
128
|
+
PAYME_AMOUNT_FIELD = 'amount' # Field for storing payment amount
|
|
129
|
+
PAYME_ONE_TIME_PAYMENT = True # Allow only one payment per account
|
|
130
|
+
|
|
131
|
+
CLICK_SERVICE_ID = 'your_click_service_id'
|
|
132
|
+
CLICK_MERCHANT_ID = 'your_click_merchant_id'
|
|
133
|
+
CLICK_SECRET_KEY = 'your_click_secret_key'
|
|
134
|
+
CLICK_ACCOUNT_MODEL = 'your_app.models.Order'
|
|
135
|
+
CLICK_COMMISSION_PERCENT = 0.0
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
3. Create webhook handlers:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
# views.py
|
|
142
|
+
from paytechuz.integrations.django.views import BasePaymeWebhookView, BaseClickWebhookView
|
|
143
|
+
from .models import Order
|
|
144
|
+
|
|
145
|
+
class PaymeWebhookView(BasePaymeWebhookView):
|
|
146
|
+
def successfully_payment(self, params, transaction):
|
|
147
|
+
order = Order.objects.get(id=transaction.account_id)
|
|
148
|
+
order.status = 'paid'
|
|
149
|
+
order.save()
|
|
150
|
+
|
|
151
|
+
def cancelled_payment(self, params, transaction):
|
|
152
|
+
order = Order.objects.get(id=transaction.account_id)
|
|
153
|
+
order.status = 'cancelled'
|
|
154
|
+
order.save()
|
|
155
|
+
|
|
156
|
+
class ClickWebhookView(BaseClickWebhookView):
|
|
157
|
+
def successfully_payment(self, params, transaction):
|
|
158
|
+
order = Order.objects.get(id=transaction.account_id)
|
|
159
|
+
order.status = 'paid'
|
|
160
|
+
order.save()
|
|
161
|
+
|
|
162
|
+
def cancelled_payment(self, params, transaction):
|
|
163
|
+
order = Order.objects.get(id=transaction.account_id)
|
|
164
|
+
order.status = 'cancelled'
|
|
165
|
+
order.save()
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
4. Add webhook URLs to `urls.py`:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
# urls.py
|
|
172
|
+
from django.urls import path
|
|
173
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
174
|
+
from .views import PaymeWebhookView, ClickWebhookView
|
|
175
|
+
|
|
176
|
+
urlpatterns = [
|
|
177
|
+
# ...
|
|
178
|
+
path('payments/webhook/payme/', csrf_exempt(PaymeWebhookView.as_view()), name='payme_webhook'),
|
|
179
|
+
path('payments/webhook/click/', csrf_exempt(ClickWebhookView.as_view()), name='click_webhook'),
|
|
180
|
+
]
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### FastAPI Integration
|
|
184
|
+
|
|
185
|
+
1. Set up database models:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime
|
|
189
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
190
|
+
from sqlalchemy.orm import sessionmaker
|
|
191
|
+
from paytechuz.integrations.fastapi import Base as PaymentsBase
|
|
192
|
+
from paytechuz.integrations.fastapi.models import run_migrations
|
|
193
|
+
from datetime import datetime, timezone
|
|
194
|
+
|
|
195
|
+
# Create database engine
|
|
196
|
+
SQLALCHEMY_DATABASE_URL = "sqlite:///./payments.db"
|
|
197
|
+
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
|
198
|
+
|
|
199
|
+
# Create base declarative class
|
|
200
|
+
Base = declarative_base()
|
|
201
|
+
|
|
202
|
+
# Create Order model
|
|
203
|
+
class Order(Base):
|
|
204
|
+
__tablename__ = "orders"
|
|
205
|
+
|
|
206
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
207
|
+
product_name = Column(String, index=True)
|
|
208
|
+
amount = Column(Float)
|
|
209
|
+
status = Column(String, default="pending")
|
|
210
|
+
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
|
211
|
+
|
|
212
|
+
# Create payment tables using run_migrations
|
|
213
|
+
run_migrations(engine)
|
|
214
|
+
|
|
215
|
+
# Create Order table
|
|
216
|
+
Base.metadata.create_all(bind=engine)
|
|
217
|
+
|
|
218
|
+
# Create session
|
|
219
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
2. Create webhook handlers:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from fastapi import FastAPI, Request, Depends
|
|
226
|
+
from sqlalchemy.orm import Session
|
|
227
|
+
from paytechuz.integrations.fastapi import PaymeWebhookHandler, ClickWebhookHandler
|
|
228
|
+
|
|
229
|
+
app = FastAPI()
|
|
230
|
+
|
|
231
|
+
# Dependency to get the database session
|
|
232
|
+
def get_db():
|
|
233
|
+
db = SessionLocal()
|
|
234
|
+
try:
|
|
235
|
+
yield db
|
|
236
|
+
finally:
|
|
237
|
+
db.close()
|
|
238
|
+
|
|
239
|
+
class CustomPaymeWebhookHandler(PaymeWebhookHandler):
|
|
240
|
+
def successfully_payment(self, params, transaction):
|
|
241
|
+
# Handle successful payment
|
|
242
|
+
order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
|
|
243
|
+
order.status = "paid"
|
|
244
|
+
self.db.commit()
|
|
245
|
+
|
|
246
|
+
def cancelled_payment(self, params, transaction):
|
|
247
|
+
# Handle cancelled payment
|
|
248
|
+
order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
|
|
249
|
+
order.status = "cancelled"
|
|
250
|
+
self.db.commit()
|
|
251
|
+
|
|
252
|
+
class CustomClickWebhookHandler(ClickWebhookHandler):
|
|
253
|
+
def successfully_payment(self, params, transaction):
|
|
254
|
+
# Handle successful payment
|
|
255
|
+
order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
|
|
256
|
+
order.status = "paid"
|
|
257
|
+
self.db.commit()
|
|
258
|
+
|
|
259
|
+
def cancelled_payment(self, params, transaction):
|
|
260
|
+
# Handle cancelled payment
|
|
261
|
+
order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
|
|
262
|
+
order.status = "cancelled"
|
|
263
|
+
self.db.commit()
|
|
264
|
+
|
|
265
|
+
@app.post("/payments/payme/webhook")
|
|
266
|
+
async def payme_webhook(request: Request, db: Session = Depends(get_db)):
|
|
267
|
+
handler = CustomPaymeWebhookHandler(
|
|
268
|
+
db=db,
|
|
269
|
+
payme_id="your_merchant_id",
|
|
270
|
+
payme_key="your_merchant_key",
|
|
271
|
+
account_model=Order,
|
|
272
|
+
account_field='id',
|
|
273
|
+
amount_field='amount'
|
|
274
|
+
)
|
|
275
|
+
return await handler.handle_webhook(request)
|
|
276
|
+
|
|
277
|
+
@app.post("/payments/click/webhook")
|
|
278
|
+
async def click_webhook(request: Request, db: Session = Depends(get_db)):
|
|
279
|
+
handler = CustomClickWebhookHandler(
|
|
280
|
+
db=db,
|
|
281
|
+
service_id="your_service_id",
|
|
282
|
+
merchant_id="your_merchant_id",
|
|
283
|
+
secret_key="your_secret_key",
|
|
284
|
+
account_model=Order
|
|
285
|
+
)
|
|
286
|
+
return await handler.handle_webhook(request)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Documentation
|
|
290
|
+
|
|
291
|
+
Detailed documentation is available in multiple languages:
|
|
292
|
+
|
|
293
|
+
- 📖 [English Documentation](src/docs/en/index.md)
|
|
294
|
+
- 📖 [O'zbek tilidagi hujjatlar](src/docs/index.md)
|
|
295
|
+
|
|
296
|
+
### Framework-Specific Documentation
|
|
297
|
+
|
|
298
|
+
- [Django Integration Guide](src/docs/en/django_integration.md) | [Django integratsiyasi bo'yicha qo'llanma](src/docs/django_integration.md)
|
|
299
|
+
- [FastAPI Integration Guide](src/docs/en/fastapi_integration.md) | [FastAPI integratsiyasi bo'yicha qo'llanma](src/docs/fastapi_integration.md)
|
|
300
|
+
|
|
301
|
+
## Supported Payment Systems
|
|
302
|
+
|
|
303
|
+
- **Payme** - [Official Website](https://payme.uz)
|
|
304
|
+
- **Click** - [Official Website](https://click.uz)
|
|
305
|
+
|
|
306
|
+
## Contributing
|
|
307
|
+
|
|
308
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -27,10 +27,10 @@ paytechuz/integrations/django/webhooks.py,sha256=cP_Jc3VlyyvyzDbBd2yEVHikw60th1_
|
|
|
27
27
|
paytechuz/integrations/django/migrations/0001_initial.py,sha256=SWHIUuwq91crzaxa9v1UK0kay8CxsjUo6t4bqg7j0Gw,1896
|
|
28
28
|
paytechuz/integrations/django/migrations/__init__.py,sha256=KLQ5NdjOMLDS21-u3b_g08G1MjPMMhG95XI_N8m4FSo,41
|
|
29
29
|
paytechuz/integrations/fastapi/__init__.py,sha256=DLnhAZQZf2ghu8BuFFfE7FzbNKWQQ2SLG8qxldRuwR4,565
|
|
30
|
-
paytechuz/integrations/fastapi/models.py,sha256=
|
|
31
|
-
paytechuz/integrations/fastapi/routes.py,sha256=
|
|
32
|
-
paytechuz/integrations/fastapi/schemas.py,sha256=
|
|
33
|
-
paytechuz-0.2.
|
|
34
|
-
paytechuz-0.2.
|
|
35
|
-
paytechuz-0.2.
|
|
36
|
-
paytechuz-0.2.
|
|
30
|
+
paytechuz/integrations/fastapi/models.py,sha256=uayHUDt0nN6hIkhbGrM1NZRXzpCPvaPMPEjowJFUQPI,4425
|
|
31
|
+
paytechuz/integrations/fastapi/routes.py,sha256=X7ejcICe4lFtpsKMXxvyrklqHWQJMhR-AdhcitSiXlE,37647
|
|
32
|
+
paytechuz/integrations/fastapi/schemas.py,sha256=PgRqviJiD4-u3_CIkUOX8R7L8Yqn8L44WLte7968G0E,3887
|
|
33
|
+
paytechuz-0.2.8.dist-info/METADATA,sha256=YKr2kp5NH_VapJ1DCn7H6zVQ_cerzHvXwnYAHqBtIfM,9407
|
|
34
|
+
paytechuz-0.2.8.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
35
|
+
paytechuz-0.2.8.dist-info/top_level.txt,sha256=oloyKGNVj9Z2h3wpKG5yPyTlpdpWW0-CWr-j-asCWBc,10
|
|
36
|
+
paytechuz-0.2.8.dist-info/RECORD,,
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: paytechuz
|
|
3
|
-
Version: 0.2.6
|
|
4
|
-
Summary: Unified Python package for Uzbekistan payment gateways
|
|
5
|
-
Home-page: https://github.com/Muhammadali-Akbarov/paytechuz
|
|
6
|
-
Author: Muhammadali Akbarov
|
|
7
|
-
Author-email: muhammadali17abc@gmail.com
|
|
8
|
-
License: MIT
|
|
9
|
-
Requires-Python: >=3.6
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
Dynamic: author
|
|
12
|
-
Dynamic: author-email
|
|
13
|
-
Dynamic: home-page
|
|
14
|
-
Dynamic: requires-python
|
|
15
|
-
|
|
16
|
-
# PayTechUZ
|
|
17
|
-
|
|
18
|
-
[](https://badge.fury.io/py/paytechuz)
|
|
19
|
-
[](https://pypi.org/project/paytechuz/)
|
|
20
|
-
[](https://opensource.org/licenses/MIT)
|
|
21
|
-
## Installation
|
|
22
|
-
|
|
23
|
-
### Basic Installation
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
pip install paytechuz
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
### Framework-Specific Installation
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
# For Django
|
|
33
|
-
pip install paytechuz[django]
|
|
34
|
-
|
|
35
|
-
# For FastAPI
|
|
36
|
-
pip install paytechuz[fastapi]
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Quick Start
|
|
40
|
-
|
|
41
|
-
### Generate Payment Links
|
|
42
|
-
|
|
43
|
-
```python
|
|
44
|
-
from paytechuz import create_gateway, PaymentGateway
|
|
45
|
-
|
|
46
|
-
# Initialize gateways
|
|
47
|
-
payme = create_gateway(PaymentGateway.PAYME.value,
|
|
48
|
-
payme_id="your_payme_id",
|
|
49
|
-
payme_key="your_payme_key",
|
|
50
|
-
is_test_mode=True
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
click = create_gateway(PaymentGateway.CLICK.value,
|
|
54
|
-
service_id="your_service_id",
|
|
55
|
-
merchant_id="your_merchant_id",
|
|
56
|
-
merchant_user_id="your_merchant_user_id",
|
|
57
|
-
secret_key="your_secret_key",
|
|
58
|
-
is_test_mode=True
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
# Generate payment links
|
|
62
|
-
payme_link = payme.create_payment(
|
|
63
|
-
id="order_123",
|
|
64
|
-
amount=150000, # amount in UZS
|
|
65
|
-
return_url="https://example.com/return"
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
click_link = click.create_payment(
|
|
69
|
-
id="order_123",
|
|
70
|
-
amount=150000, # amount in UZS
|
|
71
|
-
description="Test payment",
|
|
72
|
-
return_url="https://example.com/return"
|
|
73
|
-
)
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
### Django Integration
|
|
77
|
-
|
|
78
|
-
1. Add to `INSTALLED_APPS`:
|
|
79
|
-
|
|
80
|
-
```python
|
|
81
|
-
# settings.py
|
|
82
|
-
INSTALLED_APPS = [
|
|
83
|
-
# ...
|
|
84
|
-
'paytechuz.integrations.django',
|
|
85
|
-
]
|
|
86
|
-
|
|
87
|
-
PAYME_ID = 'your_payme_merchant_id'
|
|
88
|
-
PAYME_KEY = 'your_payme_merchant_key'
|
|
89
|
-
PAYME_ACCOUNT_MODEL = 'your_app.models.YourModel' # For example: 'orders.models.Order'
|
|
90
|
-
PAYME_ACCOUNT_FIELD = 'id'
|
|
91
|
-
PAYME_AMOUNT_FIELD = 'amount' # Field for storing payment amount
|
|
92
|
-
PAYME_ONE_TIME_PAYMENT = True # Allow only one payment per account
|
|
93
|
-
|
|
94
|
-
CLICK_SERVICE_ID = 'your_click_service_id'
|
|
95
|
-
CLICK_MERCHANT_ID = 'your_click_merchant_id'
|
|
96
|
-
CLICK_SECRET_KEY = 'your_click_secret_key'
|
|
97
|
-
CLICK_ACCOUNT_MODEL = 'your_app.models.YourModel'
|
|
98
|
-
CLICK_COMMISSION_PERCENT = 0.0
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
2. Create webhook handlers:
|
|
102
|
-
|
|
103
|
-
```python
|
|
104
|
-
# views.py
|
|
105
|
-
from paytechuz.integrations.django.views import BasePaymeWebhookView
|
|
106
|
-
from .models import Order
|
|
107
|
-
|
|
108
|
-
class PaymeWebhookView(BasePaymeWebhookView):
|
|
109
|
-
def successfully_payment(self, params, transaction):
|
|
110
|
-
order = Order.objects.get(id=transaction.account_id)
|
|
111
|
-
order.status = 'paid'
|
|
112
|
-
order.save()
|
|
113
|
-
|
|
114
|
-
def cancelled_payment(self, params, transaction):
|
|
115
|
-
order = Order.objects.get(id=transaction.account_id)
|
|
116
|
-
order.status = 'cancelled'
|
|
117
|
-
order.save()
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
3. Add webhook URLs to `urls.py`:
|
|
121
|
-
|
|
122
|
-
```python
|
|
123
|
-
# urls.py
|
|
124
|
-
from django.urls import path
|
|
125
|
-
from .views import PaymeWebhookView
|
|
126
|
-
|
|
127
|
-
urlpatterns = [
|
|
128
|
-
# ...
|
|
129
|
-
path('payments/webhook/payme/', PaymeWebhookView.as_view(), name='payme_webhook'),
|
|
130
|
-
]
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### FastAPI Integration
|
|
134
|
-
|
|
135
|
-
1. Create webhook handler:
|
|
136
|
-
|
|
137
|
-
```python
|
|
138
|
-
from fastapi import FastAPI, Request
|
|
139
|
-
from paytechuz.integrations.fastapi import PaymeWebhookHandler
|
|
140
|
-
|
|
141
|
-
app = FastAPI()
|
|
142
|
-
|
|
143
|
-
class CustomPaymeWebhookHandler(PaymeWebhookHandler):
|
|
144
|
-
def successfully_payment(self, params, transaction):
|
|
145
|
-
# Handle successful payment
|
|
146
|
-
order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
|
|
147
|
-
order.status = "paid"
|
|
148
|
-
self.db.commit()
|
|
149
|
-
|
|
150
|
-
def cancelled_payment(self, params, transaction):
|
|
151
|
-
# Handle cancelled payment
|
|
152
|
-
order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
|
|
153
|
-
order.status = "cancelled"
|
|
154
|
-
self.db.commit()
|
|
155
|
-
|
|
156
|
-
@app.post("/payments/payme/webhook")
|
|
157
|
-
async def payme_webhook(request: Request):
|
|
158
|
-
handler = CustomPaymeWebhookHandler(
|
|
159
|
-
payme_id="your_merchant_id",
|
|
160
|
-
payme_key="your_merchant_key"
|
|
161
|
-
)
|
|
162
|
-
return await handler.handle_webhook(request)
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
## Documentation
|
|
166
|
-
|
|
167
|
-
Detailed documentation is available in multiple languages:
|
|
168
|
-
|
|
169
|
-
- 📖 [English Documentation](docs/en/index.md)
|
|
170
|
-
- 📖 [O'zbek tilidagi hujjatlar](docs/uz/index.md)
|
|
File without changes
|
|
File without changes
|