paytechuz 0.2.7__tar.gz → 0.2.8__tar.gz
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-0.2.8/PKG-INFO +312 -0
- paytechuz-0.2.8/README.md +297 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/pyproject.toml +1 -1
- {paytechuz-0.2.7 → paytechuz-0.2.8}/setup.py +1 -1
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/gateways/click/client.py +13 -20
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/integrations/fastapi/models.py +55 -23
- {paytechuz-0.2.7/src/paytechuz → paytechuz-0.2.8/src}/integrations/fastapi/schemas.py +31 -14
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/integrations/fastapi/models.py +16 -11
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/integrations/fastapi/routes.py +163 -61
- {paytechuz-0.2.7/src → paytechuz-0.2.8/src/paytechuz}/integrations/fastapi/schemas.py +26 -7
- paytechuz-0.2.8/src/paytechuz.egg-info/PKG-INFO +312 -0
- paytechuz-0.2.7/PKG-INFO +0 -170
- paytechuz-0.2.7/README.md +0 -155
- paytechuz-0.2.7/src/paytechuz.egg-info/PKG-INFO +0 -170
- {paytechuz-0.2.7 → paytechuz-0.2.8}/MANIFEST.in +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/setup.cfg +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/core/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/core/base.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/core/constants.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/core/exceptions.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/core/http.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/core/payme/errors.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/core/utils.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/gateways/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/gateways/click/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/gateways/click/merchant.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/gateways/click/webhook.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/gateways/payme/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/gateways/payme/cards.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/gateways/payme/client.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/gateways/payme/receipts.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/gateways/payme/webhook.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/integrations/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/integrations/django/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/integrations/django/admin.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/integrations/django/apps.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/integrations/django/migrations/0001_initial.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/integrations/django/migrations/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/integrations/django/models.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/integrations/django/signals.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/integrations/django/views.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/integrations/django/webhooks.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/integrations/fastapi/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/integrations/fastapi/routes.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/core/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/core/base.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/core/constants.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/core/exceptions.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/core/http.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/core/payme/errors.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/core/utils.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/gateways/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/gateways/click/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/gateways/click/client.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/gateways/click/merchant.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/gateways/click/webhook.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/gateways/payme/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/gateways/payme/cards.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/gateways/payme/client.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/gateways/payme/receipts.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/gateways/payme/webhook.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/integrations/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/integrations/django/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/integrations/django/admin.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/integrations/django/apps.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/integrations/django/migrations/0001_initial.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/integrations/django/migrations/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/integrations/django/models.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/integrations/django/signals.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/integrations/django/views.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/integrations/django/webhooks.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz/integrations/fastapi/__init__.py +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz.egg-info/SOURCES.txt +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz.egg-info/dependency_links.txt +0 -0
- {paytechuz-0.2.7 → paytechuz-0.2.8}/src/paytechuz.egg-info/top_level.txt +0 -0
paytechuz-0.2.8/PKG-INFO
ADDED
|
@@ -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.
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# PayTechUZ
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/paytechuz)
|
|
4
|
+
[](https://pypi.org/project/paytechuz/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🔄 **API**: Consistent interface for multiple payment providers
|
|
12
|
+
- 🛡️ **Secure**: Built-in security features for payment processing
|
|
13
|
+
- 🔌 **Framework Integration**: Native support for Django and FastAPI
|
|
14
|
+
- 🌐 **Webhook Handling**: Easy-to-use webhook handlers for payment notifications
|
|
15
|
+
- 📊 **Transaction Management**: Automatic transaction tracking and management
|
|
16
|
+
- 🧩 **Extensible**: Easy to add new payment providers
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### Basic Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install paytechuz
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Framework-Specific Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# For Django
|
|
29
|
+
pip install paytechuz[django]
|
|
30
|
+
|
|
31
|
+
# For FastAPI
|
|
32
|
+
pip install paytechuz[fastapi]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
### Generate Payment Links
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from paytechuz.gateways.payme import PaymeGateway
|
|
41
|
+
from paytechuz.gateways.click import ClickGateway
|
|
42
|
+
|
|
43
|
+
# Initialize Payme gateway
|
|
44
|
+
payme = PaymeGateway(
|
|
45
|
+
payme_id="your_payme_id",
|
|
46
|
+
payme_key="your_payme_key",
|
|
47
|
+
is_test_mode=True # Set to False in production environment
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Initialize Click gateway
|
|
51
|
+
click = ClickGateway(
|
|
52
|
+
service_id="your_service_id",
|
|
53
|
+
merchant_id="your_merchant_id",
|
|
54
|
+
merchant_user_id="your_merchant_user_id",
|
|
55
|
+
secret_key="your_secret_key",
|
|
56
|
+
is_test_mode=True # Set to False in production environment
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Generate payment links
|
|
60
|
+
payme_link = payme.create_payment(
|
|
61
|
+
id="order_123",
|
|
62
|
+
amount=150000, # amount in UZS
|
|
63
|
+
return_url="https://example.com/return"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
click_link = click.create_payment(
|
|
67
|
+
id="order_123",
|
|
68
|
+
amount=150000, # amount in UZS
|
|
69
|
+
description="Test payment",
|
|
70
|
+
return_url="https://example.com/return"
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Django Integration
|
|
75
|
+
|
|
76
|
+
1. Create Order model:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# models.py
|
|
80
|
+
from django.db import models
|
|
81
|
+
from django.utils import timezone
|
|
82
|
+
|
|
83
|
+
class Order(models.Model):
|
|
84
|
+
STATUS_CHOICES = (
|
|
85
|
+
('pending', 'Pending'),
|
|
86
|
+
('paid', 'Paid'),
|
|
87
|
+
('cancelled', 'Cancelled'),
|
|
88
|
+
('delivered', 'Delivered'),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
product_name = models.CharField(max_length=255)
|
|
92
|
+
amount = models.DecimalField(max_digits=12, decimal_places=2)
|
|
93
|
+
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
|
|
94
|
+
created_at = models.DateTimeField(default=timezone.now)
|
|
95
|
+
|
|
96
|
+
def __str__(self):
|
|
97
|
+
return f"{self.id} - {self.product_name} ({self.amount})"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
2. Add to `INSTALLED_APPS` and configure settings:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
# settings.py
|
|
104
|
+
INSTALLED_APPS = [
|
|
105
|
+
# ...
|
|
106
|
+
'paytechuz.integrations.django',
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
PAYME_ID = 'your_payme_merchant_id'
|
|
110
|
+
PAYME_KEY = 'your_payme_merchant_key'
|
|
111
|
+
PAYME_ACCOUNT_MODEL = 'your_app.models.Order' # For example: 'orders.models.Order'
|
|
112
|
+
PAYME_ACCOUNT_FIELD = 'id'
|
|
113
|
+
PAYME_AMOUNT_FIELD = 'amount' # Field for storing payment amount
|
|
114
|
+
PAYME_ONE_TIME_PAYMENT = True # Allow only one payment per account
|
|
115
|
+
|
|
116
|
+
CLICK_SERVICE_ID = 'your_click_service_id'
|
|
117
|
+
CLICK_MERCHANT_ID = 'your_click_merchant_id'
|
|
118
|
+
CLICK_SECRET_KEY = 'your_click_secret_key'
|
|
119
|
+
CLICK_ACCOUNT_MODEL = 'your_app.models.Order'
|
|
120
|
+
CLICK_COMMISSION_PERCENT = 0.0
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
3. Create webhook handlers:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
# views.py
|
|
127
|
+
from paytechuz.integrations.django.views import BasePaymeWebhookView, BaseClickWebhookView
|
|
128
|
+
from .models import Order
|
|
129
|
+
|
|
130
|
+
class PaymeWebhookView(BasePaymeWebhookView):
|
|
131
|
+
def successfully_payment(self, params, transaction):
|
|
132
|
+
order = Order.objects.get(id=transaction.account_id)
|
|
133
|
+
order.status = 'paid'
|
|
134
|
+
order.save()
|
|
135
|
+
|
|
136
|
+
def cancelled_payment(self, params, transaction):
|
|
137
|
+
order = Order.objects.get(id=transaction.account_id)
|
|
138
|
+
order.status = 'cancelled'
|
|
139
|
+
order.save()
|
|
140
|
+
|
|
141
|
+
class ClickWebhookView(BaseClickWebhookView):
|
|
142
|
+
def successfully_payment(self, params, transaction):
|
|
143
|
+
order = Order.objects.get(id=transaction.account_id)
|
|
144
|
+
order.status = 'paid'
|
|
145
|
+
order.save()
|
|
146
|
+
|
|
147
|
+
def cancelled_payment(self, params, transaction):
|
|
148
|
+
order = Order.objects.get(id=transaction.account_id)
|
|
149
|
+
order.status = 'cancelled'
|
|
150
|
+
order.save()
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
4. Add webhook URLs to `urls.py`:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# urls.py
|
|
157
|
+
from django.urls import path
|
|
158
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
159
|
+
from .views import PaymeWebhookView, ClickWebhookView
|
|
160
|
+
|
|
161
|
+
urlpatterns = [
|
|
162
|
+
# ...
|
|
163
|
+
path('payments/webhook/payme/', csrf_exempt(PaymeWebhookView.as_view()), name='payme_webhook'),
|
|
164
|
+
path('payments/webhook/click/', csrf_exempt(ClickWebhookView.as_view()), name='click_webhook'),
|
|
165
|
+
]
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### FastAPI Integration
|
|
169
|
+
|
|
170
|
+
1. Set up database models:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime
|
|
174
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
175
|
+
from sqlalchemy.orm import sessionmaker
|
|
176
|
+
from paytechuz.integrations.fastapi import Base as PaymentsBase
|
|
177
|
+
from paytechuz.integrations.fastapi.models import run_migrations
|
|
178
|
+
from datetime import datetime, timezone
|
|
179
|
+
|
|
180
|
+
# Create database engine
|
|
181
|
+
SQLALCHEMY_DATABASE_URL = "sqlite:///./payments.db"
|
|
182
|
+
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
|
183
|
+
|
|
184
|
+
# Create base declarative class
|
|
185
|
+
Base = declarative_base()
|
|
186
|
+
|
|
187
|
+
# Create Order model
|
|
188
|
+
class Order(Base):
|
|
189
|
+
__tablename__ = "orders"
|
|
190
|
+
|
|
191
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
192
|
+
product_name = Column(String, index=True)
|
|
193
|
+
amount = Column(Float)
|
|
194
|
+
status = Column(String, default="pending")
|
|
195
|
+
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
|
196
|
+
|
|
197
|
+
# Create payment tables using run_migrations
|
|
198
|
+
run_migrations(engine)
|
|
199
|
+
|
|
200
|
+
# Create Order table
|
|
201
|
+
Base.metadata.create_all(bind=engine)
|
|
202
|
+
|
|
203
|
+
# Create session
|
|
204
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
2. Create webhook handlers:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from fastapi import FastAPI, Request, Depends
|
|
211
|
+
from sqlalchemy.orm import Session
|
|
212
|
+
from paytechuz.integrations.fastapi import PaymeWebhookHandler, ClickWebhookHandler
|
|
213
|
+
|
|
214
|
+
app = FastAPI()
|
|
215
|
+
|
|
216
|
+
# Dependency to get the database session
|
|
217
|
+
def get_db():
|
|
218
|
+
db = SessionLocal()
|
|
219
|
+
try:
|
|
220
|
+
yield db
|
|
221
|
+
finally:
|
|
222
|
+
db.close()
|
|
223
|
+
|
|
224
|
+
class CustomPaymeWebhookHandler(PaymeWebhookHandler):
|
|
225
|
+
def successfully_payment(self, params, transaction):
|
|
226
|
+
# Handle successful payment
|
|
227
|
+
order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
|
|
228
|
+
order.status = "paid"
|
|
229
|
+
self.db.commit()
|
|
230
|
+
|
|
231
|
+
def cancelled_payment(self, params, transaction):
|
|
232
|
+
# Handle cancelled payment
|
|
233
|
+
order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
|
|
234
|
+
order.status = "cancelled"
|
|
235
|
+
self.db.commit()
|
|
236
|
+
|
|
237
|
+
class CustomClickWebhookHandler(ClickWebhookHandler):
|
|
238
|
+
def successfully_payment(self, params, transaction):
|
|
239
|
+
# Handle successful payment
|
|
240
|
+
order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
|
|
241
|
+
order.status = "paid"
|
|
242
|
+
self.db.commit()
|
|
243
|
+
|
|
244
|
+
def cancelled_payment(self, params, transaction):
|
|
245
|
+
# Handle cancelled payment
|
|
246
|
+
order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
|
|
247
|
+
order.status = "cancelled"
|
|
248
|
+
self.db.commit()
|
|
249
|
+
|
|
250
|
+
@app.post("/payments/payme/webhook")
|
|
251
|
+
async def payme_webhook(request: Request, db: Session = Depends(get_db)):
|
|
252
|
+
handler = CustomPaymeWebhookHandler(
|
|
253
|
+
db=db,
|
|
254
|
+
payme_id="your_merchant_id",
|
|
255
|
+
payme_key="your_merchant_key",
|
|
256
|
+
account_model=Order,
|
|
257
|
+
account_field='id',
|
|
258
|
+
amount_field='amount'
|
|
259
|
+
)
|
|
260
|
+
return await handler.handle_webhook(request)
|
|
261
|
+
|
|
262
|
+
@app.post("/payments/click/webhook")
|
|
263
|
+
async def click_webhook(request: Request, db: Session = Depends(get_db)):
|
|
264
|
+
handler = CustomClickWebhookHandler(
|
|
265
|
+
db=db,
|
|
266
|
+
service_id="your_service_id",
|
|
267
|
+
merchant_id="your_merchant_id",
|
|
268
|
+
secret_key="your_secret_key",
|
|
269
|
+
account_model=Order
|
|
270
|
+
)
|
|
271
|
+
return await handler.handle_webhook(request)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Documentation
|
|
275
|
+
|
|
276
|
+
Detailed documentation is available in multiple languages:
|
|
277
|
+
|
|
278
|
+
- 📖 [English Documentation](src/docs/en/index.md)
|
|
279
|
+
- 📖 [O'zbek tilidagi hujjatlar](src/docs/index.md)
|
|
280
|
+
|
|
281
|
+
### Framework-Specific Documentation
|
|
282
|
+
|
|
283
|
+
- [Django Integration Guide](src/docs/en/django_integration.md) | [Django integratsiyasi bo'yicha qo'llanma](src/docs/django_integration.md)
|
|
284
|
+
- [FastAPI Integration Guide](src/docs/en/fastapi_integration.md) | [FastAPI integratsiyasi bo'yicha qo'llanma](src/docs/fastapi_integration.md)
|
|
285
|
+
|
|
286
|
+
## Supported Payment Systems
|
|
287
|
+
|
|
288
|
+
- **Payme** - [Official Website](https://payme.uz)
|
|
289
|
+
- **Click** - [Official Website](https://click.uz)
|
|
290
|
+
|
|
291
|
+
## Contributing
|
|
292
|
+
|
|
293
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
294
|
+
|
|
295
|
+
## License
|
|
296
|
+
|
|
297
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -7,7 +7,7 @@ from typing import Dict, Any, Optional, Union
|
|
|
7
7
|
from paytechuz.core.base import BasePaymentGateway
|
|
8
8
|
from paytechuz.core.http import HttpClient
|
|
9
9
|
from paytechuz.core.constants import ClickNetworks
|
|
10
|
-
from paytechuz.core.utils import
|
|
10
|
+
from paytechuz.core.utils import handle_exceptions
|
|
11
11
|
from paytechuz.gateways.click.merchant import ClickMerchantApi
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
@@ -65,7 +65,7 @@ class ClickGateway(BasePaymentGateway):
|
|
|
65
65
|
id: Union[int, str],
|
|
66
66
|
amount: Union[int, float, str],
|
|
67
67
|
**kwargs
|
|
68
|
-
) ->
|
|
68
|
+
) -> str:
|
|
69
69
|
"""
|
|
70
70
|
Create a payment using Click.
|
|
71
71
|
|
|
@@ -81,13 +81,12 @@ class ClickGateway(BasePaymentGateway):
|
|
|
81
81
|
- email: Customer email
|
|
82
82
|
|
|
83
83
|
Returns:
|
|
84
|
-
|
|
84
|
+
Payment URL string for redirecting the user to Click payment page
|
|
85
85
|
"""
|
|
86
|
-
# Format amount
|
|
87
|
-
amount_tiyin = format_amount(amount)
|
|
86
|
+
# Format amount for URL (no need to convert to tiyin for URL)
|
|
88
87
|
|
|
89
88
|
# Extract additional parameters
|
|
90
|
-
description = kwargs.get('description', f'Payment for account {
|
|
89
|
+
description = kwargs.get('description', f'Payment for account {id}')
|
|
91
90
|
return_url = kwargs.get('return_url')
|
|
92
91
|
callback_url = kwargs.get('callback_url')
|
|
93
92
|
# These parameters are not used in the URL but are available in the API
|
|
@@ -111,18 +110,8 @@ class ClickGateway(BasePaymentGateway):
|
|
|
111
110
|
if description:
|
|
112
111
|
payment_url += f"&merchant_user_id={description}"
|
|
113
112
|
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
'transaction_id': transaction_id,
|
|
119
|
-
'payment_url': payment_url,
|
|
120
|
-
'amount': amount,
|
|
121
|
-
'account_id': id,
|
|
122
|
-
'status': 'created',
|
|
123
|
-
'service_id': self.service_id,
|
|
124
|
-
'merchant_id': self.merchant_id
|
|
125
|
-
}
|
|
113
|
+
# Return the payment URL directly
|
|
114
|
+
return payment_url
|
|
126
115
|
|
|
127
116
|
@handle_exceptions
|
|
128
117
|
def check_payment(self, transaction_id: str) -> Dict[str, Any]:
|
|
@@ -139,7 +128,9 @@ class ClickGateway(BasePaymentGateway):
|
|
|
139
128
|
# Format: click_account_id_amount
|
|
140
129
|
parts = transaction_id.split('_')
|
|
141
130
|
if len(parts) < 3 or parts[0] != 'click':
|
|
142
|
-
raise ValueError(
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"Invalid transaction ID format: {transaction_id}"
|
|
133
|
+
)
|
|
143
134
|
|
|
144
135
|
account_id = parts[1]
|
|
145
136
|
|
|
@@ -188,7 +179,9 @@ class ClickGateway(BasePaymentGateway):
|
|
|
188
179
|
# Format: click_account_id_amount
|
|
189
180
|
parts = transaction_id.split('_')
|
|
190
181
|
if len(parts) < 3 or parts[0] != 'click':
|
|
191
|
-
raise ValueError(
|
|
182
|
+
raise ValueError(
|
|
183
|
+
f"Invalid transaction ID format: {transaction_id}"
|
|
184
|
+
)
|
|
192
185
|
|
|
193
186
|
account_id = parts[1]
|
|
194
187
|
|