paytechuz 0.1.0__py3-none-any.whl → 0.1.1__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 DELETED
@@ -1,192 +0,0 @@
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 DELETED
File without changes
File without changes
gateways/click/client.py DELETED
@@ -1,202 +0,0 @@
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 format_amount, handle_exceptions
11
- from paytechuz.gateways.click.merchant import ClickMerchantApi
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
- class ClickGateway(BasePaymentGateway):
16
- """
17
- Click payment gateway implementation.
18
-
19
- This class provides methods for interacting with the Click payment gateway,
20
- including creating payments, checking payment status, and canceling payments.
21
- """
22
-
23
- def __init__(
24
- self,
25
- service_id: str,
26
- merchant_id: str,
27
- merchant_user_id: Optional[str] = None,
28
- secret_key: Optional[str] = None,
29
- is_test_mode: bool = False
30
- ):
31
- """
32
- Initialize the Click gateway.
33
-
34
- Args:
35
- service_id: Click service ID
36
- merchant_id: Click merchant ID
37
- merchant_user_id: Click merchant user ID
38
- secret_key: Secret key for authentication
39
- is_test_mode: Whether to use the test environment
40
- """
41
- super().__init__(is_test_mode)
42
- self.service_id = service_id
43
- self.merchant_id = merchant_id
44
- self.merchant_user_id = merchant_user_id
45
- self.secret_key = secret_key
46
-
47
- # Set the API URL based on the environment
48
- url = ClickNetworks.TEST_NET if is_test_mode else ClickNetworks.PROD_NET
49
-
50
- # Initialize HTTP client
51
- self.http_client = HttpClient(base_url=url)
52
-
53
- # Initialize merchant API
54
- self.merchant_api = ClickMerchantApi(
55
- http_client=self.http_client,
56
- service_id=service_id,
57
- merchant_user_id=merchant_user_id,
58
- secret_key=secret_key
59
- )
60
-
61
- @handle_exceptions
62
- def create_payment(
63
- self,
64
- amount: Union[int, float, str],
65
- account_id: Union[int, str],
66
- **kwargs
67
- ) -> Dict[str, Any]:
68
- """
69
- Create a payment using Click.
70
-
71
- Args:
72
- amount: The payment amount in som
73
- account_id: The account ID or order ID
74
- **kwargs: Additional parameters for the payment
75
- - description: Payment description
76
- - return_url: URL to return after payment
77
- - callback_url: URL for payment notifications
78
- - language: Language code (uz, ru, en)
79
- - phone: Customer phone number
80
- - email: Customer email
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
- 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={account_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"&merchant_user_id={description}"
112
-
113
- # Generate a unique transaction ID
114
- transaction_id = f"click_{account_id}_{int(amount_tiyin)}"
115
-
116
- return {
117
- 'transaction_id': transaction_id,
118
- 'payment_url': payment_url,
119
- 'amount': amount,
120
- 'account_id': account_id,
121
- 'status': 'created',
122
- 'service_id': self.service_id,
123
- 'merchant_id': self.merchant_id
124
- }
125
-
126
- @handle_exceptions
127
- def check_payment(self, transaction_id: str) -> Dict[str, Any]:
128
- """
129
- Check payment status using Click merchant API.
130
-
131
- Args:
132
- transaction_id: The transaction ID to check
133
-
134
- Returns:
135
- Dict containing payment status and details
136
- """
137
- # Extract account_id from transaction_id
138
- # Format: click_account_id_amount
139
- parts = transaction_id.split('_')
140
- if len(parts) < 3 or parts[0] != 'click':
141
- raise ValueError(f"Invalid transaction ID format: {transaction_id}")
142
-
143
- account_id = parts[1]
144
-
145
- # Check payment status using merchant API
146
- payment_data = self.merchant_api.check_payment(account_id)
147
-
148
- # Extract payment status
149
- status = payment_data.get('status')
150
-
151
- # Map Click status to our status
152
- status_mapping = {
153
- 'success': 'paid',
154
- 'processing': 'waiting',
155
- 'failed': 'failed',
156
- 'cancelled': 'cancelled'
157
- }
158
-
159
- mapped_status = status_mapping.get(status, 'unknown')
160
-
161
- return {
162
- 'transaction_id': transaction_id,
163
- 'status': mapped_status,
164
- 'amount': payment_data.get('amount'),
165
- 'paid_at': payment_data.get('paid_at'),
166
- 'created_at': payment_data.get('created_at'),
167
- 'raw_response': payment_data
168
- }
169
-
170
- @handle_exceptions
171
- def cancel_payment(
172
- self,
173
- transaction_id: str,
174
- reason: Optional[str] = None
175
- ) -> Dict[str, Any]:
176
- """
177
- Cancel payment using Click merchant API.
178
-
179
- Args:
180
- transaction_id: The transaction ID to cancel
181
- reason: Optional reason for cancellation
182
-
183
- Returns:
184
- Dict containing cancellation status and details
185
- """
186
- # Extract account_id from transaction_id
187
- # Format: click_account_id_amount
188
- parts = transaction_id.split('_')
189
- if len(parts) < 3 or parts[0] != 'click':
190
- raise ValueError(f"Invalid transaction ID format: {transaction_id}")
191
-
192
- account_id = parts[1]
193
-
194
- # Cancel payment using merchant API
195
- cancel_data = self.merchant_api.cancel_payment(account_id, reason)
196
-
197
- return {
198
- 'transaction_id': transaction_id,
199
- 'status': 'cancelled',
200
- 'cancelled_at': cancel_data.get('cancelled_at'),
201
- 'raw_response': cancel_data
202
- }
@@ -1,264 +0,0 @@
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
- class ClickMerchantApi:
15
- """
16
- Click merchant API operations.
17
-
18
- This class provides methods for interacting with the Click merchant API,
19
- including checking payment status and canceling payments.
20
- """
21
-
22
- def __init__(
23
- self,
24
- http_client: HttpClient,
25
- service_id: str,
26
- merchant_user_id: Optional[str] = None,
27
- secret_key: Optional[str] = None
28
- ):
29
- """
30
- Initialize the Click merchant API.
31
-
32
- Args:
33
- http_client: HTTP client for making requests
34
- service_id: Click service ID
35
- merchant_user_id: Click merchant user ID
36
- secret_key: Secret key for authentication
37
- """
38
- self.http_client = http_client
39
- self.service_id = service_id
40
- self.merchant_user_id = merchant_user_id
41
- self.secret_key = secret_key
42
-
43
- def _generate_signature(self, data: Dict[str, Any]) -> str:
44
- """
45
- Generate signature for Click API requests.
46
-
47
- Args:
48
- data: Request data
49
-
50
- Returns:
51
- Signature string
52
- """
53
- if not self.secret_key:
54
- return ""
55
-
56
- # Sort keys alphabetically
57
- sorted_data = {k: data[k] for k in sorted(data.keys())}
58
-
59
- # Create string to sign
60
- sign_string = ""
61
- for key, value in sorted_data.items():
62
- if key != "sign":
63
- sign_string += str(value)
64
-
65
- # Add secret key
66
- sign_string += self.secret_key
67
-
68
- # Generate signature
69
- return hashlib.md5(sign_string.encode('utf-8')).hexdigest()
70
-
71
- @handle_exceptions
72
- def check_payment(self, account_id: Union[int, str]) -> Dict[str, Any]:
73
- """
74
- Check payment status.
75
-
76
- Args:
77
- account_id: Account ID or order ID
78
-
79
- Returns:
80
- Dict containing payment status and details
81
- """
82
- # Prepare request data
83
- data = {
84
- "service_id": self.service_id,
85
- "merchant_transaction_id": str(account_id),
86
- "request_id": str(generate_timestamp())
87
- }
88
-
89
- # Add signature if secret key is provided
90
- if self.secret_key:
91
- data["sign"] = self._generate_signature(data)
92
-
93
- # Make request
94
- response = self.http_client.post(
95
- endpoint=f"{ClickEndpoints.MERCHANT_API}/payment/status",
96
- json_data=data
97
- )
98
-
99
- return response
100
-
101
- @handle_exceptions
102
- def cancel_payment(
103
- self,
104
- account_id: Union[int, str],
105
- reason: Optional[str] = None
106
- ) -> Dict[str, Any]:
107
- """
108
- Cancel payment.
109
-
110
- Args:
111
- account_id: Account ID or order ID
112
- reason: Optional reason for cancellation
113
-
114
- Returns:
115
- Dict containing cancellation status and details
116
- """
117
- # Prepare request data
118
- data = {
119
- "service_id": self.service_id,
120
- "merchant_transaction_id": str(account_id),
121
- "request_id": str(generate_timestamp())
122
- }
123
-
124
- # Add reason if provided
125
- if reason:
126
- data["reason"] = reason
127
-
128
- # Add signature if secret key is provided
129
- if self.secret_key:
130
- data["sign"] = self._generate_signature(data)
131
-
132
- # Make request
133
- response = self.http_client.post(
134
- endpoint=f"{ClickEndpoints.MERCHANT_API}/payment/cancel",
135
- json_data=data
136
- )
137
-
138
- return response
139
-
140
- @handle_exceptions
141
- def create_invoice(
142
- self,
143
- amount: Union[int, float],
144
- account_id: Union[int, str],
145
- **kwargs
146
- ) -> Dict[str, Any]:
147
- """
148
- Create an invoice.
149
-
150
- Args:
151
- amount: Payment amount
152
- account_id: Account ID or order ID
153
- **kwargs: Additional parameters
154
- - description: Payment description
155
- - phone: Customer phone number
156
- - email: Customer email
157
- - expire_time: Invoice expiration time in minutes
158
-
159
- Returns:
160
- Dict containing invoice details
161
- """
162
- # Extract additional parameters
163
- description = kwargs.get('description', f'Payment for account {account_id}')
164
- phone = kwargs.get('phone')
165
- email = kwargs.get('email')
166
- expire_time = kwargs.get('expire_time', 60) # Default 1 hour
167
-
168
- # Prepare request data
169
- data = {
170
- "service_id": self.service_id,
171
- "amount": float(amount),
172
- "merchant_transaction_id": str(account_id),
173
- "description": description,
174
- "request_id": str(generate_timestamp()),
175
- "expire_time": expire_time
176
- }
177
-
178
- # Add optional parameters
179
- if phone:
180
- data["phone"] = phone
181
-
182
- if email:
183
- data["email"] = email
184
-
185
- # Add signature if secret key is provided
186
- if self.secret_key:
187
- data["sign"] = self._generate_signature(data)
188
-
189
- # Make request
190
- response = self.http_client.post(
191
- endpoint=f"{ClickEndpoints.MERCHANT_API}/invoice/create",
192
- json_data=data
193
- )
194
-
195
- return response
196
-
197
- @handle_exceptions
198
- def check_invoice(self, invoice_id: str) -> Dict[str, Any]:
199
- """
200
- Check invoice status.
201
-
202
- Args:
203
- invoice_id: Invoice ID
204
-
205
- Returns:
206
- Dict containing invoice status and details
207
- """
208
- # Prepare request data
209
- data = {
210
- "service_id": self.service_id,
211
- "invoice_id": invoice_id,
212
- "request_id": str(generate_timestamp())
213
- }
214
-
215
- # Add signature if secret key is provided
216
- if self.secret_key:
217
- data["sign"] = self._generate_signature(data)
218
-
219
- # Make request
220
- response = self.http_client.post(
221
- endpoint=f"{ClickEndpoints.MERCHANT_API}/invoice/status",
222
- json_data=data
223
- )
224
-
225
- return response
226
-
227
- @handle_exceptions
228
- def cancel_invoice(
229
- self,
230
- invoice_id: str,
231
- reason: Optional[str] = None
232
- ) -> Dict[str, Any]:
233
- """
234
- Cancel invoice.
235
-
236
- Args:
237
- invoice_id: Invoice ID
238
- reason: Optional reason for cancellation
239
-
240
- Returns:
241
- Dict containing cancellation status and details
242
- """
243
- # Prepare request data
244
- data = {
245
- "service_id": self.service_id,
246
- "invoice_id": invoice_id,
247
- "request_id": str(generate_timestamp())
248
- }
249
-
250
- # Add reason if provided
251
- if reason:
252
- data["reason"] = reason
253
-
254
- # Add signature if secret key is provided
255
- if self.secret_key:
256
- data["sign"] = self._generate_signature(data)
257
-
258
- # Make request
259
- response = self.http_client.post(
260
- endpoint=f"{ClickEndpoints.MERCHANT_API}/invoice/cancel",
261
- json_data=data
262
- )
263
-
264
- return response