paytechuz 0.2.8b0__py3-none-any.whl → 0.2.10__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 +33 -11
- paytechuz/integrations/fastapi/routes.py +158 -60
- paytechuz/integrations/fastapi/schemas.py +26 -7
- paytechuz-0.2.10.dist-info/METADATA +318 -0
- {paytechuz-0.2.8b0.dist-info → paytechuz-0.2.10.dist-info}/RECORD +7 -7
- {paytechuz-0.2.8b0.dist-info → paytechuz-0.2.10.dist-info}/WHEEL +1 -1
- paytechuz-0.2.8b0.dist-info/METADATA +0 -170
- {paytechuz-0.2.8b0.dist-info → paytechuz-0.2.10.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,24 +125,16 @@ 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
|
-
# Set state based on current state and reason
|
|
136
137
|
if self.state == self.INITIATING or reason_code == 3:
|
|
137
|
-
# If transaction is in INITIATING state or reason is 3 (execution error),
|
|
138
|
-
# set state to CANCELLED_DURING_INIT (-1)
|
|
139
138
|
self.state = self.CANCELLED_DURING_INIT
|
|
140
139
|
else:
|
|
141
140
|
# Otherwise, set state to CANCELLED (-2)
|
|
@@ -155,3 +154,26 @@ class PaymentTransaction(Base):
|
|
|
155
154
|
db.refresh(self)
|
|
156
155
|
|
|
157
156
|
return self
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def run_migrations(engine: Any) -> None:
|
|
160
|
+
"""
|
|
161
|
+
Run database migrations for PayTechUZ FastAPI integration.
|
|
162
|
+
|
|
163
|
+
This function creates all necessary tables in the database for the
|
|
164
|
+
PayTechUZ payment system. Call this function when setting up your FastAPI
|
|
165
|
+
application to ensure all required database tables are created.
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
```python
|
|
169
|
+
from sqlalchemy import create_engine
|
|
170
|
+
from paytechuz.integrations.fastapi.models import run_migrations
|
|
171
|
+
|
|
172
|
+
engine = create_engine("sqlite:///./payments.db")
|
|
173
|
+
run_migrations(engine)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
engine: SQLAlchemy engine instance
|
|
178
|
+
"""
|
|
179
|
+
Base.metadata.create_all(bind=engine)
|
|
@@ -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,7 +361,8 @@ 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
368
|
if t.state not in [
|
|
@@ -373,9 +373,11 @@ class PaymeWebhookHandler:
|
|
|
373
373
|
]
|
|
374
374
|
|
|
375
375
|
if non_final_transactions:
|
|
376
|
-
# 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
|
|
377
378
|
raise InvalidAccount(
|
|
378
|
-
f"Account with {self.account_field}={account.id}
|
|
379
|
+
(f"Account with {self.account_field}={account.id} "
|
|
380
|
+
f"already has a pending transaction")
|
|
379
381
|
)
|
|
380
382
|
|
|
381
383
|
# Check for existing transaction with the same transaction_id
|
|
@@ -390,7 +392,9 @@ class PaymeWebhookHandler:
|
|
|
390
392
|
|
|
391
393
|
# For existing transactions, use the original time from extra_data
|
|
392
394
|
# This ensures the same response is returned for repeated calls
|
|
393
|
-
create_time = transaction.extra_data.get(
|
|
395
|
+
create_time = transaction.extra_data.get(
|
|
396
|
+
'create_time', params.get('time')
|
|
397
|
+
)
|
|
394
398
|
|
|
395
399
|
return {
|
|
396
400
|
'transaction': transaction.transaction_id,
|
|
@@ -407,7 +411,9 @@ class PaymeWebhookHandler:
|
|
|
407
411
|
state=PaymentTransaction.INITIATING,
|
|
408
412
|
extra_data={
|
|
409
413
|
'account_field': self.account_field,
|
|
410
|
-
'account_value':
|
|
414
|
+
'account_value': (
|
|
415
|
+
params.get('account', {}).get(self.account_field)
|
|
416
|
+
),
|
|
411
417
|
'create_time': params.get('time'),
|
|
412
418
|
'raw_params': params
|
|
413
419
|
}
|
|
@@ -420,7 +426,8 @@ class PaymeWebhookHandler:
|
|
|
420
426
|
# Call the event method
|
|
421
427
|
self.transaction_created(params, transaction, account)
|
|
422
428
|
|
|
423
|
-
# Use the time from the request params
|
|
429
|
+
# Use the time from the request params
|
|
430
|
+
# instead of transaction.created_at
|
|
424
431
|
create_time = params.get('time')
|
|
425
432
|
|
|
426
433
|
return {
|
|
@@ -455,7 +462,9 @@ class PaymeWebhookHandler:
|
|
|
455
462
|
return {
|
|
456
463
|
'transaction': transaction.transaction_id,
|
|
457
464
|
'state': transaction.state,
|
|
458
|
-
'perform_time': int(
|
|
465
|
+
'perform_time': int(
|
|
466
|
+
transaction.performed_at.timestamp() * 1000
|
|
467
|
+
) if transaction.performed_at else 0,
|
|
459
468
|
}
|
|
460
469
|
|
|
461
470
|
def _check_transaction(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -479,18 +488,28 @@ class PaymeWebhookHandler:
|
|
|
479
488
|
self.check_transaction(params, transaction)
|
|
480
489
|
|
|
481
490
|
# Use the original time from extra_data for consistency
|
|
482
|
-
create_time = transaction.extra_data.get(
|
|
491
|
+
create_time = transaction.extra_data.get(
|
|
492
|
+
'create_time', int(transaction.created_at.timestamp() * 1000)
|
|
493
|
+
)
|
|
483
494
|
|
|
484
495
|
return {
|
|
485
496
|
'transaction': transaction.transaction_id,
|
|
486
497
|
'state': transaction.state,
|
|
487
498
|
'create_time': create_time,
|
|
488
|
-
'perform_time':
|
|
489
|
-
|
|
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
|
+
),
|
|
490
507
|
'reason': transaction.reason,
|
|
491
508
|
}
|
|
492
509
|
|
|
493
|
-
def _cancel_response(
|
|
510
|
+
def _cancel_response(
|
|
511
|
+
self, transaction: PaymentTransaction
|
|
512
|
+
) -> Dict[str, Any]:
|
|
494
513
|
"""
|
|
495
514
|
Helper method to generate cancel transaction response.
|
|
496
515
|
|
|
@@ -515,7 +534,8 @@ class PaymeWebhookHandler:
|
|
|
515
534
|
return {
|
|
516
535
|
'transaction': transaction.transaction_id,
|
|
517
536
|
'state': transaction.state,
|
|
518
|
-
'cancel_time': int(transaction.cancelled_at.timestamp() * 1000)
|
|
537
|
+
'cancel_time': (int(transaction.cancelled_at.timestamp() * 1000)
|
|
538
|
+
if transaction.cancelled_at else 0),
|
|
519
539
|
'reason': reason,
|
|
520
540
|
}
|
|
521
541
|
|
|
@@ -538,15 +558,22 @@ class PaymeWebhookHandler:
|
|
|
538
558
|
)
|
|
539
559
|
|
|
540
560
|
# Check if transaction is already cancelled
|
|
541
|
-
|
|
542
|
-
|
|
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
|
|
543
568
|
if 'reason' in params:
|
|
544
569
|
reason = params.get('reason')
|
|
545
570
|
|
|
546
|
-
# If reason is not provided, use default reason
|
|
571
|
+
# If reason is not provided, use default reason
|
|
572
|
+
# from PaymeCancelReason
|
|
547
573
|
if reason is None:
|
|
548
574
|
from paytechuz.core.constants import PaymeCancelReason
|
|
549
|
-
|
|
575
|
+
# Default reason 5
|
|
576
|
+
reason = PaymeCancelReason.REASON_FUND_RETURNED
|
|
550
577
|
|
|
551
578
|
# Convert reason to int if it's a string
|
|
552
579
|
if isinstance(reason, str) and reason.isdigit():
|
|
@@ -573,7 +600,8 @@ class PaymeWebhookHandler:
|
|
|
573
600
|
# Ensure the reason is stored in extra_data
|
|
574
601
|
extra_data = transaction.extra_data or {}
|
|
575
602
|
if 'cancel_reason' not in extra_data:
|
|
576
|
-
|
|
603
|
+
# Default reason 5 if none provided
|
|
604
|
+
extra_data['cancel_reason'] = reason if reason is not None else 5
|
|
577
605
|
transaction.extra_data = extra_data
|
|
578
606
|
self.db.commit()
|
|
579
607
|
self.db.refresh(transaction)
|
|
@@ -620,9 +648,18 @@ class PaymeWebhookHandler:
|
|
|
620
648
|
self.account_field: transaction.account_id
|
|
621
649
|
},
|
|
622
650
|
'state': transaction.state,
|
|
623
|
-
'create_time': transaction.extra_data.get(
|
|
624
|
-
|
|
625
|
-
|
|
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
|
+
),
|
|
626
663
|
'reason': transaction.reason,
|
|
627
664
|
})
|
|
628
665
|
|
|
@@ -633,7 +670,9 @@ class PaymeWebhookHandler:
|
|
|
633
670
|
|
|
634
671
|
# Event methods that can be overridden by subclasses
|
|
635
672
|
|
|
636
|
-
def before_check_perform_transaction(
|
|
673
|
+
def before_check_perform_transaction(
|
|
674
|
+
self, params: Dict[str, Any], account: Any
|
|
675
|
+
) -> None:
|
|
637
676
|
"""
|
|
638
677
|
Called before checking if a transaction can be performed.
|
|
639
678
|
|
|
@@ -643,7 +682,9 @@ class PaymeWebhookHandler:
|
|
|
643
682
|
"""
|
|
644
683
|
pass
|
|
645
684
|
|
|
646
|
-
def transaction_already_exists(
|
|
685
|
+
def transaction_already_exists(
|
|
686
|
+
self, params: Dict[str, Any], transaction: PaymentTransaction
|
|
687
|
+
) -> None:
|
|
647
688
|
"""
|
|
648
689
|
Called when a transaction already exists.
|
|
649
690
|
|
|
@@ -653,7 +694,12 @@ class PaymeWebhookHandler:
|
|
|
653
694
|
"""
|
|
654
695
|
pass
|
|
655
696
|
|
|
656
|
-
def transaction_created(
|
|
697
|
+
def transaction_created(
|
|
698
|
+
self,
|
|
699
|
+
params: Dict[str, Any],
|
|
700
|
+
transaction: PaymentTransaction,
|
|
701
|
+
account: Any
|
|
702
|
+
) -> None:
|
|
657
703
|
"""
|
|
658
704
|
Called when a transaction is created.
|
|
659
705
|
|
|
@@ -664,7 +710,11 @@ class PaymeWebhookHandler:
|
|
|
664
710
|
"""
|
|
665
711
|
pass
|
|
666
712
|
|
|
667
|
-
def successfully_payment(
|
|
713
|
+
def successfully_payment(
|
|
714
|
+
self,
|
|
715
|
+
params: Dict[str, Any],
|
|
716
|
+
transaction: PaymentTransaction
|
|
717
|
+
) -> None:
|
|
668
718
|
"""
|
|
669
719
|
Called when a payment is successful.
|
|
670
720
|
|
|
@@ -674,7 +724,11 @@ class PaymeWebhookHandler:
|
|
|
674
724
|
"""
|
|
675
725
|
pass
|
|
676
726
|
|
|
677
|
-
def check_transaction(
|
|
727
|
+
def check_transaction(
|
|
728
|
+
self,
|
|
729
|
+
params: Dict[str, Any],
|
|
730
|
+
transaction: PaymentTransaction
|
|
731
|
+
) -> None:
|
|
678
732
|
"""
|
|
679
733
|
Called when checking a transaction.
|
|
680
734
|
|
|
@@ -684,7 +738,11 @@ class PaymeWebhookHandler:
|
|
|
684
738
|
"""
|
|
685
739
|
pass
|
|
686
740
|
|
|
687
|
-
def cancelled_payment(
|
|
741
|
+
def cancelled_payment(
|
|
742
|
+
self,
|
|
743
|
+
params: Dict[str, Any],
|
|
744
|
+
transaction: PaymentTransaction
|
|
745
|
+
) -> None:
|
|
688
746
|
"""
|
|
689
747
|
Called when a payment is cancelled.
|
|
690
748
|
|
|
@@ -694,7 +752,11 @@ class PaymeWebhookHandler:
|
|
|
694
752
|
"""
|
|
695
753
|
pass
|
|
696
754
|
|
|
697
|
-
def get_statement(
|
|
755
|
+
def get_statement(
|
|
756
|
+
self,
|
|
757
|
+
params: Dict[str, Any],
|
|
758
|
+
transactions: list
|
|
759
|
+
) -> None:
|
|
698
760
|
"""
|
|
699
761
|
Called when getting a statement.
|
|
700
762
|
|
|
@@ -723,7 +785,9 @@ class ClickWebhookHandler:
|
|
|
723
785
|
print(f"Payment successful: {transaction.transaction_id}")
|
|
724
786
|
|
|
725
787
|
# Update your order status
|
|
726
|
-
order = db.query(Order)
|
|
788
|
+
order = (db.query(Order)
|
|
789
|
+
.filter(Order.id == transaction.account_id)
|
|
790
|
+
.first())
|
|
727
791
|
order.status = 'paid'
|
|
728
792
|
db.commit()
|
|
729
793
|
```
|
|
@@ -792,7 +856,8 @@ class ClickWebhookHandler:
|
|
|
792
856
|
|
|
793
857
|
# Validate amount
|
|
794
858
|
try:
|
|
795
|
-
|
|
859
|
+
expected = float(getattr(account, 'amount', 0))
|
|
860
|
+
self._validate_amount(amount, expected)
|
|
796
861
|
except Exception as e:
|
|
797
862
|
logger.error(f"Invalid amount: {e}")
|
|
798
863
|
return {
|
|
@@ -890,7 +955,8 @@ class ClickWebhookHandler:
|
|
|
890
955
|
self.successfully_payment(params, transaction)
|
|
891
956
|
else:
|
|
892
957
|
# Mark transaction as cancelled
|
|
893
|
-
|
|
958
|
+
error_reason = f"Error code: {error}"
|
|
959
|
+
transaction.mark_as_cancelled(self.db, reason=error_reason)
|
|
894
960
|
|
|
895
961
|
# Call the event method
|
|
896
962
|
self.cancelled_payment(params, transaction)
|
|
@@ -941,10 +1007,15 @@ class ClickWebhookHandler:
|
|
|
941
1007
|
)
|
|
942
1008
|
|
|
943
1009
|
# Create string to sign
|
|
944
|
-
to_sign =
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
+
)
|
|
948
1019
|
|
|
949
1020
|
# Generate signature
|
|
950
1021
|
signature = hashlib.md5(to_sign.encode('utf-8')).hexdigest()
|
|
@@ -963,7 +1034,11 @@ class ClickWebhookHandler:
|
|
|
963
1034
|
if isinstance(merchant_trans_id, str) and merchant_trans_id.isdigit():
|
|
964
1035
|
merchant_trans_id = int(merchant_trans_id)
|
|
965
1036
|
|
|
966
|
-
account =
|
|
1037
|
+
account = (
|
|
1038
|
+
self.db.query(self.account_model)
|
|
1039
|
+
.filter_by(id=merchant_trans_id)
|
|
1040
|
+
.first()
|
|
1041
|
+
)
|
|
967
1042
|
if not account:
|
|
968
1043
|
raise HTTPException(
|
|
969
1044
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
@@ -972,25 +1047,35 @@ class ClickWebhookHandler:
|
|
|
972
1047
|
|
|
973
1048
|
return account
|
|
974
1049
|
|
|
975
|
-
def _validate_amount(
|
|
1050
|
+
def _validate_amount(
|
|
1051
|
+
self, received_amount: float, expected_amount: float
|
|
1052
|
+
) -> None:
|
|
976
1053
|
"""
|
|
977
1054
|
Validate payment amount.
|
|
978
1055
|
"""
|
|
979
1056
|
# Add commission if needed
|
|
980
1057
|
if self.commission_percent > 0:
|
|
981
|
-
|
|
1058
|
+
commission_factor = 1 + (self.commission_percent / 100)
|
|
1059
|
+
expected_amount = expected_amount * commission_factor
|
|
982
1060
|
expected_amount = round(expected_amount, 2)
|
|
983
1061
|
|
|
984
1062
|
# Allow small difference due to floating point precision
|
|
985
1063
|
if abs(received_amount - expected_amount) > 0.01:
|
|
986
1064
|
raise HTTPException(
|
|
987
1065
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
988
|
-
detail=
|
|
1066
|
+
detail=(
|
|
1067
|
+
f"Incorrect amount. Expected: {expected_amount}, "
|
|
1068
|
+
f"received: {received_amount}"
|
|
1069
|
+
)
|
|
989
1070
|
)
|
|
990
1071
|
|
|
991
1072
|
# Event methods that can be overridden by subclasses
|
|
992
1073
|
|
|
993
|
-
def transaction_already_exists(
|
|
1074
|
+
def transaction_already_exists(
|
|
1075
|
+
self,
|
|
1076
|
+
params: Dict[str, Any],
|
|
1077
|
+
transaction: PaymentTransaction
|
|
1078
|
+
) -> None:
|
|
994
1079
|
"""
|
|
995
1080
|
Called when a transaction already exists.
|
|
996
1081
|
|
|
@@ -1000,7 +1085,12 @@ class ClickWebhookHandler:
|
|
|
1000
1085
|
"""
|
|
1001
1086
|
pass
|
|
1002
1087
|
|
|
1003
|
-
def transaction_created(
|
|
1088
|
+
def transaction_created(
|
|
1089
|
+
self,
|
|
1090
|
+
params: Dict[str, Any],
|
|
1091
|
+
transaction: PaymentTransaction,
|
|
1092
|
+
account: Any
|
|
1093
|
+
) -> None:
|
|
1004
1094
|
"""
|
|
1005
1095
|
Called when a transaction is created.
|
|
1006
1096
|
|
|
@@ -1011,7 +1101,11 @@ class ClickWebhookHandler:
|
|
|
1011
1101
|
"""
|
|
1012
1102
|
pass
|
|
1013
1103
|
|
|
1014
|
-
def successfully_payment(
|
|
1104
|
+
def successfully_payment(
|
|
1105
|
+
self,
|
|
1106
|
+
params: Dict[str, Any],
|
|
1107
|
+
transaction: PaymentTransaction
|
|
1108
|
+
) -> None:
|
|
1015
1109
|
"""
|
|
1016
1110
|
Called when a payment is successful.
|
|
1017
1111
|
|
|
@@ -1021,7 +1115,11 @@ class ClickWebhookHandler:
|
|
|
1021
1115
|
"""
|
|
1022
1116
|
pass
|
|
1023
1117
|
|
|
1024
|
-
def cancelled_payment(
|
|
1118
|
+
def cancelled_payment(
|
|
1119
|
+
self,
|
|
1120
|
+
params: Dict[str, Any],
|
|
1121
|
+
transaction: PaymentTransaction
|
|
1122
|
+
) -> None:
|
|
1025
1123
|
"""
|
|
1026
1124
|
Called when a payment is cancelled.
|
|
1027
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,318 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: paytechuz
|
|
3
|
+
Version: 0.2.10
|
|
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 datetime import datetime, timezone
|
|
189
|
+
|
|
190
|
+
from sqlalchemy.orm import sessionmaker
|
|
191
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
192
|
+
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime
|
|
193
|
+
|
|
194
|
+
from paytechuz.integrations.fastapi import Base as PaymentsBase
|
|
195
|
+
from paytechuz.integrations.fastapi.models import run_migrations
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# Create database engine
|
|
199
|
+
SQLALCHEMY_DATABASE_URL = "sqlite:///./payments.db"
|
|
200
|
+
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
|
201
|
+
|
|
202
|
+
# Create base declarative class
|
|
203
|
+
Base = declarative_base()
|
|
204
|
+
|
|
205
|
+
# Create Order model
|
|
206
|
+
class Order(Base):
|
|
207
|
+
__tablename__ = "orders"
|
|
208
|
+
|
|
209
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
210
|
+
product_name = Column(String, index=True)
|
|
211
|
+
amount = Column(Float)
|
|
212
|
+
status = Column(String, default="pending")
|
|
213
|
+
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
|
214
|
+
|
|
215
|
+
# Create payment tables using run_migrations
|
|
216
|
+
run_migrations(engine)
|
|
217
|
+
|
|
218
|
+
# Create Order table
|
|
219
|
+
Base.metadata.create_all(bind=engine)
|
|
220
|
+
|
|
221
|
+
# Create session
|
|
222
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
2. Create webhook handlers:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from fastapi import FastAPI, Request, Depends
|
|
229
|
+
|
|
230
|
+
from sqlalchemy.orm import Session
|
|
231
|
+
|
|
232
|
+
from paytechuz.integrations.fastapi import PaymeWebhookHandler, ClickWebhookHandler
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
app = FastAPI()
|
|
236
|
+
|
|
237
|
+
# Dependency to get the database session
|
|
238
|
+
def get_db():
|
|
239
|
+
db = SessionLocal()
|
|
240
|
+
try:
|
|
241
|
+
yield db
|
|
242
|
+
finally:
|
|
243
|
+
db.close()
|
|
244
|
+
|
|
245
|
+
class CustomPaymeWebhookHandler(PaymeWebhookHandler):
|
|
246
|
+
def successfully_payment(self, params, transaction):
|
|
247
|
+
# Handle successful payment
|
|
248
|
+
order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
|
|
249
|
+
order.status = "paid"
|
|
250
|
+
self.db.commit()
|
|
251
|
+
|
|
252
|
+
def cancelled_payment(self, params, transaction):
|
|
253
|
+
# Handle cancelled payment
|
|
254
|
+
order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
|
|
255
|
+
order.status = "cancelled"
|
|
256
|
+
self.db.commit()
|
|
257
|
+
|
|
258
|
+
class CustomClickWebhookHandler(ClickWebhookHandler):
|
|
259
|
+
def successfully_payment(self, params, transaction):
|
|
260
|
+
# Handle successful payment
|
|
261
|
+
order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
|
|
262
|
+
order.status = "paid"
|
|
263
|
+
self.db.commit()
|
|
264
|
+
|
|
265
|
+
def cancelled_payment(self, params, transaction):
|
|
266
|
+
# Handle cancelled payment
|
|
267
|
+
order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
|
|
268
|
+
order.status = "cancelled"
|
|
269
|
+
self.db.commit()
|
|
270
|
+
|
|
271
|
+
@app.post("/payments/payme/webhook")
|
|
272
|
+
async def payme_webhook(request: Request, db: Session = Depends(get_db)):
|
|
273
|
+
handler = CustomPaymeWebhookHandler(
|
|
274
|
+
db=db,
|
|
275
|
+
payme_id="your_merchant_id",
|
|
276
|
+
payme_key="your_merchant_key",
|
|
277
|
+
account_model=Order,
|
|
278
|
+
account_field='id',
|
|
279
|
+
amount_field='amount'
|
|
280
|
+
)
|
|
281
|
+
return await handler.handle_webhook(request)
|
|
282
|
+
|
|
283
|
+
@app.post("/payments/click/webhook")
|
|
284
|
+
async def click_webhook(request: Request, db: Session = Depends(get_db)):
|
|
285
|
+
handler = CustomClickWebhookHandler(
|
|
286
|
+
db=db,
|
|
287
|
+
service_id="your_service_id",
|
|
288
|
+
merchant_id="your_merchant_id",
|
|
289
|
+
secret_key="your_secret_key",
|
|
290
|
+
account_model=Order
|
|
291
|
+
)
|
|
292
|
+
return await handler.handle_webhook(request)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Documentation
|
|
296
|
+
|
|
297
|
+
Detailed documentation is available in multiple languages:
|
|
298
|
+
|
|
299
|
+
- 📖 [English Documentation](src/docs/en/index.md)
|
|
300
|
+
- 📖 [O'zbek tilidagi hujjatlar](src/docs/index.md)
|
|
301
|
+
|
|
302
|
+
### Framework-Specific Documentation
|
|
303
|
+
|
|
304
|
+
- [Django Integration Guide](src/docs/en/django_integration.md) | [Django integratsiyasi bo'yicha qo'llanma](src/docs/django_integration.md)
|
|
305
|
+
- [FastAPI Integration Guide](src/docs/en/fastapi_integration.md) | [FastAPI integratsiyasi bo'yicha qo'llanma](src/docs/fastapi_integration.md)
|
|
306
|
+
|
|
307
|
+
## Supported Payment Systems
|
|
308
|
+
|
|
309
|
+
- **Payme** - [Official Website](https://payme.uz)
|
|
310
|
+
- **Click** - [Official Website](https://click.uz)
|
|
311
|
+
|
|
312
|
+
## Contributing
|
|
313
|
+
|
|
314
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
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=9IqrsndIVuIDwDbijZ89biJxEWQASXRBfWVShxgerAc,5113
|
|
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.10.dist-info/METADATA,sha256=eQP9iaSd-TgqTNeJkB3ECKFYTsqZpG_dFqwLpkWCrsw,9414
|
|
34
|
+
paytechuz-0.2.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
35
|
+
paytechuz-0.2.10.dist-info/top_level.txt,sha256=oloyKGNVj9Z2h3wpKG5yPyTlpdpWW0-CWr-j-asCWBc,10
|
|
36
|
+
paytechuz-0.2.10.dist-info/RECORD,,
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: paytechuz
|
|
3
|
-
Version: 0.2.8b0
|
|
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
|