paytechuz 0.2.19__py3-none-any.whl → 0.2.20__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 +97 -0
- core/constants.py +68 -0
- core/exceptions.py +190 -0
- core/http.py +268 -0
- core/payme/errors.py +25 -0
- core/utils.py +192 -0
- gateways/__init__.py +0 -0
- gateways/click/__init__.py +0 -0
- gateways/click/client.py +199 -0
- gateways/click/merchant.py +265 -0
- gateways/click/webhook.py +227 -0
- gateways/payme/__init__.py +0 -0
- gateways/payme/cards.py +222 -0
- gateways/payme/client.py +262 -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 +102 -0
- integrations/django/webhooks.py +884 -0
- integrations/fastapi/__init__.py +21 -0
- integrations/fastapi/models.py +183 -0
- integrations/fastapi/routes.py +1038 -0
- integrations/fastapi/schemas.py +116 -0
- {paytechuz-0.2.19.dist-info → paytechuz-0.2.20.dist-info}/METADATA +33 -6
- {paytechuz-0.2.19.dist-info → paytechuz-0.2.20.dist-info}/RECORD +35 -4
- {paytechuz-0.2.19.dist-info → paytechuz-0.2.20.dist-info}/WHEEL +1 -1
- paytechuz-0.2.20.dist-info/top_level.txt +4 -0
- paytechuz-0.2.19.dist-info/top_level.txt +0 -1
|
@@ -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,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Payme payment gateway client.
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Dict, Any, Optional, Union
|
|
6
|
+
import base64
|
|
7
|
+
|
|
8
|
+
from paytechuz.core.base import BasePaymentGateway
|
|
9
|
+
from paytechuz.core.http import HttpClient
|
|
10
|
+
from paytechuz.core.constants import PaymeNetworks
|
|
11
|
+
from paytechuz.core.utils import format_amount, handle_exceptions
|
|
12
|
+
from paytechuz.gateways.payme.cards import PaymeCards
|
|
13
|
+
from paytechuz.gateways.payme.receipts import PaymeReceipts
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PaymeGateway(BasePaymentGateway):
|
|
19
|
+
"""
|
|
20
|
+
Payme payment gateway implementation.
|
|
21
|
+
|
|
22
|
+
This class provides methods for interacting with the Payme payment gateway,
|
|
23
|
+
including creating payments, checking payment status, and canceling payments.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
payme_id: str,
|
|
29
|
+
payme_key: Optional[str] = None,
|
|
30
|
+
fallback_id: Optional[str] = None,
|
|
31
|
+
is_test_mode: bool = False
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Initialize the Payme gateway.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
payme_id: Payme merchant ID
|
|
38
|
+
payme_key: Payme merchant key for authentication
|
|
39
|
+
fallback_id: Fallback merchant ID
|
|
40
|
+
is_test_mode: Whether to use the test environment
|
|
41
|
+
"""
|
|
42
|
+
super().__init__(is_test_mode)
|
|
43
|
+
self.payme_id = payme_id
|
|
44
|
+
self.payme_key = payme_key
|
|
45
|
+
self.fallback_id = fallback_id
|
|
46
|
+
|
|
47
|
+
# Set the API URL based on the environment
|
|
48
|
+
url = PaymeNetworks.TEST_NET if is_test_mode else PaymeNetworks.PROD_NET
|
|
49
|
+
|
|
50
|
+
# Initialize HTTP client
|
|
51
|
+
self.http_client = HttpClient(base_url=url)
|
|
52
|
+
|
|
53
|
+
# Initialize components
|
|
54
|
+
self.cards = PaymeCards(http_client=self.http_client, payme_id=payme_id)
|
|
55
|
+
self.receipts = PaymeReceipts(
|
|
56
|
+
http_client=self.http_client,
|
|
57
|
+
payme_id=payme_id,
|
|
58
|
+
payme_key=payme_key
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def generate_pay_link(
|
|
62
|
+
self,
|
|
63
|
+
id: Union[int, str],
|
|
64
|
+
amount: Union[int, float, str],
|
|
65
|
+
return_url: str,
|
|
66
|
+
account_field_name: str = "order_id"
|
|
67
|
+
) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Generate a payment link for a specific order.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
id : Union[int, str]
|
|
74
|
+
Unique identifier for the account/order.
|
|
75
|
+
amount : Union[int, float, str]
|
|
76
|
+
Payment amount in som.
|
|
77
|
+
return_url : str
|
|
78
|
+
URL to redirect after payment completion.
|
|
79
|
+
account_field_name : str, optional
|
|
80
|
+
Field name for account identifier (default: "order_id").
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
str
|
|
85
|
+
Payme checkout URL with encoded parameters.
|
|
86
|
+
|
|
87
|
+
References
|
|
88
|
+
----------
|
|
89
|
+
https://developer.help.paycom.uz/initsializatsiya-platezhey/
|
|
90
|
+
"""
|
|
91
|
+
# Convert amount to tiyin (1 som = 100 tiyin)
|
|
92
|
+
amount_tiyin = int(float(amount) * 100)
|
|
93
|
+
|
|
94
|
+
# Build parameters
|
|
95
|
+
params = (
|
|
96
|
+
f'm={self.payme_id};'
|
|
97
|
+
f'ac.{account_field_name}={id};'
|
|
98
|
+
f'a={amount_tiyin};'
|
|
99
|
+
f'c={return_url}'
|
|
100
|
+
)
|
|
101
|
+
encoded_params = base64.b64encode(params.encode("utf-8")).decode("utf-8")
|
|
102
|
+
|
|
103
|
+
# Return URL based on environment
|
|
104
|
+
base_url = "https://test.paycom.uz" if self.is_test_mode else "https://checkout.paycom.uz"
|
|
105
|
+
return f"{base_url}/{encoded_params}"
|
|
106
|
+
|
|
107
|
+
async def generate_pay_link_async(
|
|
108
|
+
self,
|
|
109
|
+
id: Union[int, str],
|
|
110
|
+
amount: Union[int, float, str],
|
|
111
|
+
return_url: str,
|
|
112
|
+
account_field_name: str = "order_id"
|
|
113
|
+
) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Async version of generate_pay_link.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
id : Union[int, str]
|
|
120
|
+
Unique identifier for the account/order.
|
|
121
|
+
amount : Union[int, float, str]
|
|
122
|
+
Payment amount in som.
|
|
123
|
+
return_url : str
|
|
124
|
+
URL to redirect after payment completion.
|
|
125
|
+
account_field_name : str, optional
|
|
126
|
+
Field name for account identifier (default: "order_id").
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
str
|
|
131
|
+
Payme checkout URL with encoded parameters.
|
|
132
|
+
"""
|
|
133
|
+
return self.generate_pay_link(
|
|
134
|
+
id=id,
|
|
135
|
+
amount=amount,
|
|
136
|
+
return_url=return_url,
|
|
137
|
+
account_field_name=account_field_name
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@handle_exceptions
|
|
141
|
+
def create_payment(
|
|
142
|
+
self,
|
|
143
|
+
id: Union[int, str],
|
|
144
|
+
amount: Union[int, float, str],
|
|
145
|
+
return_url: str = "",
|
|
146
|
+
account_field_name: str = "order_id"
|
|
147
|
+
) -> str:
|
|
148
|
+
"""
|
|
149
|
+
Create a payment using Payme.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
id: Account or order ID
|
|
153
|
+
amount: Payment amount in som
|
|
154
|
+
return_url: Return URL after payment (default: "")
|
|
155
|
+
account_field_name: Field name for account ID (default: "order_id")
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
str: Payme payment URL
|
|
159
|
+
"""
|
|
160
|
+
return self.generate_pay_link(
|
|
161
|
+
id=id,
|
|
162
|
+
amount=amount,
|
|
163
|
+
return_url=return_url,
|
|
164
|
+
account_field_name=account_field_name
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
@handle_exceptions
|
|
168
|
+
async def create_payment_async(
|
|
169
|
+
self,
|
|
170
|
+
id: Union[int, str],
|
|
171
|
+
amount: Union[int, float, str],
|
|
172
|
+
return_url: str = "",
|
|
173
|
+
account_field_name: str = "order_id"
|
|
174
|
+
) -> str:
|
|
175
|
+
"""
|
|
176
|
+
Async version of create_payment.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
id: Account or order ID
|
|
180
|
+
amount: Payment amount in som
|
|
181
|
+
return_url: Return URL after payment (default: "")
|
|
182
|
+
account_field_name: Field name for account ID (default: "order_id")
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
str: Payme payment URL
|
|
186
|
+
"""
|
|
187
|
+
return await self.generate_pay_link_async(
|
|
188
|
+
id=id,
|
|
189
|
+
amount=amount,
|
|
190
|
+
return_url=return_url,
|
|
191
|
+
account_field_name=account_field_name
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
@handle_exceptions
|
|
195
|
+
def check_payment(self, transaction_id: str) -> Dict[str, Any]:
|
|
196
|
+
"""
|
|
197
|
+
Check payment status using Payme receipts.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
transaction_id: The receipt ID to check
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Dict containing payment status and details
|
|
204
|
+
"""
|
|
205
|
+
receipt_data = self.receipts.check(receipt_id=transaction_id)
|
|
206
|
+
|
|
207
|
+
# Extract receipt status
|
|
208
|
+
receipt = receipt_data.get('receipt', {})
|
|
209
|
+
status = receipt.get('state')
|
|
210
|
+
|
|
211
|
+
# Map Payme status to our status
|
|
212
|
+
status_mapping = {
|
|
213
|
+
0: 'created',
|
|
214
|
+
1: 'waiting',
|
|
215
|
+
2: 'paid',
|
|
216
|
+
3: 'cancelled',
|
|
217
|
+
4: 'refunded'
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
mapped_status = status_mapping.get(status, 'unknown')
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
'transaction_id': transaction_id,
|
|
224
|
+
'status': mapped_status,
|
|
225
|
+
'amount': receipt.get('amount') / 100, # Convert from tiyin to som
|
|
226
|
+
'paid_at': receipt.get('pay_time'),
|
|
227
|
+
'created_at': receipt.get('create_time'),
|
|
228
|
+
'cancelled_at': receipt.get('cancel_time'),
|
|
229
|
+
'raw_response': receipt_data
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@handle_exceptions
|
|
233
|
+
def cancel_payment(
|
|
234
|
+
self,
|
|
235
|
+
transaction_id: str,
|
|
236
|
+
reason: Optional[str] = None
|
|
237
|
+
) -> Dict[str, Any]:
|
|
238
|
+
"""
|
|
239
|
+
Cancel payment using Payme receipts.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
transaction_id: The receipt ID to cancel
|
|
243
|
+
reason: Optional reason for cancellation
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Dict containing cancellation status and details
|
|
247
|
+
"""
|
|
248
|
+
receipt_data = self.receipts.cancel(
|
|
249
|
+
receipt_id=transaction_id,
|
|
250
|
+
reason=reason or "Cancelled by merchant"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Extract receipt status
|
|
254
|
+
receipt = receipt_data.get('receipt', {})
|
|
255
|
+
status = receipt.get('state')
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
'transaction_id': transaction_id,
|
|
259
|
+
'status': 'cancelled' if status == 3 else 'unknown',
|
|
260
|
+
'cancelled_at': receipt.get('cancel_time'),
|
|
261
|
+
'raw_response': receipt_data
|
|
262
|
+
}
|