paytechuz 0.2.19__py3-none-any.whl → 0.2.21__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.

@@ -0,0 +1,336 @@
1
+ """
2
+ Payme receipts operations.
3
+ """
4
+ # base64 is used indirectly through generate_basic_auth
5
+ import logging
6
+ from typing import Dict, Any, Optional
7
+
8
+ from paytechuz.core.http import HttpClient
9
+ from paytechuz.core.constants import PaymeEndpoints
10
+ from paytechuz.core.utils import handle_exceptions, generate_basic_auth
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class PaymeReceipts:
15
+ """
16
+ Payme receipts operations.
17
+
18
+ This class provides methods for working with receipts in the Payme payment system,
19
+ including creating receipts, paying receipts, and checking receipt status.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ http_client: HttpClient,
25
+ payme_id: str,
26
+ payme_key: Optional[str] = None
27
+ ):
28
+ """
29
+ Initialize the Payme receipts component.
30
+
31
+ Args:
32
+ http_client: HTTP client for making requests
33
+ payme_id: Payme merchant ID
34
+ payme_key: Payme merchant key for authentication
35
+ """
36
+ self.http_client = http_client
37
+ self.payme_id = payme_id
38
+ self.payme_key = payme_key
39
+
40
+ def _get_auth_headers(self, language: str = 'uz') -> Dict[str, str]:
41
+ """
42
+ Get authentication headers for Payme API.
43
+
44
+ Args:
45
+ language: Language code (uz, ru, en)
46
+
47
+ Returns:
48
+ Dict containing authentication headers
49
+ """
50
+ headers = {"Accept-Language": language}
51
+
52
+ if self.payme_key:
53
+ auth = generate_basic_auth(self.payme_id, self.payme_key)
54
+ headers["Authorization"] = auth
55
+
56
+ return headers
57
+
58
+ @handle_exceptions
59
+ def create(
60
+ self,
61
+ amount: int,
62
+ account: Dict[str, Any],
63
+ **kwargs
64
+ ) -> Dict[str, Any]:
65
+ """
66
+ Create a new receipt.
67
+
68
+ Args:
69
+ amount: Payment amount in tiyin (1 som = 100 tiyin)
70
+ account: Account information (e.g., {"account_id": "12345"})
71
+ **kwargs: Additional parameters
72
+ - description: Payment description
73
+ - detail: Payment details
74
+ - callback_url: URL to redirect after payment
75
+ - return_url: URL to return after payment
76
+ - phone: Customer phone number
77
+ - email: Customer email
78
+ - language: Language code (uz, ru, en)
79
+ - expire_minutes: Payment expiration time in minutes
80
+
81
+ Returns:
82
+ Dict containing receipt creation response
83
+ """
84
+ # Extract additional parameters
85
+ description = kwargs.get('description', 'Payment')
86
+ detail = kwargs.get('detail', {})
87
+ callback_url = kwargs.get('callback_url')
88
+ return_url = kwargs.get('return_url')
89
+ phone = kwargs.get('phone')
90
+ email = kwargs.get('email')
91
+ language = kwargs.get('language', 'uz')
92
+ expire_minutes = kwargs.get('expire_minutes', 60) # Default 1 hour
93
+
94
+ # Prepare request data
95
+ data = {
96
+ "method": PaymeEndpoints.RECEIPTS_CREATE,
97
+ "params": {
98
+ "amount": amount,
99
+ "account": account,
100
+ "description": description,
101
+ "detail": detail,
102
+ "merchant_id": self.payme_id
103
+ }
104
+ }
105
+
106
+ # Add optional parameters
107
+ if callback_url:
108
+ data["params"]["callback_url"] = callback_url
109
+
110
+ if return_url:
111
+ data["params"]["return_url"] = return_url
112
+
113
+ if phone:
114
+ data["params"]["phone"] = phone
115
+
116
+ if email:
117
+ data["params"]["email"] = email
118
+
119
+ if expire_minutes:
120
+ data["params"]["expire_minutes"] = expire_minutes
121
+
122
+ # Get authentication headers
123
+ headers = self._get_auth_headers(language)
124
+
125
+ # Make request
126
+ response = self.http_client.post(
127
+ endpoint="",
128
+ json_data=data,
129
+ headers=headers
130
+ )
131
+
132
+ return response
133
+
134
+ @handle_exceptions
135
+ def pay(
136
+ self,
137
+ receipt_id: str,
138
+ token: str,
139
+ **kwargs
140
+ ) -> Dict[str, Any]:
141
+ """
142
+ Pay a receipt with a card token.
143
+
144
+ Args:
145
+ receipt_id: Receipt ID
146
+ token: Card token
147
+ **kwargs: Additional parameters
148
+ - language: Language code (uz, ru, en)
149
+
150
+ Returns:
151
+ Dict containing receipt payment response
152
+ """
153
+ # Extract additional parameters
154
+ language = kwargs.get('language', 'uz')
155
+
156
+ # Prepare request data
157
+ data = {
158
+ "method": PaymeEndpoints.RECEIPTS_PAY,
159
+ "params": {
160
+ "id": receipt_id,
161
+ "token": token
162
+ }
163
+ }
164
+
165
+ # Get authentication headers
166
+ headers = self._get_auth_headers(language)
167
+
168
+ # Make request
169
+ response = self.http_client.post(
170
+ endpoint="",
171
+ json_data=data,
172
+ headers=headers
173
+ )
174
+
175
+ return response
176
+
177
+ @handle_exceptions
178
+ def send(
179
+ self,
180
+ receipt_id: str,
181
+ phone: str,
182
+ **kwargs
183
+ ) -> Dict[str, Any]:
184
+ """
185
+ Send a receipt to a phone number.
186
+
187
+ Args:
188
+ receipt_id: Receipt ID
189
+ phone: Phone number
190
+ **kwargs: Additional parameters
191
+ - language: Language code (uz, ru, en)
192
+
193
+ Returns:
194
+ Dict containing receipt sending response
195
+ """
196
+ # Extract additional parameters
197
+ language = kwargs.get('language', 'uz')
198
+
199
+ # Prepare request data
200
+ data = {
201
+ "method": PaymeEndpoints.RECEIPTS_SEND,
202
+ "params": {
203
+ "id": receipt_id,
204
+ "phone": phone
205
+ }
206
+ }
207
+
208
+ # Get authentication headers
209
+ headers = self._get_auth_headers(language)
210
+
211
+ # Make request
212
+ response = self.http_client.post(
213
+ endpoint="",
214
+ json_data=data,
215
+ headers=headers
216
+ )
217
+
218
+ return response
219
+
220
+ @handle_exceptions
221
+ def check(self, receipt_id: str, **kwargs) -> Dict[str, Any]:
222
+ """
223
+ Check receipt status.
224
+
225
+ Args:
226
+ receipt_id: Receipt ID
227
+ **kwargs: Additional parameters
228
+ - language: Language code (uz, ru, en)
229
+
230
+ Returns:
231
+ Dict containing receipt status response
232
+ """
233
+ # Extract additional parameters
234
+ language = kwargs.get('language', 'uz')
235
+
236
+ # Prepare request data
237
+ data = {
238
+ "method": PaymeEndpoints.RECEIPTS_CHECK,
239
+ "params": {
240
+ "id": receipt_id
241
+ }
242
+ }
243
+
244
+ # Get authentication headers
245
+ headers = self._get_auth_headers(language)
246
+
247
+ # Make request
248
+ response = self.http_client.post(
249
+ endpoint="",
250
+ json_data=data,
251
+ headers=headers
252
+ )
253
+
254
+ return response
255
+
256
+ @handle_exceptions
257
+ def cancel(
258
+ self,
259
+ receipt_id: str,
260
+ reason: Optional[str] = None,
261
+ **kwargs
262
+ ) -> Dict[str, Any]:
263
+ """
264
+ Cancel a receipt.
265
+
266
+ Args:
267
+ receipt_id: Receipt ID
268
+ reason: Cancellation reason
269
+ **kwargs: Additional parameters
270
+ - language: Language code (uz, ru, en)
271
+
272
+ Returns:
273
+ Dict containing receipt cancellation response
274
+ """
275
+ # Extract additional parameters
276
+ language = kwargs.get('language', 'uz')
277
+
278
+ # Prepare request data
279
+ data = {
280
+ "method": PaymeEndpoints.RECEIPTS_CANCEL,
281
+ "params": {
282
+ "id": receipt_id
283
+ }
284
+ }
285
+
286
+ # Add reason if provided
287
+ if reason:
288
+ data["params"]["reason"] = reason
289
+
290
+ # Get authentication headers
291
+ headers = self._get_auth_headers(language)
292
+
293
+ # Make request
294
+ response = self.http_client.post(
295
+ endpoint="",
296
+ json_data=data,
297
+ headers=headers
298
+ )
299
+
300
+ return response
301
+
302
+ @handle_exceptions
303
+ def get(self, receipt_id: str, **kwargs) -> Dict[str, Any]:
304
+ """
305
+ Get receipt details.
306
+
307
+ Args:
308
+ receipt_id: Receipt ID
309
+ **kwargs: Additional parameters
310
+ - language: Language code (uz, ru, en)
311
+
312
+ Returns:
313
+ Dict containing receipt details response
314
+ """
315
+ # Extract additional parameters
316
+ language = kwargs.get('language', 'uz')
317
+
318
+ # Prepare request data
319
+ data = {
320
+ "method": PaymeEndpoints.RECEIPTS_GET,
321
+ "params": {
322
+ "id": receipt_id
323
+ }
324
+ }
325
+
326
+ # Get authentication headers
327
+ headers = self._get_auth_headers(language)
328
+
329
+ # Make request
330
+ response = self.http_client.post(
331
+ endpoint="",
332
+ json_data=data,
333
+ headers=headers
334
+ )
335
+
336
+ return response
@@ -0,0 +1,379 @@
1
+ """
2
+ Payme webhook handler.
3
+ """
4
+ import base64
5
+ import binascii
6
+ import logging
7
+ from typing import Dict, Any, Optional, Callable
8
+
9
+ from paytechuz.core.base import BaseWebhookHandler
10
+ from paytechuz.core.constants import TransactionState, PaymeCancelReason
11
+ from paytechuz.core.exceptions import (
12
+ PermissionDenied,
13
+ MethodNotFound,
14
+ TransactionNotFound,
15
+ AccountNotFound,
16
+ InternalServiceError,
17
+ TransactionCancelled
18
+ )
19
+ from paytechuz.core.utils import handle_exceptions
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ class PaymeWebhookHandler(BaseWebhookHandler):
24
+ """
25
+ Payme webhook handler.
26
+
27
+ This class handles webhook requests from the Payme payment system,
28
+ including transaction creation, confirmation, and cancellation.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ merchant_key: str,
34
+ find_transaction_func: Callable[[str], Dict[str, Any]],
35
+ find_account_func: Callable[[Dict[str, Any]], Dict[str, Any]],
36
+ create_transaction_func: Callable[[Dict[str, Any]], Dict[str, Any]],
37
+ perform_transaction_func: Callable[[str], bool],
38
+ cancel_transaction_func: Callable[[str, int], bool],
39
+ get_statement_func: Optional[Callable[[int, int], list]] = None,
40
+ check_perform_func: Optional[Callable[[Dict[str, Any]], bool]] = None,
41
+ set_fiscal_data_func: Optional[
42
+ Callable[[str, Dict[str, Any]], bool]
43
+ ] = None
44
+ ):
45
+ """
46
+ Initialize the Payme webhook handler.
47
+
48
+ Args:
49
+ merchant_key: Payme merchant key for authentication
50
+ find_transaction_func: Function to find a transaction by ID
51
+ find_account_func: Function to find an account by parameters
52
+ create_transaction_func: Function to create a transaction
53
+ perform_transaction_func: Function to perform a transaction
54
+ cancel_transaction_func: Function to cancel a transaction
55
+ get_statement_func: Function to get transaction statement
56
+ check_perform_func: Function to check transaction can be performed
57
+ set_fiscal_data_func: Function to set fiscal data for a transaction
58
+ """
59
+ self.merchant_key = merchant_key
60
+ self.find_transaction = find_transaction_func
61
+ self.find_account = find_account_func
62
+ self.create_transaction = create_transaction_func
63
+ self.perform_transaction = perform_transaction_func
64
+ self.cancel_transaction = cancel_transaction_func
65
+ self.get_statement = get_statement_func
66
+ self.check_perform = check_perform_func
67
+ self.set_fiscal_data = set_fiscal_data_func
68
+
69
+ def _check_auth(self, auth_header: Optional[str]) -> None:
70
+ """
71
+ Check authentication header.
72
+
73
+ Args:
74
+ auth_header: Authentication header
75
+
76
+ Raises:
77
+ PermissionDenied: If authentication fails
78
+ """
79
+ if not auth_header:
80
+ raise PermissionDenied("Missing authentication credentials")
81
+
82
+ try:
83
+ auth_parts = auth_header.split()
84
+ if len(auth_parts) != 2 or auth_parts[0].lower() != 'basic':
85
+ raise PermissionDenied("Invalid authentication format")
86
+
87
+ auth_decoded = base64.b64decode(auth_parts[1]).decode('utf-8')
88
+ _, password = auth_decoded.split(':')
89
+
90
+ if password != self.merchant_key:
91
+ raise PermissionDenied("Invalid merchant key")
92
+ except (binascii.Error, UnicodeDecodeError, ValueError) as e:
93
+ logger.error(f"Authentication error: {e}")
94
+ raise PermissionDenied("Authentication error")
95
+
96
+ @handle_exceptions
97
+ def handle_webhook(
98
+ self, data: Dict[str, Any], auth_header: Optional[str] = None
99
+ ) -> Dict[str, Any]:
100
+ """
101
+ Handle webhook data from Payme.
102
+
103
+ Args:
104
+ data: The webhook data received from Payme
105
+ auth_header: Authentication header
106
+
107
+ Returns:
108
+ Dict containing the response to be sent back to Payme
109
+
110
+ Raises:
111
+ PermissionDenied: If authentication fails
112
+ MethodNotFound: If the requested method is not supported
113
+ """
114
+ # Check authentication
115
+ self._check_auth(auth_header)
116
+
117
+ # Extract method and params
118
+ try:
119
+ method = data.get('method')
120
+ params = data.get('params', {})
121
+ request_id = data.get('id', 0)
122
+ except (KeyError, TypeError) as e:
123
+ logger.error(f"Invalid webhook data: {e}")
124
+ raise InternalServiceError("Invalid webhook data")
125
+
126
+ # Map methods to handler functions
127
+ method_handlers = {
128
+ 'CheckPerformTransaction': self._handle_check_perform,
129
+ 'CreateTransaction': self._handle_create_transaction,
130
+ 'PerformTransaction': self._handle_perform_transaction,
131
+ 'CheckTransaction': self._handle_check_transaction,
132
+ 'CancelTransaction': self._handle_cancel_transaction,
133
+ 'GetStatement': self._handle_get_statement,
134
+ 'SetFiscalData': self._handle_set_fiscal_data,
135
+ }
136
+
137
+ # Call the appropriate handler
138
+ if method in method_handlers:
139
+ result = method_handlers[method](params)
140
+ return {
141
+ 'jsonrpc': '2.0',
142
+ 'id': request_id,
143
+ 'result': result
144
+ }
145
+
146
+ logger.warning(f"Method not found: {method}")
147
+ raise MethodNotFound(f"Method not supported: {method}")
148
+
149
+ def _handle_check_perform(self, params: Dict[str, Any]) -> Dict[str, Any]:
150
+ """
151
+ Handle CheckPerformTransaction method.
152
+
153
+ Args:
154
+ params: Method parameters
155
+
156
+ Returns:
157
+ Dict containing the response
158
+ """
159
+ if not self.check_perform:
160
+ # Default implementation if no custom function is provided
161
+ account = self.find_account(params.get('account', {}))
162
+ if not account:
163
+ raise AccountNotFound("Account not found")
164
+
165
+ return {'allow': True}
166
+
167
+ # Call custom function
168
+ result = self.check_perform(params)
169
+ return {'allow': result}
170
+
171
+ def _handle_create_transaction(
172
+ self, params: Dict[str, Any]
173
+ ) -> Dict[str, Any]:
174
+ """
175
+ Handle CreateTransaction method.
176
+
177
+ Args:
178
+ params: Method parameters
179
+
180
+ Returns:
181
+ Dict containing the response
182
+ """
183
+ transaction_id = params.get('id')
184
+
185
+ # Check if transaction already exists
186
+ try:
187
+ existing_transaction = self.find_transaction(transaction_id)
188
+
189
+ # If transaction exists, return its details
190
+ return {
191
+ 'transaction': existing_transaction['id'],
192
+ 'state': existing_transaction['state'],
193
+ 'create_time': existing_transaction['create_time'],
194
+ }
195
+ except TransactionNotFound:
196
+ # Transaction doesn't exist, create a new one
197
+ pass
198
+
199
+ # Find account
200
+ account = self.find_account(params.get('account', {}))
201
+ if not account:
202
+ raise AccountNotFound("Account not found")
203
+
204
+ # Create transaction
205
+ transaction = self.create_transaction({
206
+ 'id': transaction_id,
207
+ 'account': account,
208
+ 'amount': params.get('amount'),
209
+ 'time': params.get('time'),
210
+ })
211
+
212
+ return {
213
+ 'transaction': transaction['id'],
214
+ 'state': transaction['state'],
215
+ 'create_time': transaction['create_time'],
216
+ }
217
+
218
+ def _handle_perform_transaction(
219
+ self, params: Dict[str, Any]
220
+ ) -> Dict[str, Any]:
221
+ """
222
+ Handle PerformTransaction method.
223
+
224
+ Args:
225
+ params: Method parameters
226
+
227
+ Returns:
228
+ Dict containing the response
229
+ """
230
+ transaction_id = params.get('id')
231
+
232
+ # Find transaction
233
+ transaction = self.find_transaction(transaction_id)
234
+
235
+ # Perform transaction
236
+ self.perform_transaction(transaction_id)
237
+
238
+ return {
239
+ 'transaction': transaction['id'],
240
+ 'state': transaction['state'],
241
+ 'perform_time': transaction.get('perform_time', 0),
242
+ }
243
+
244
+ def _handle_check_transaction(
245
+ self, params: Dict[str, Any]
246
+ ) -> Dict[str, Any]:
247
+ """
248
+ Handle CheckTransaction method.
249
+
250
+ Args:
251
+ params: Method parameters
252
+
253
+ Returns:
254
+ Dict containing the response
255
+ """
256
+ transaction_id = params.get('id')
257
+
258
+ # Find transaction
259
+ transaction = self.find_transaction(transaction_id)
260
+
261
+ return {
262
+ 'transaction': transaction['id'],
263
+ 'state': transaction['state'],
264
+ 'create_time': transaction['create_time'],
265
+ 'perform_time': transaction.get('perform_time', 0),
266
+ 'cancel_time': transaction.get('cancel_time', 0),
267
+ 'reason': transaction.get('reason'),
268
+ }
269
+
270
+ def _cancel_response(self, transaction: Dict[str, Any]) -> Dict[str, Any]:
271
+ """
272
+ Helper method to generate cancel transaction response.
273
+
274
+ Args:
275
+ transaction: Transaction data
276
+
277
+ Returns:
278
+ Dict containing the response
279
+ """
280
+ return {
281
+ 'transaction': transaction['id'],
282
+ 'state': transaction['state'],
283
+ 'cancel_time': transaction.get('cancel_time', 0),
284
+ }
285
+
286
+ def _handle_cancel_transaction(
287
+ self, params: Dict[str, Any]
288
+ ) -> Dict[str, Any]:
289
+ """
290
+ Handle CancelTransaction method.
291
+
292
+ Args:
293
+ params: Method parameters
294
+
295
+ Returns:
296
+ Dict containing the response
297
+ """
298
+ transaction_id = params.get('id')
299
+ reason = params.get(
300
+ 'reason', PaymeCancelReason.REASON_MERCHANT_DECISION
301
+ )
302
+
303
+ # Find transaction
304
+ transaction = self.find_transaction(transaction_id)
305
+
306
+ # Check if transaction is already cancelled
307
+ canceled_states = [
308
+ TransactionState.CANCELED.value,
309
+ TransactionState.CANCELED_DURING_INIT.value
310
+ ]
311
+ if transaction.get('state') in canceled_states:
312
+ # If transaction is already cancelled, return the existing data
313
+ return self._cancel_response(transaction)
314
+
315
+ # Check if transaction can be cancelled based on its current state
316
+ if transaction.get('state') == TransactionState.SUCCESSFULLY.value:
317
+ # Transaction was successfully performed, can be cancelled
318
+ pass
319
+ elif transaction.get('state') == TransactionState.INITIATING.value:
320
+ # Transaction is in initiating state, can be cancelled
321
+ pass
322
+ else:
323
+ # If transaction is in another state, it cannot be cancelled
324
+ raise TransactionCancelled(
325
+ f"Transaction {transaction_id} cannot be cancelled"
326
+ )
327
+
328
+ # Cancel transaction
329
+ self.cancel_transaction(transaction_id, reason)
330
+
331
+ # Get updated transaction
332
+ updated_transaction = self.find_transaction(transaction_id)
333
+
334
+ # Return cancel response
335
+ return self._cancel_response(updated_transaction)
336
+
337
+ def _handle_get_statement(self, params: Dict[str, Any]) -> Dict[str, Any]:
338
+ """
339
+ Handle GetStatement method.
340
+
341
+ Args:
342
+ params: Method parameters
343
+
344
+ Returns:
345
+ Dict containing the response
346
+ """
347
+ if not self.get_statement:
348
+ raise MethodNotFound("GetStatement method not implemented")
349
+
350
+ from_date = params.get('from')
351
+ to_date = params.get('to')
352
+
353
+ # Get statement
354
+ transactions = self.get_statement(from_date, to_date)
355
+
356
+ return {'transactions': transactions}
357
+
358
+ def _handle_set_fiscal_data(
359
+ self, params: Dict[str, Any]
360
+ ) -> Dict[str, Any]:
361
+ """
362
+ Handle SetFiscalData method.
363
+
364
+ Args:
365
+ params: Method parameters
366
+
367
+ Returns:
368
+ Dict containing the response
369
+ """
370
+ if not self.set_fiscal_data:
371
+ raise MethodNotFound("SetFiscalData method not implemented")
372
+
373
+ transaction_id = params.get('id')
374
+ fiscal_data = params.get('fiscal_data', {})
375
+
376
+ # Set fiscal data
377
+ success = self.set_fiscal_data(transaction_id, fiscal_data)
378
+
379
+ return {'success': success}
File without changes