paytechuz 0.1.0__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.
- core/__init__.py +0 -0
- core/base.py +88 -0
- core/constants.py +68 -0
- core/exceptions.py +190 -0
- core/http.py +268 -0
- core/utils.py +192 -0
- gateways/__init__.py +0 -0
- gateways/click/__init__.py +0 -0
- gateways/click/client.py +202 -0
- gateways/click/merchant.py +264 -0
- gateways/click/webhook.py +227 -0
- gateways/payme/__init__.py +0 -0
- gateways/payme/cards.py +222 -0
- gateways/payme/client.py +238 -0
- gateways/payme/receipts.py +336 -0
- gateways/payme/webhook.py +379 -0
- integrations/__init__.py +0 -0
- integrations/django/__init__.py +4 -0
- integrations/django/admin.py +78 -0
- integrations/django/apps.py +21 -0
- integrations/django/migrations/0001_initial.py +51 -0
- integrations/django/migrations/__init__.py +3 -0
- integrations/django/models.py +174 -0
- integrations/django/signals.py +46 -0
- integrations/django/views.py +100 -0
- integrations/django/webhooks.py +880 -0
- integrations/fastapi/__init__.py +21 -0
- integrations/fastapi/models.py +151 -0
- integrations/fastapi/routes.py +1028 -0
- integrations/fastapi/schemas.py +99 -0
- paytechuz-0.1.0.dist-info/METADATA +198 -0
- paytechuz-0.1.0.dist-info/RECORD +36 -0
- paytechuz-0.1.0.dist-info/WHEEL +5 -0
- paytechuz-0.1.0.dist-info/top_level.txt +4 -0
- tests/__init__.py +1 -0
- tests/test_gateway.py +70 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Click webhook handler.
|
|
3
|
+
"""
|
|
4
|
+
import hashlib
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Dict, Any, Callable
|
|
7
|
+
|
|
8
|
+
from paytechuz.core.base import BaseWebhookHandler
|
|
9
|
+
from paytechuz.core.constants import ClickActions
|
|
10
|
+
from paytechuz.core.exceptions import (
|
|
11
|
+
PermissionDenied,
|
|
12
|
+
InvalidAmount,
|
|
13
|
+
TransactionNotFound,
|
|
14
|
+
UnsupportedMethod,
|
|
15
|
+
AccountNotFound
|
|
16
|
+
)
|
|
17
|
+
from paytechuz.core.utils import handle_exceptions
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
class ClickWebhookHandler(BaseWebhookHandler):
|
|
22
|
+
"""
|
|
23
|
+
Click webhook handler.
|
|
24
|
+
|
|
25
|
+
This class handles webhook requests from the Click payment system,
|
|
26
|
+
including transaction preparation and completion.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
service_id: str,
|
|
32
|
+
secret_key: str,
|
|
33
|
+
find_transaction_func: Callable[[str], Dict[str, Any]],
|
|
34
|
+
find_account_func: Callable[[str], Dict[str, Any]],
|
|
35
|
+
create_transaction_func: Callable[[Dict[str, Any]], Dict[str, Any]],
|
|
36
|
+
complete_transaction_func: Callable[[str, bool], Dict[str, Any]],
|
|
37
|
+
commission_percent: float = 0.0
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Initialize the Click webhook handler.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
service_id: Click service ID
|
|
44
|
+
secret_key: Secret key for authentication
|
|
45
|
+
find_transaction_func: Function to find a transaction by ID
|
|
46
|
+
find_account_func: Function to find an account by ID
|
|
47
|
+
create_transaction_func: Function to create a transaction
|
|
48
|
+
complete_transaction_func: Function to complete a transaction
|
|
49
|
+
commission_percent: Commission percentage
|
|
50
|
+
"""
|
|
51
|
+
self.service_id = service_id
|
|
52
|
+
self.secret_key = secret_key
|
|
53
|
+
self.find_transaction = find_transaction_func
|
|
54
|
+
self.find_account = find_account_func
|
|
55
|
+
self.create_transaction = create_transaction_func
|
|
56
|
+
self.complete_transaction = complete_transaction_func
|
|
57
|
+
self.commission_percent = commission_percent
|
|
58
|
+
|
|
59
|
+
def _check_auth(self, params: Dict[str, Any]) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Check authentication using signature.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
params: Request parameters
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
PermissionDenied: If authentication fails
|
|
68
|
+
"""
|
|
69
|
+
if str(params.get('service_id')) != self.service_id:
|
|
70
|
+
raise PermissionDenied("Invalid service ID")
|
|
71
|
+
|
|
72
|
+
# Check signature if secret key is provided
|
|
73
|
+
if self.secret_key:
|
|
74
|
+
sign_string = params.get('sign_string')
|
|
75
|
+
sign_time = params.get('sign_time')
|
|
76
|
+
|
|
77
|
+
if not sign_string or not sign_time:
|
|
78
|
+
raise PermissionDenied("Missing signature parameters")
|
|
79
|
+
|
|
80
|
+
# Create string to sign
|
|
81
|
+
to_sign = f"{params.get('click_trans_id')}{params.get('service_id')}"
|
|
82
|
+
to_sign += f"{self.secret_key}{params.get('merchant_trans_id')}"
|
|
83
|
+
to_sign += f"{params.get('amount')}{params.get('action')}"
|
|
84
|
+
to_sign += f"{sign_time}"
|
|
85
|
+
|
|
86
|
+
# Generate signature
|
|
87
|
+
signature = hashlib.md5(to_sign.encode('utf-8')).hexdigest()
|
|
88
|
+
|
|
89
|
+
if signature != sign_string:
|
|
90
|
+
raise PermissionDenied("Invalid signature")
|
|
91
|
+
|
|
92
|
+
def _validate_amount(
|
|
93
|
+
self,
|
|
94
|
+
received_amount: float,
|
|
95
|
+
expected_amount: float
|
|
96
|
+
) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Validate payment amount.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
received_amount: Amount received from Click
|
|
102
|
+
expected_amount: Expected amount
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
InvalidAmount: If amounts don't match
|
|
106
|
+
"""
|
|
107
|
+
# Add commission if needed
|
|
108
|
+
if self.commission_percent > 0:
|
|
109
|
+
expected_amount = expected_amount * (1 + self.commission_percent / 100)
|
|
110
|
+
expected_amount = round(expected_amount, 2)
|
|
111
|
+
|
|
112
|
+
# Allow small difference due to floating point precision
|
|
113
|
+
if abs(received_amount - expected_amount) > 0.01:
|
|
114
|
+
raise InvalidAmount(f"Incorrect amount. Expected: {expected_amount}, received: {received_amount}")
|
|
115
|
+
|
|
116
|
+
@handle_exceptions
|
|
117
|
+
def handle_webhook(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
118
|
+
"""
|
|
119
|
+
Handle webhook data from Click.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
data: The webhook data received from Click
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Dict containing the response to be sent back to Click
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
PermissionDenied: If authentication fails
|
|
129
|
+
UnsupportedMethod: If the requested action is not supported
|
|
130
|
+
"""
|
|
131
|
+
# Check authentication
|
|
132
|
+
self._check_auth(data)
|
|
133
|
+
|
|
134
|
+
# Extract parameters
|
|
135
|
+
click_trans_id = data.get('click_trans_id')
|
|
136
|
+
merchant_trans_id = data.get('merchant_trans_id')
|
|
137
|
+
amount = float(data.get('amount', 0))
|
|
138
|
+
action = int(data.get('action', -1))
|
|
139
|
+
error = int(data.get('error', 0))
|
|
140
|
+
|
|
141
|
+
# Find account
|
|
142
|
+
try:
|
|
143
|
+
account = self.find_account(merchant_trans_id)
|
|
144
|
+
except AccountNotFound:
|
|
145
|
+
logger.error(f"Account not found: {merchant_trans_id}")
|
|
146
|
+
return {
|
|
147
|
+
'click_trans_id': click_trans_id,
|
|
148
|
+
'merchant_trans_id': merchant_trans_id,
|
|
149
|
+
'error': -5,
|
|
150
|
+
'error_note': "User not found"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Validate amount
|
|
154
|
+
try:
|
|
155
|
+
self._validate_amount(amount, float(account.get('amount', 0)))
|
|
156
|
+
except InvalidAmount as e:
|
|
157
|
+
logger.error(f"Invalid amount: {e}")
|
|
158
|
+
return {
|
|
159
|
+
'click_trans_id': click_trans_id,
|
|
160
|
+
'merchant_trans_id': merchant_trans_id,
|
|
161
|
+
'error': -2,
|
|
162
|
+
'error_note': str(e)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Check if transaction already exists
|
|
166
|
+
try:
|
|
167
|
+
transaction = self.find_transaction(click_trans_id)
|
|
168
|
+
|
|
169
|
+
# If transaction is already completed, return success
|
|
170
|
+
if transaction.get('state') == 2: # SUCCESSFULLY
|
|
171
|
+
return {
|
|
172
|
+
'click_trans_id': click_trans_id,
|
|
173
|
+
'merchant_trans_id': merchant_trans_id,
|
|
174
|
+
'merchant_prepare_id': transaction.get('id'),
|
|
175
|
+
'error': 0,
|
|
176
|
+
'error_note': "Success"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# If transaction is cancelled, return error
|
|
180
|
+
if transaction.get('state') == -2: # CANCELLED
|
|
181
|
+
return {
|
|
182
|
+
'click_trans_id': click_trans_id,
|
|
183
|
+
'merchant_trans_id': merchant_trans_id,
|
|
184
|
+
'merchant_prepare_id': transaction.get('id'),
|
|
185
|
+
'error': -9,
|
|
186
|
+
'error_note': "Transaction cancelled"
|
|
187
|
+
}
|
|
188
|
+
except TransactionNotFound:
|
|
189
|
+
# Transaction doesn't exist, continue with the flow
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
# Handle different actions
|
|
193
|
+
if action == ClickActions.PREPARE:
|
|
194
|
+
# Create transaction
|
|
195
|
+
transaction = self.create_transaction({
|
|
196
|
+
'click_trans_id': click_trans_id,
|
|
197
|
+
'merchant_trans_id': merchant_trans_id,
|
|
198
|
+
'amount': amount,
|
|
199
|
+
'account': account
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
'click_trans_id': click_trans_id,
|
|
204
|
+
'merchant_trans_id': merchant_trans_id,
|
|
205
|
+
'merchant_prepare_id': transaction.get('id'),
|
|
206
|
+
'error': 0,
|
|
207
|
+
'error_note': "Success"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
elif action == ClickActions.COMPLETE:
|
|
211
|
+
# Check if error is negative (payment failed)
|
|
212
|
+
is_successful = error >= 0
|
|
213
|
+
|
|
214
|
+
# Complete transaction
|
|
215
|
+
transaction = self.complete_transaction(click_trans_id, is_successful)
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
'click_trans_id': click_trans_id,
|
|
219
|
+
'merchant_trans_id': merchant_trans_id,
|
|
220
|
+
'merchant_prepare_id': transaction.get('id'),
|
|
221
|
+
'error': 0,
|
|
222
|
+
'error_note': "Success"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
else:
|
|
226
|
+
logger.error(f"Unsupported action: {action}")
|
|
227
|
+
raise UnsupportedMethod(f"Unsupported action: {action}")
|
|
File without changes
|
gateways/payme/cards.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Payme cards operations.
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Dict, Any
|
|
6
|
+
|
|
7
|
+
from paytechuz.core.http import HttpClient
|
|
8
|
+
from paytechuz.core.constants import PaymeEndpoints
|
|
9
|
+
from paytechuz.core.utils import handle_exceptions
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
class PaymeCards:
|
|
14
|
+
"""
|
|
15
|
+
Payme cards operations.
|
|
16
|
+
|
|
17
|
+
This class provides methods for working with cards in the Payme payment system,
|
|
18
|
+
including creating cards, verifying cards, and removing cards.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, http_client: HttpClient, payme_id: str):
|
|
22
|
+
"""
|
|
23
|
+
Initialize the Payme cards component.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
http_client: HTTP client for making requests
|
|
27
|
+
payme_id: Payme merchant ID
|
|
28
|
+
"""
|
|
29
|
+
self.http_client = http_client
|
|
30
|
+
self.payme_id = payme_id
|
|
31
|
+
|
|
32
|
+
@handle_exceptions
|
|
33
|
+
def create(
|
|
34
|
+
self,
|
|
35
|
+
card_number: str,
|
|
36
|
+
expire_date: str,
|
|
37
|
+
save: bool = True,
|
|
38
|
+
**kwargs
|
|
39
|
+
) -> Dict[str, Any]:
|
|
40
|
+
"""
|
|
41
|
+
Create a new card.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
card_number: Card number
|
|
45
|
+
expire_date: Card expiration date in format "MM/YY"
|
|
46
|
+
save: Whether to save the card for future use
|
|
47
|
+
**kwargs: Additional parameters
|
|
48
|
+
- phone: Customer phone number
|
|
49
|
+
- language: Language code (uz, ru, en)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Dict containing card creation response
|
|
53
|
+
"""
|
|
54
|
+
# Extract additional parameters
|
|
55
|
+
phone = kwargs.get('phone')
|
|
56
|
+
language = kwargs.get('language', 'uz')
|
|
57
|
+
|
|
58
|
+
# Prepare request data
|
|
59
|
+
data = {
|
|
60
|
+
"method": PaymeEndpoints.CARDS_CREATE,
|
|
61
|
+
"params": {
|
|
62
|
+
"card": {
|
|
63
|
+
"number": card_number,
|
|
64
|
+
"expire": expire_date
|
|
65
|
+
},
|
|
66
|
+
"save": save,
|
|
67
|
+
"merchant_id": self.payme_id
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Add optional parameters
|
|
72
|
+
if phone:
|
|
73
|
+
data["params"]["phone"] = phone
|
|
74
|
+
|
|
75
|
+
# Add language header
|
|
76
|
+
headers = {"Accept-Language": language}
|
|
77
|
+
|
|
78
|
+
# Make request
|
|
79
|
+
response = self.http_client.post(
|
|
80
|
+
endpoint="",
|
|
81
|
+
json_data=data,
|
|
82
|
+
headers=headers
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return response
|
|
86
|
+
|
|
87
|
+
@handle_exceptions
|
|
88
|
+
def verify(
|
|
89
|
+
self,
|
|
90
|
+
token: str,
|
|
91
|
+
code: str,
|
|
92
|
+
**kwargs
|
|
93
|
+
) -> Dict[str, Any]:
|
|
94
|
+
"""
|
|
95
|
+
Verify a card with the verification code.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
token: Card token received from create method
|
|
99
|
+
code: Verification code sent to the card owner
|
|
100
|
+
**kwargs: Additional parameters
|
|
101
|
+
- language: Language code (uz, ru, en)
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Dict containing card verification response
|
|
105
|
+
"""
|
|
106
|
+
# Extract additional parameters
|
|
107
|
+
language = kwargs.get('language', 'uz')
|
|
108
|
+
|
|
109
|
+
# Prepare request data
|
|
110
|
+
data = {
|
|
111
|
+
"method": PaymeEndpoints.CARDS_VERIFY,
|
|
112
|
+
"params": {
|
|
113
|
+
"token": token,
|
|
114
|
+
"code": code
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Add language header
|
|
119
|
+
headers = {"Accept-Language": language}
|
|
120
|
+
|
|
121
|
+
# Make request
|
|
122
|
+
response = self.http_client.post(
|
|
123
|
+
endpoint="",
|
|
124
|
+
json_data=data,
|
|
125
|
+
headers=headers
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return response
|
|
129
|
+
|
|
130
|
+
@handle_exceptions
|
|
131
|
+
def check(self, token: str) -> Dict[str, Any]:
|
|
132
|
+
"""
|
|
133
|
+
Check if a card exists and is active.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
token: Card token
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Dict containing card check response
|
|
140
|
+
"""
|
|
141
|
+
# Prepare request data
|
|
142
|
+
data = {
|
|
143
|
+
"method": PaymeEndpoints.CARDS_CHECK,
|
|
144
|
+
"params": {
|
|
145
|
+
"token": token
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Make request
|
|
150
|
+
response = self.http_client.post(
|
|
151
|
+
endpoint="",
|
|
152
|
+
json_data=data
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return response
|
|
156
|
+
|
|
157
|
+
@handle_exceptions
|
|
158
|
+
def remove(self, token: str) -> Dict[str, Any]:
|
|
159
|
+
"""
|
|
160
|
+
Remove a card.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
token: Card token
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Dict containing card removal response
|
|
167
|
+
"""
|
|
168
|
+
# Prepare request data
|
|
169
|
+
data = {
|
|
170
|
+
"method": PaymeEndpoints.CARDS_REMOVE,
|
|
171
|
+
"params": {
|
|
172
|
+
"token": token
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Make request
|
|
177
|
+
response = self.http_client.post(
|
|
178
|
+
endpoint="",
|
|
179
|
+
json_data=data
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return response
|
|
183
|
+
|
|
184
|
+
@handle_exceptions
|
|
185
|
+
def get_verify_code(
|
|
186
|
+
self,
|
|
187
|
+
token: str,
|
|
188
|
+
**kwargs
|
|
189
|
+
) -> Dict[str, Any]:
|
|
190
|
+
"""
|
|
191
|
+
Get a new verification code for a card.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
token: Card token
|
|
195
|
+
**kwargs: Additional parameters
|
|
196
|
+
- language: Language code (uz, ru, en)
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Dict containing verification code response
|
|
200
|
+
"""
|
|
201
|
+
# Extract additional parameters
|
|
202
|
+
language = kwargs.get('language', 'uz')
|
|
203
|
+
|
|
204
|
+
# Prepare request data
|
|
205
|
+
data = {
|
|
206
|
+
"method": PaymeEndpoints.CARDS_GET_VERIFY_CODE,
|
|
207
|
+
"params": {
|
|
208
|
+
"token": token
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# Add language header
|
|
213
|
+
headers = {"Accept-Language": language}
|
|
214
|
+
|
|
215
|
+
# Make request
|
|
216
|
+
response = self.http_client.post(
|
|
217
|
+
endpoint="",
|
|
218
|
+
json_data=data,
|
|
219
|
+
headers=headers
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return response
|
gateways/payme/client.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Payme payment gateway client.
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Dict, Any, Optional, Union
|
|
6
|
+
|
|
7
|
+
from paytechuz.core.base import BasePaymentGateway
|
|
8
|
+
from paytechuz.core.http import HttpClient
|
|
9
|
+
from paytechuz.core.constants import PaymeNetworks
|
|
10
|
+
from paytechuz.core.utils import format_amount, handle_exceptions
|
|
11
|
+
from paytechuz.gateways.payme.cards import PaymeCards
|
|
12
|
+
from paytechuz.gateways.payme.receipts import PaymeReceipts
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
class PaymeGateway(BasePaymentGateway):
|
|
17
|
+
"""
|
|
18
|
+
Payme payment gateway implementation.
|
|
19
|
+
|
|
20
|
+
This class provides methods for interacting with the Payme payment gateway,
|
|
21
|
+
including creating payments, checking payment status, and canceling payments.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
payme_id: str,
|
|
27
|
+
payme_key: Optional[str] = None,
|
|
28
|
+
fallback_id: Optional[str] = None,
|
|
29
|
+
is_test_mode: bool = False
|
|
30
|
+
):
|
|
31
|
+
"""
|
|
32
|
+
Initialize the Payme gateway.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
payme_id: Payme merchant ID
|
|
36
|
+
payme_key: Payme merchant key for authentication
|
|
37
|
+
fallback_id: Fallback merchant ID
|
|
38
|
+
is_test_mode: Whether to use the test environment
|
|
39
|
+
"""
|
|
40
|
+
super().__init__(is_test_mode)
|
|
41
|
+
self.payme_id = payme_id
|
|
42
|
+
self.payme_key = payme_key
|
|
43
|
+
self.fallback_id = fallback_id
|
|
44
|
+
|
|
45
|
+
# Set the API URL based on the environment
|
|
46
|
+
url = PaymeNetworks.TEST_NET if is_test_mode else PaymeNetworks.PROD_NET
|
|
47
|
+
|
|
48
|
+
# Initialize HTTP client
|
|
49
|
+
self.http_client = HttpClient(base_url=url)
|
|
50
|
+
|
|
51
|
+
# Initialize components
|
|
52
|
+
self.cards = PaymeCards(http_client=self.http_client, payme_id=payme_id)
|
|
53
|
+
self.receipts = PaymeReceipts(
|
|
54
|
+
http_client=self.http_client,
|
|
55
|
+
payme_id=payme_id,
|
|
56
|
+
payme_key=payme_key
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@handle_exceptions
|
|
60
|
+
def create_payment(
|
|
61
|
+
self,
|
|
62
|
+
amount: Union[int, float, str],
|
|
63
|
+
account_id: Union[int, str],
|
|
64
|
+
**kwargs
|
|
65
|
+
) -> Dict[str, Any]:
|
|
66
|
+
"""
|
|
67
|
+
Create a payment using Payme receipts.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
amount: The payment amount in som
|
|
71
|
+
account_id: The account ID or order ID
|
|
72
|
+
**kwargs: Additional parameters for the payment
|
|
73
|
+
- description: Payment description
|
|
74
|
+
- detail: Payment details
|
|
75
|
+
- callback_url: URL to redirect after payment
|
|
76
|
+
- return_url: URL to return after payment
|
|
77
|
+
- phone: Customer phone number
|
|
78
|
+
- email: Customer email
|
|
79
|
+
- language: Language code (uz, ru, en)
|
|
80
|
+
- expire_minutes: Payment expiration time in minutes
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Dict containing payment details including transaction ID and payment URL
|
|
84
|
+
"""
|
|
85
|
+
# Format amount to tiyin (1 som = 100 tiyin)
|
|
86
|
+
amount_tiyin = format_amount(amount)
|
|
87
|
+
|
|
88
|
+
# Extract additional parameters
|
|
89
|
+
description = kwargs.get('description', f'Payment for account {account_id}')
|
|
90
|
+
detail = kwargs.get('detail', {})
|
|
91
|
+
callback_url = kwargs.get('callback_url')
|
|
92
|
+
return_url = kwargs.get('return_url')
|
|
93
|
+
phone = kwargs.get('phone')
|
|
94
|
+
email = kwargs.get('email')
|
|
95
|
+
language = kwargs.get('language', 'uz')
|
|
96
|
+
expire_minutes = kwargs.get('expire_minutes', 60) # Default 1 hour
|
|
97
|
+
|
|
98
|
+
# Check if we have a merchant key
|
|
99
|
+
if self.payme_key:
|
|
100
|
+
# Create receipt using the API
|
|
101
|
+
receipt_data = self.receipts.create(
|
|
102
|
+
amount=amount_tiyin,
|
|
103
|
+
account={"account_id": str(account_id)},
|
|
104
|
+
description=description,
|
|
105
|
+
detail=detail,
|
|
106
|
+
callback_url=callback_url,
|
|
107
|
+
return_url=return_url,
|
|
108
|
+
phone=phone,
|
|
109
|
+
email=email,
|
|
110
|
+
language=language,
|
|
111
|
+
expire_minutes=expire_minutes
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Extract receipt ID and payment URL
|
|
115
|
+
receipt_id = receipt_data.get('receipt', {}).get('_id')
|
|
116
|
+
payment_url = receipt_data.get('receipt', {}).get('pay_url')
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
'transaction_id': receipt_id,
|
|
120
|
+
'payment_url': payment_url,
|
|
121
|
+
'amount': amount,
|
|
122
|
+
'account_id': account_id,
|
|
123
|
+
'status': 'created',
|
|
124
|
+
'raw_response': receipt_data
|
|
125
|
+
}
|
|
126
|
+
else:
|
|
127
|
+
# Generate a payment URL using payme-pkg style
|
|
128
|
+
# This is a fallback method that doesn't require authentication
|
|
129
|
+
import base64
|
|
130
|
+
from paytechuz.core.utils import generate_id
|
|
131
|
+
|
|
132
|
+
# Generate a unique transaction ID
|
|
133
|
+
transaction_id = generate_id("payme")
|
|
134
|
+
|
|
135
|
+
# Format amount to the smallest currency unit (tiyin)
|
|
136
|
+
# amount_tiyin is already in tiyin format
|
|
137
|
+
|
|
138
|
+
# Build the payment parameters string
|
|
139
|
+
# Format: m=merchant_id;ac.field=value;a=amount;c=callback_url
|
|
140
|
+
params_str = f"m={self.payme_id};ac.id={account_id};a={amount_tiyin}"
|
|
141
|
+
|
|
142
|
+
# Add callback URL if provided (this is used for return URL in payme-pkg)
|
|
143
|
+
if return_url:
|
|
144
|
+
params_str += f";c={return_url}"
|
|
145
|
+
|
|
146
|
+
# Encode the parameters string to base64
|
|
147
|
+
encoded_params = base64.b64encode(params_str.encode("utf-8")).decode("utf-8")
|
|
148
|
+
|
|
149
|
+
# Build the payment URL
|
|
150
|
+
if self.is_test_mode:
|
|
151
|
+
payment_url = f"https://test.paycom.uz/{encoded_params}"
|
|
152
|
+
else:
|
|
153
|
+
payment_url = f"https://checkout.paycom.uz/{encoded_params}"
|
|
154
|
+
|
|
155
|
+
# Print the parameters for debugging
|
|
156
|
+
print("Payme payment parameters:")
|
|
157
|
+
print(f"Parameters string: {params_str}")
|
|
158
|
+
print(f"Encoded parameters: {encoded_params}")
|
|
159
|
+
print(f"Payment URL: {payment_url}")
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
'transaction_id': transaction_id,
|
|
163
|
+
'payment_url': payment_url,
|
|
164
|
+
'amount': amount,
|
|
165
|
+
'account_id': account_id,
|
|
166
|
+
'status': 'created',
|
|
167
|
+
'raw_response': {}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@handle_exceptions
|
|
171
|
+
def check_payment(self, transaction_id: str) -> Dict[str, Any]:
|
|
172
|
+
"""
|
|
173
|
+
Check payment status using Payme receipts.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
transaction_id: The receipt ID to check
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Dict containing payment status and details
|
|
180
|
+
"""
|
|
181
|
+
receipt_data = self.receipts.check(receipt_id=transaction_id)
|
|
182
|
+
|
|
183
|
+
# Extract receipt status
|
|
184
|
+
receipt = receipt_data.get('receipt', {})
|
|
185
|
+
status = receipt.get('state')
|
|
186
|
+
|
|
187
|
+
# Map Payme status to our status
|
|
188
|
+
status_mapping = {
|
|
189
|
+
0: 'created',
|
|
190
|
+
1: 'waiting',
|
|
191
|
+
2: 'paid',
|
|
192
|
+
3: 'cancelled',
|
|
193
|
+
4: 'refunded'
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
mapped_status = status_mapping.get(status, 'unknown')
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
'transaction_id': transaction_id,
|
|
200
|
+
'status': mapped_status,
|
|
201
|
+
'amount': receipt.get('amount') / 100, # Convert from tiyin to som
|
|
202
|
+
'paid_at': receipt.get('pay_time'),
|
|
203
|
+
'created_at': receipt.get('create_time'),
|
|
204
|
+
'cancelled_at': receipt.get('cancel_time'),
|
|
205
|
+
'raw_response': receipt_data
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@handle_exceptions
|
|
209
|
+
def cancel_payment(
|
|
210
|
+
self,
|
|
211
|
+
transaction_id: str,
|
|
212
|
+
reason: Optional[str] = None
|
|
213
|
+
) -> Dict[str, Any]:
|
|
214
|
+
"""
|
|
215
|
+
Cancel payment using Payme receipts.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
transaction_id: The receipt ID to cancel
|
|
219
|
+
reason: Optional reason for cancellation
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Dict containing cancellation status and details
|
|
223
|
+
"""
|
|
224
|
+
receipt_data = self.receipts.cancel(
|
|
225
|
+
receipt_id=transaction_id,
|
|
226
|
+
reason=reason or "Cancelled by merchant"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Extract receipt status
|
|
230
|
+
receipt = receipt_data.get('receipt', {})
|
|
231
|
+
status = receipt.get('state')
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
'transaction_id': transaction_id,
|
|
235
|
+
'status': 'cancelled' if status == 3 else 'unknown',
|
|
236
|
+
'cancelled_at': receipt.get('cancel_time'),
|
|
237
|
+
'raw_response': receipt_data
|
|
238
|
+
}
|