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/utils.py ADDED
@@ -0,0 +1,192 @@
1
+ """
2
+ Utility functions for payment gateways.
3
+ """
4
+ import base64
5
+ import hashlib
6
+ import hmac
7
+ import json
8
+ import logging
9
+ import time
10
+ from datetime import datetime
11
+ from typing import Dict, Any, Union, Optional
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ def generate_timestamp() -> int:
16
+ """
17
+ Generate a Unix timestamp.
18
+
19
+ Returns:
20
+ Current Unix timestamp in seconds
21
+ """
22
+ return int(time.time())
23
+
24
+
25
+ def generate_id(prefix: str = "") -> str:
26
+ """
27
+ Generate a unique ID.
28
+
29
+ Args:
30
+ prefix: Prefix for the ID
31
+
32
+ Returns:
33
+ Unique ID
34
+ """
35
+ timestamp = generate_timestamp()
36
+ unique_id = f"{timestamp}{hash(timestamp)}"
37
+ if prefix:
38
+ return f"{prefix}_{unique_id}"
39
+ return unique_id
40
+
41
+ def format_amount(amount: Union[int, float, str]) -> int:
42
+ """
43
+ Format amount to integer (in tiyin/kopeyka).
44
+
45
+ Args:
46
+ amount: Amount in som/ruble
47
+
48
+ Returns:
49
+ Amount in tiyin/kopeyka (integer)
50
+ """
51
+ try:
52
+ # Convert to float first to handle string inputs
53
+ float_amount = float(amount)
54
+ # Convert to tiyin/kopeyka (multiply by 100) and round to integer
55
+ return int(float_amount * 100)
56
+ except (ValueError, TypeError) as e:
57
+ logger.error(f"Failed to format amount: {amount}, Error: {e}")
58
+ raise ValueError(f"Invalid amount format: {amount}")
59
+
60
+
61
+ def format_datetime(dt: datetime) -> str:
62
+ """
63
+ Format datetime to ISO 8601 format.
64
+
65
+ Args:
66
+ dt: Datetime object
67
+
68
+ Returns:
69
+ Formatted datetime string
70
+ """
71
+ return dt.strftime("%Y-%m-%dT%H:%M:%S%z")
72
+
73
+ def datetime_to_timestamp(dt: datetime) -> int:
74
+ """
75
+ Convert datetime to Unix timestamp.
76
+
77
+ Args:
78
+ dt: Datetime object
79
+
80
+ Returns:
81
+ Unix timestamp in seconds
82
+ """
83
+ return int(dt.timestamp())
84
+
85
+
86
+ def timestamp_to_datetime(timestamp: int) -> datetime:
87
+ """
88
+ Convert Unix timestamp to datetime.
89
+
90
+ Args:
91
+ timestamp: Unix timestamp in seconds
92
+
93
+ Returns:
94
+ Datetime object
95
+ """
96
+ return datetime.fromtimestamp(timestamp)
97
+
98
+ def generate_hmac_signature(
99
+ data: Union[str, Dict[str, Any], bytes],
100
+ secret_key: str,
101
+ algorithm: str = "sha256"
102
+ ) -> str:
103
+ """
104
+ Generate HMAC signature.
105
+
106
+ Args:
107
+ data: Data to sign
108
+ secret_key: Secret key for signing
109
+ algorithm: Hash algorithm to use
110
+
111
+ Returns:
112
+ HMAC signature as hexadecimal string
113
+ """
114
+ if isinstance(data, dict):
115
+ data = json.dumps(data, separators=(',', ':'))
116
+
117
+ if isinstance(data, str):
118
+ data = data.encode('utf-8')
119
+
120
+ key = secret_key.encode('utf-8')
121
+
122
+ if algorithm.lower() == "sha256":
123
+ signature = hmac.new(key, data, hashlib.sha256).hexdigest()
124
+ elif algorithm.lower() == "sha512":
125
+ signature = hmac.new(key, data, hashlib.sha512).hexdigest()
126
+ elif algorithm.lower() == "md5":
127
+ signature = hmac.new(key, data, hashlib.md5).hexdigest()
128
+ else:
129
+ raise ValueError(f"Unsupported algorithm: {algorithm}")
130
+
131
+ return signature
132
+
133
+
134
+ def generate_basic_auth(username: str, password: str) -> str:
135
+ """
136
+ Generate Basic Authentication header value.
137
+
138
+ Args:
139
+ username: Username
140
+ password: Password
141
+
142
+ Returns:
143
+ Basic Authentication header value
144
+ """
145
+ auth_str = f"{username}:{password}"
146
+ auth_bytes = auth_str.encode('utf-8')
147
+ encoded = base64.b64encode(auth_bytes).decode('utf-8')
148
+ return f"Basic {encoded}"
149
+
150
+ def handle_exceptions(func):
151
+ """
152
+ Decorator to handle exceptions and convert them to payment exceptions.
153
+
154
+ Args:
155
+ func: Function to decorate
156
+
157
+ Returns:
158
+ Decorated function
159
+ """
160
+ from paytechuz.core.exceptions import (
161
+ InternalServiceError,
162
+ exception_whitelist
163
+ )
164
+
165
+ def wrapper(*args, **kwargs):
166
+ try:
167
+ return func(*args, **kwargs)
168
+ except exception_whitelist as exc:
169
+ # No need to wrap exceptions that are already payment exceptions
170
+ raise exc
171
+ except Exception as exc:
172
+ logger.exception(f"Unexpected error in {func.__name__}: {exc}")
173
+ raise InternalServiceError(str(exc))
174
+
175
+ return wrapper
176
+
177
+
178
+ def validate_required_fields(data: Dict[str, Any], required_fields: list) -> Optional[str]:
179
+ """
180
+ Validate that all required fields are present in the data.
181
+
182
+ Args:
183
+ data: Data to validate
184
+ required_fields: List of required field names
185
+
186
+ Returns:
187
+ Error message if validation fails, None otherwise
188
+ """
189
+ missing_fields = [field for field in required_fields if field not in data or data[field] is None]
190
+ if missing_fields:
191
+ return f"Missing required fields: {', '.join(missing_fields)}"
192
+ return None
gateways/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,199 @@
1
+ """
2
+ Click 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 ClickNetworks
10
+ from paytechuz.core.utils import handle_exceptions
11
+ from paytechuz.gateways.click.merchant import ClickMerchantApi
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ClickGateway(BasePaymentGateway):
17
+ """
18
+ Click payment gateway implementation.
19
+
20
+ This class provides methods for interacting with the Click payment gateway,
21
+ including creating payments, checking payment status, and canceling payments.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ service_id: str,
27
+ merchant_id: str,
28
+ merchant_user_id: Optional[str] = None,
29
+ secret_key: Optional[str] = None,
30
+ is_test_mode: bool = False
31
+ ):
32
+ """
33
+ Initialize the Click gateway.
34
+
35
+ Args:
36
+ service_id: Click service ID
37
+ merchant_id: Click merchant ID
38
+ merchant_user_id: Click merchant user ID
39
+ secret_key: Secret key for authentication
40
+ is_test_mode: Whether to use the test environment
41
+ """
42
+ super().__init__(is_test_mode)
43
+ self.service_id = service_id
44
+ self.merchant_id = merchant_id
45
+ self.merchant_user_id = merchant_user_id
46
+ self.secret_key = secret_key
47
+
48
+ # Set the API URL based on the environment
49
+ url = ClickNetworks.TEST_NET if is_test_mode else ClickNetworks.PROD_NET
50
+
51
+ # Initialize HTTP client
52
+ self.http_client = HttpClient(base_url=url)
53
+
54
+ # Initialize merchant API
55
+ self.merchant_api = ClickMerchantApi(
56
+ http_client=self.http_client,
57
+ service_id=service_id,
58
+ merchant_user_id=merchant_user_id,
59
+ secret_key=secret_key
60
+ )
61
+
62
+ @handle_exceptions
63
+ def create_payment(
64
+ self,
65
+ id: Union[int, str],
66
+ amount: Union[int, float, str],
67
+ **kwargs
68
+ ) -> str:
69
+ """
70
+ Create a payment using Click.
71
+
72
+ Args:
73
+ id: The account ID or order ID
74
+ amount: The payment amount in som
75
+ **kwargs: Additional parameters for the payment
76
+ - description: Payment description
77
+ - return_url: URL to return after payment
78
+ - callback_url: URL for payment notifications
79
+ - language: Language code (uz, ru, en)
80
+ - phone: Customer phone number
81
+ - email: Customer email
82
+
83
+ Returns:
84
+ Payment URL string for redirecting the user to Click payment page
85
+ """
86
+ # Format amount for URL (no need to convert to tiyin for URL)
87
+
88
+ # Extract additional parameters
89
+ description = kwargs.get('description', f'Payment for account {id}')
90
+ return_url = kwargs.get('return_url')
91
+ callback_url = kwargs.get('callback_url')
92
+ # These parameters are not used in the URL but are available in the API
93
+ # language = kwargs.get('language', 'uz')
94
+ # phone = kwargs.get('phone')
95
+ # email = kwargs.get('email')
96
+
97
+ # Create payment URL
98
+ payment_url = "https://my.click.uz/services/pay"
99
+ payment_url += f"?service_id={self.service_id}"
100
+ payment_url += f"&merchant_id={self.merchant_id}"
101
+ payment_url += f"&amount={amount}"
102
+ payment_url += f"&transaction_param={id}"
103
+
104
+ if return_url:
105
+ payment_url += f"&return_url={return_url}"
106
+
107
+ if callback_url:
108
+ payment_url += f"&callback_url={callback_url}"
109
+
110
+ if description:
111
+ payment_url += f"&description={description}"
112
+
113
+ if self.merchant_user_id:
114
+ payment_url += f"&merchant_user_id={self.merchant_user_id}"
115
+
116
+ # Return the payment URL directly
117
+ return payment_url
118
+
119
+ @handle_exceptions
120
+ def check_payment(self, transaction_id: str) -> Dict[str, Any]:
121
+ """
122
+ Check payment status using Click merchant API.
123
+
124
+ Args:
125
+ transaction_id: The transaction ID to check
126
+
127
+ Returns:
128
+ Dict containing payment status and details
129
+ """
130
+ # Extract account_id from transaction_id
131
+ # Format: click_account_id_amount
132
+ parts = transaction_id.split('_')
133
+ if len(parts) < 3 or parts[0] != 'click':
134
+ raise ValueError(
135
+ f"Invalid transaction ID format: {transaction_id}"
136
+ )
137
+
138
+ account_id = parts[1]
139
+
140
+ # Check payment status using merchant API
141
+ payment_data = self.merchant_api.check_payment(account_id)
142
+
143
+ # Extract payment status
144
+ status = payment_data.get('status')
145
+
146
+ # Map Click status to our status
147
+ status_mapping = {
148
+ 'success': 'paid',
149
+ 'processing': 'waiting',
150
+ 'failed': 'failed',
151
+ 'cancelled': 'cancelled'
152
+ }
153
+
154
+ mapped_status = status_mapping.get(status, 'unknown')
155
+
156
+ return {
157
+ 'transaction_id': transaction_id,
158
+ 'status': mapped_status,
159
+ 'amount': payment_data.get('amount'),
160
+ 'paid_at': payment_data.get('paid_at'),
161
+ 'created_at': payment_data.get('created_at'),
162
+ 'raw_response': payment_data
163
+ }
164
+
165
+ @handle_exceptions
166
+ def cancel_payment(
167
+ self,
168
+ transaction_id: str,
169
+ reason: Optional[str] = None
170
+ ) -> Dict[str, Any]:
171
+ """
172
+ Cancel payment using Click merchant API.
173
+
174
+ Args:
175
+ transaction_id: The transaction ID to cancel
176
+ reason: Optional reason for cancellation
177
+
178
+ Returns:
179
+ Dict containing cancellation status and details
180
+ """
181
+ # Extract account_id from transaction_id
182
+ # Format: click_account_id_amount
183
+ parts = transaction_id.split('_')
184
+ if len(parts) < 3 or parts[0] != 'click':
185
+ raise ValueError(
186
+ f"Invalid transaction ID format: {transaction_id}"
187
+ )
188
+
189
+ account_id = parts[1]
190
+
191
+ # Cancel payment using merchant API
192
+ cancel_data = self.merchant_api.cancel_payment(account_id, reason)
193
+
194
+ return {
195
+ 'transaction_id': transaction_id,
196
+ 'status': 'cancelled',
197
+ 'cancelled_at': cancel_data.get('cancelled_at'),
198
+ 'raw_response': cancel_data
199
+ }
@@ -0,0 +1,265 @@
1
+ """
2
+ Click merchant API operations.
3
+ """
4
+ import hashlib
5
+ import logging
6
+ from typing import Dict, Any, Optional, Union
7
+
8
+ from paytechuz.core.http import HttpClient
9
+ from paytechuz.core.constants import ClickEndpoints
10
+ from paytechuz.core.utils import handle_exceptions, generate_timestamp
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class ClickMerchantApi:
16
+ """
17
+ Click merchant API operations.
18
+
19
+ This class provides methods for interacting with the Click merchant API,
20
+ including checking payment status and canceling payments.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ http_client: HttpClient,
26
+ service_id: str,
27
+ merchant_user_id: Optional[str] = None,
28
+ secret_key: Optional[str] = None
29
+ ):
30
+ """
31
+ Initialize the Click merchant API.
32
+
33
+ Args:
34
+ http_client: HTTP client for making requests
35
+ service_id: Click service ID
36
+ merchant_user_id: Click merchant user ID
37
+ secret_key: Secret key for authentication
38
+ """
39
+ self.http_client = http_client
40
+ self.service_id = service_id
41
+ self.merchant_user_id = merchant_user_id
42
+ self.secret_key = secret_key
43
+
44
+ def _generate_signature(self, data: Dict[str, Any]) -> str:
45
+ """
46
+ Generate signature for Click API requests.
47
+
48
+ Args:
49
+ data: Request data
50
+
51
+ Returns:
52
+ Signature string
53
+ """
54
+ if not self.secret_key:
55
+ return ""
56
+
57
+ # Sort keys alphabetically
58
+ sorted_data = {k: data[k] for k in sorted(data.keys())}
59
+
60
+ # Create string to sign
61
+ sign_string = ""
62
+ for key, value in sorted_data.items():
63
+ if key != "sign":
64
+ sign_string += str(value)
65
+
66
+ # Add secret key
67
+ sign_string += self.secret_key
68
+
69
+ # Generate signature
70
+ return hashlib.md5(sign_string.encode('utf-8')).hexdigest()
71
+
72
+ @handle_exceptions
73
+ def check_payment(self, id: Union[int, str]) -> Dict[str, Any]:
74
+ """
75
+ Check payment status.
76
+
77
+ Args:
78
+ account_id: Account ID or order ID
79
+
80
+ Returns:
81
+ Dict containing payment status and details
82
+ """
83
+ # Prepare request data
84
+ data = {
85
+ "service_id": self.service_id,
86
+ "merchant_transaction_id": str(id),
87
+ "request_id": str(generate_timestamp())
88
+ }
89
+
90
+ # Add signature if secret key is provided
91
+ if self.secret_key:
92
+ data["sign"] = self._generate_signature(data)
93
+
94
+ # Make request
95
+ response = self.http_client.post(
96
+ endpoint=f"{ClickEndpoints.MERCHANT_API}/payment/status",
97
+ json_data=data
98
+ )
99
+
100
+ return response
101
+
102
+ @handle_exceptions
103
+ def cancel_payment(
104
+ self,
105
+ id: Union[int, str],
106
+ reason: Optional[str] = None
107
+ ) -> Dict[str, Any]:
108
+ """
109
+ Cancel payment.
110
+
111
+ Args:
112
+ id: Account ID or order ID
113
+ reason: Optional reason for cancellation
114
+
115
+ Returns:
116
+ Dict containing cancellation status and details
117
+ """
118
+ # Prepare request data
119
+ data = {
120
+ "service_id": self.service_id,
121
+ "merchant_transaction_id": str(id),
122
+ "request_id": str(generate_timestamp())
123
+ }
124
+
125
+ # Add reason if provided
126
+ if reason:
127
+ data["reason"] = reason
128
+
129
+ # Add signature if secret key is provided
130
+ if self.secret_key:
131
+ data["sign"] = self._generate_signature(data)
132
+
133
+ # Make request
134
+ response = self.http_client.post(
135
+ endpoint=f"{ClickEndpoints.MERCHANT_API}/payment/cancel",
136
+ json_data=data
137
+ )
138
+
139
+ return response
140
+
141
+ @handle_exceptions
142
+ def create_invoice(
143
+ self,
144
+ id: Union[int, str],
145
+ amount: Union[int, float],
146
+ **kwargs
147
+ ) -> Dict[str, Any]:
148
+ """
149
+ Create an invoice.
150
+
151
+ Args:
152
+ amount: Payment amount
153
+ id: Account ID or order ID
154
+ **kwargs: Additional parameters
155
+ - description: Payment description
156
+ - phone: Customer phone number
157
+ - email: Customer email
158
+ - expire_time: Invoice expiration time in minutes
159
+
160
+ Returns:
161
+ Dict containing invoice details
162
+ """
163
+ # Extract additional parameters
164
+ description = kwargs.get('description', f'Payment for account {id}')
165
+ phone = kwargs.get('phone')
166
+ email = kwargs.get('email')
167
+ expire_time = kwargs.get('expire_time', 60) # Default 1 hour
168
+
169
+ # Prepare request data
170
+ data = {
171
+ "service_id": self.service_id,
172
+ "amount": float(amount),
173
+ "merchant_transaction_id": str(id),
174
+ "description": description,
175
+ "request_id": str(generate_timestamp()),
176
+ "expire_time": expire_time
177
+ }
178
+
179
+ # Add optional parameters
180
+ if phone:
181
+ data["phone"] = phone
182
+
183
+ if email:
184
+ data["email"] = email
185
+
186
+ # Add signature if secret key is provided
187
+ if self.secret_key:
188
+ data["sign"] = self._generate_signature(data)
189
+
190
+ # Make request
191
+ response = self.http_client.post(
192
+ endpoint=f"{ClickEndpoints.MERCHANT_API}/invoice/create",
193
+ json_data=data
194
+ )
195
+
196
+ return response
197
+
198
+ @handle_exceptions
199
+ def check_invoice(self, invoice_id: str) -> Dict[str, Any]:
200
+ """
201
+ Check invoice status.
202
+
203
+ Args:
204
+ invoice_id: Invoice ID
205
+
206
+ Returns:
207
+ Dict containing invoice status and details
208
+ """
209
+ # Prepare request data
210
+ data = {
211
+ "service_id": self.service_id,
212
+ "invoice_id": invoice_id,
213
+ "request_id": str(generate_timestamp())
214
+ }
215
+
216
+ # Add signature if secret key is provided
217
+ if self.secret_key:
218
+ data["sign"] = self._generate_signature(data)
219
+
220
+ # Make request
221
+ response = self.http_client.post(
222
+ endpoint=f"{ClickEndpoints.MERCHANT_API}/invoice/status",
223
+ json_data=data
224
+ )
225
+
226
+ return response
227
+
228
+ @handle_exceptions
229
+ def cancel_invoice(
230
+ self,
231
+ invoice_id: str,
232
+ reason: Optional[str] = None
233
+ ) -> Dict[str, Any]:
234
+ """
235
+ Cancel invoice.
236
+
237
+ Args:
238
+ invoice_id: Invoice ID
239
+ reason: Optional reason for cancellation
240
+
241
+ Returns:
242
+ Dict containing cancellation status and details
243
+ """
244
+ # Prepare request data
245
+ data = {
246
+ "service_id": self.service_id,
247
+ "invoice_id": invoice_id,
248
+ "request_id": str(generate_timestamp())
249
+ }
250
+
251
+ # Add reason if provided
252
+ if reason:
253
+ data["reason"] = reason
254
+
255
+ # Add signature if secret key is provided
256
+ if self.secret_key:
257
+ data["sign"] = self._generate_signature(data)
258
+
259
+ # Make request
260
+ response = self.http_client.post(
261
+ endpoint=f"{ClickEndpoints.MERCHANT_API}/invoice/cancel",
262
+ json_data=data
263
+ )
264
+
265
+ return response