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
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
|
gateways/click/client.py
ADDED
|
@@ -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
|