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.

gateways/click/webhook.py DELETED
@@ -1,227 +0,0 @@
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 DELETED
@@ -1,222 +0,0 @@
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 DELETED
@@ -1,238 +0,0 @@
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
- }